|
7 S6 ~' J, D. e! [* j: F# C' } 一、前言本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,不用重启项目,以及在页面分配给用户 角色 、 按钮 、uri 权限后,后端动态分配权限,用户无需在页面重新登录才能获取最新权限,一切权限动态加载,灵活配置 : u$ o2 C0 H- U' u5 X5 L; D1 K
基本环境spring-boot 2.1.7mybatis-plus 2.1.0mysql 5.7.24redis 5.0.5温馨小提示:案例demo源码附文章末尾,有需要的小伙伴们可参考哦 ~二、SpringBoot集成Shiro
# ?$ {* L$ c) h, q% F+ Y. f 1、引入相关maven依赖
" ~6 h" b, z2 U/ {0 j 1.4.0
1 Q1 a, j' P; p. I7 o( x 3.1.0
. R* t& L) {# M. A) b. A! A" p7 I- x7 j8 `0 p
; Q3 @* U4 s; D
7 G, ~- l! D6 E& D( A4 ~8 R" d8 L w
org.springframework.boot
& U& }0 i, q/ m+ a4 Z" c spring-boot-starter-aop
C# c/ a" z3 H& q" z# g! `+ E0 G" S. M
4 k5 D- f7 B2 f/ }! ^7 j- _" W- t* ^7 O; a. s, k7 K+ Z# F
! t: P5 a, t. g9 U& r
org.springframework.boot
9 G7 I4 g' }7 q; ^2 d; {9 J spring-boot-starter-data-redis-reactive" y( @1 H* [; g; y
& ~# o$ I! H8 D6 v" O
) a: k7 g) b2 C4 T) B6 B6 i
. ~9 } ^% n: i+ z1 q$ k: `8 U: e org.apache.shiro5 ~5 s1 l9 \0 {8 l2 _
shiro-spring/ w) q0 J% {0 l& ~1 g+ y
${shiro-spring.version}. ]3 I# m7 f9 C4 e. j
2 g5 s5 i6 v. f) q: n$ [ T% |+ Y* S5 R7 I* d7 g; C) k
; _: q" g, y2 g2 j1 t' U& J
org.crazycake! u. U1 e4 T5 C8 b+ M1 p! {9 F
shiro-redis$ M5 Y, w' A3 J3 u; W3 f( W
${shiro-redis.version}
) A0 H J# h: W: d8 x4 `
8 E k w& D5 _3 T' D" e
0 a& m$ \* q& F0 k$ {+ _: s0 ] $ Q6 x: K8 U3 C4 L1 x( O
2、自定义RealmdoGetAuthenticationInfo:身份认证 (主要是在登录时的逻辑处理)doGetAuthorizationInfo:登陆认证成功后的处理 ex: 赋予角色和权限【 注:用户进行权限验证时 Shiro会去缓存中找,如果查不到数据,会执行doGetAuthorizationInfo这个方法去查权限,并放入缓存中 】 -> 因此我们在前端页面分配用户权限时 执行清除shiro缓存的方法即可实现动态分配用户权限
8 E+ J5 ?0 G4 f! J6 u8 h$ D: X* a8 g @Slf4j
X% K0 a9 n5 C( h8 @" R& ? public class ShiroRealm extends AuthorizingRealm {9 a9 t5 S7 z Q4 H
@Autowired6 Y3 U" ], X! H; t0 q
private UserMapper userMapper;$ ]1 h4 u6 `7 z2 @9 P) Q
@Autowired
3 l' l, K7 V" k! g; V4 g5 _ private MenuMapper menuMapper;
. c. ?3 F% E# h3 {. A7 ]# J @Autowired
0 u) J1 D/ `2 q" j; \8 x+ [ private RoleMapper roleMapper;0 L: M/ I9 `5 d& X: R; [
@Override9 T- v% r2 o, F& q4 g
public String getName() {
4 A4 O& E5 R7 c, m return "shiroRealm";
- u; m3 w) T. {# l- Q) k) i" s" w }' y7 W( h3 Z/ j
/**
9 z" y" V" a7 |1 p * 赋予角色和权限:用户进行权限验证时 Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
) S4 r/ g" E4 ] */
. o( f4 @! J! o/ A2 e/ ` @Override; R- B5 y8 j5 Q% Y: e7 M/ _% B
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
( Q& l& G8 ~) O$ L SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- A2 i0 [, }4 B7 |8 m* `8 e3 p- \ // 获取用户
5 W/ O$ l4 I3 j7 K D User user = (User) principalCollection.getPrimaryPrincipal();
6 w- q5 B1 e+ T0 g3 L/ u Integer userId =user.getId();$ T6 K3 V. ^) A
// 这里可以进行授权和处理7 o) V& g: l# x4 ]& [% m
Set rolesSet = new HashSet<>();& F: }; S' j/ _$ A' |0 q# }
Set permsSet = new HashSet<>();
& _) Q9 _8 a( h; c2 A // 获取当前用户对应的权限(这里根据业务自行查询)
/ N9 O% V% S6 t+ ], f) N0 Y. w, w List roleList = roleMapper.selectRoleByUserId( userId );" Y* F0 B5 Q J2 j0 V0 G: V
for (Role role:roleList) {
! {$ s! b5 d3 I+ h M rolesSet.add( role.getCode() );
4 c7 O3 I" k$ X, X8 r1 F1 ]! d List menuList = menuMapper.selectMenuByRoleId( role.getId() );9 b+ ]5 D8 D4 T# y0 }# C5 m
for (Menu menu :menuList) {
0 K/ K& e, _+ p0 @ q7 b b$ f0 p( ~ permsSet.add( menu.getResources() );' o! ]3 l# Z9 q( F6 ^9 c
}" `" M5 w' a0 }/ |/ `/ B
}+ k/ i6 l3 E0 i1 O ~
//将查到的权限和角色分别传入authorizationInfo中
1 @0 j& M+ m" j; { authorizationInfo.setStringPermissions(permsSet);: o1 }8 ^+ w5 ^+ P
authorizationInfo.setRoles(rolesSet);, t p" [' Q* q/ p2 @. ]' B
log.info("--------------- 赋予角色和权限成功! ---------------");8 b( |, i( W l0 B# |5 x9 N
return authorizationInfo;
. x* X. Y% e5 A5 U. _/ M: r }
3 p+ A) r0 b& `& P /**' C% v! r0 j6 _7 |1 M
* 身份认证 - 之后走上面的 授权4 |, n0 _1 F j& _2 Y7 Z& [0 k/ J$ A
*/6 m- C( }, E2 F. I3 }# c
@Override. O" K8 o: B6 @" G0 R! ~- ^
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {' y- ~. h' T: h& E
UsernamePasswordToken tokenInfo = (UsernamePasswordToken)authenticationToken;
8 G7 c4 q/ l$ e& {8 C4 {; | // 获取用户输入的账号- K* i2 B* {: A( @
String username = tokenInfo.getUsername();; G, a" P: p7 ]/ |8 L
// 获取用户输入的密码
: ]& E3 q0 B3 }9 ?. } String password = String.valueOf( tokenInfo.getPassword() );
9 \" g* g5 G# ]# Y6 d( _% w // 通过username从数据库中查找 User对象,如果找到进行验证9 x3 t& B6 ^2 O+ r
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
, H) S% O& ~4 y- A% }/ i User user = userMapper.selectUserByUsername(username);
9 I4 e& p m- q) e* {) k: R // 判断账号是否存在
3 H. W# h0 Z) q6 y$ {2 h: h if (user == null) {
' I; m1 ?! v, p! F //返回null -> shiro就会知道这是用户不存在的异常
P: Q, p" v6 J% a; h& _ return null;+ f' v a% A8 M7 s
}
, R6 ?0 ^8 \( I4 P+ `8 }; g, M // 验证密码 【注:这里不采用shiro自身密码验证 , 采用的话会导致用户登录密码错误时,已登录的账号也会自动下线! 如果采用,移除下面的清除缓存到登录处 处理】) z& [6 _# R( p! o: l7 c
if ( !password.equals( user.getPwd() ) ){
' Y, F9 i# ^' u throw new IncorrectCredentialsException("用户名或者密码错误");
3 C- x5 a3 b! G( x }' @- x) d. r* H+ Q/ t
// 判断账号是否被冻结( J4 {+ ?( P$ ~8 y) A/ D8 w8 r
if (user.getFlag()==null|| "0".equals(user.getFlag())){; |# n. B, x2 v4 R1 x) m
throw new LockedAccountException();
% Y: P) w5 z' g. t5 L } j5 H9 b3 N9 M- U
/**" y0 {1 \5 L! t' P+ H! e5 e1 M% o
* 进行验证 -> 注:shiro会自动验证密码
/ L& ?- }$ \3 U7 H0 p* i0 ]# p0 {, A * 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值
- Y3 _* |+ O% T * 参数2:hashedCredentials -> 密码
% l: `$ F: _8 i7 a3 p, F; s6 P* a * 参数3:credentialsSalt -> 设置盐值
+ b+ t0 ]# Q' r) w * 参数4:realmName -> 自定义的Realm- x! m; v e( ]$ F# Z# f( y4 s% c
*/
$ n. l8 m- O( h2 n SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
4 i3 R$ X9 P& T. I5 b5 P // 验证成功开始踢人(清除缓存和Session)
$ p6 |, U$ ]0 ~+ a" I4 x ShiroUtils.deleteCache(username,true);
# k$ J& p. T- C) y9 v9 g8 Y; T // 认证成功后更新token
. e& p: u' }$ a/ Z; w String token = ShiroUtils.getSession().getId().toString();
) h1 u. S$ V2 B+ C" c' ~; l& { user.setToken( token );9 e4 b9 |2 {+ A1 c7 }2 G
userMapper.updateById(user);! t: Y$ P1 `0 V- W
return authenticationInfo;0 e6 D" S5 g, X7 j/ y6 R
}
# Q) O6 p* |* f) n }; c$ ?8 v9 f/ V
$ S$ ]0 {8 L2 `3 w. K! W# @
3、Shiro配置类@Configuration7 x5 T! l/ ^# X- u5 x+ R, E! A" ^2 H/ Y
public class ShiroConfig {9 }4 g+ j' j# P
private final String CACHE_KEY = "shiro:cache:";
8 Z) V" m* A$ X/ J% B$ b J$ A5 ?2 C% f private final String SESSION_KEY = "shiro:session:";
" F! S+ R2 t* j& P /**& V U' m& @" X9 D1 q) Y
* 默认过期时间30分钟,即在30分钟内不进行操作则清空缓存信息,页面即会提醒重新登录
# W9 r/ u9 O8 p, R7 L3 g */
6 W; A: i" p! {% W private final int EXPIRE = 1800;* U* o! E" S4 s% H9 r( Z
/**
$ _: P: f d! c* p' N * Redis配置
! N: E! V$ ?" z8 {* ~. r */
6 k6 I, ?! C+ w" y, `5 J @Value("${spring.redis.host}")% x4 q$ B7 J. G# f5 J# M
private String host;
( d3 M8 Y) ]2 [- a' K& G/ I# U6 H @Value("${spring.redis.port}")
* c# `0 R3 w [, Z private int port;7 P: _. E$ q& \$ e% a5 Z
@Value("${spring.redis.timeout}")
; q9 Q+ z: {' S; R& L! C private int timeout;3 O% e4 i) k1 v6 c' [$ [/ r$ z, a8 w
// @Value("${spring.redis.password}")
* }8 C: I) w( B // private String password;
f6 K' t, c$ I, f( d/ w /**: f9 u5 T a0 r {5 O
* 开启Shiro-aop注解支持:使用代理方式所以需要开启代码支持
! f3 A( N0 t( I, j5 L; p */# r$ V V$ t2 O9 T
@Bean/ e( i9 M# r9 J" a& S# f* B5 {
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {* @# e/ M; r" c
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();" R: t$ _* A5 T& {2 I( }+ y1 N
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
8 A# \8 Q* G \7 X return authorizationAttributeSourceAdvisor;
- }- C, h2 R9 B7 W }
' y; @: E- v7 u: C* t1 {( O /**
5 ?5 [/ T; P; s6 M s" h/ M& ]; I * Shiro基础配置 O6 h Z! \6 T, B! ?
*/
% F$ Q* y# `" L* T( r$ I @Bean: r$ M* ^) t3 f" q- K+ ~( `4 `8 c+ l
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager, ShiroServiceImpl shiroConfig){) f( M, X- n% b- {' b# y3 J/ o
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
4 R4 H: i* S8 d9 J, j shiroFilterFactoryBean.setSecurityManager(securityManager);
3 ?' U+ X( O+ t" C // 自定义过滤器' P4 Y& J/ K/ \! H' i) {# H- V5 O
Map filtersMap = new LinkedHashMap<>();- F. K7 Z6 ]# J/ h0 v# H( C
// 定义过滤器名称 【注:map里面key值对于的value要为authc才能使用自定义的过滤器】
& y( L3 j* V) u$ v5 ` filtersMap.put( "zqPerms", new MyPermissionsAuthorizationFilter() );- J' B9 A4 p* o8 t% U* r' R. p( _
filtersMap.put( "zqRoles", new MyRolesAuthorizationFilter() );
( f% u8 m' V. i2 ?* [ filtersMap.put( "token", new TokenCheckFilter() );
7 o7 }7 v2 r; n* \3 F( d$ L shiroFilterFactoryBean.setFilters(filtersMap);( i/ h: R) Y$ X& | F- S7 ?
// 登录的路径: 如果你没有登录则会跳到这个页面中 - 如果没有设置值则会默认跳转到工程根目录下的"/login.jsp"页面 或 "/login" 映射
0 d$ R& e! ^2 @- M& ? shiroFilterFactoryBean.setLoginUrl("/api/auth/unLogin");: u8 s l; ]9 F6 x. `) X" m- ~
// 登录成功后跳转的主页面 (这里没用,前端vue控制了跳转)
1 R* z' G. S$ R2 [7 z( T1 t // shiroFilterFactoryBean.setSuccessUrl("/index");1 ^% L, [2 ~& u7 M# E# E6 b. ]8 }/ y
// 设置没有权限时跳转的url
& {+ h# V$ `! z/ B shiroFilterFactoryBean.setUnauthorizedUrl("/api/auth/unauth");
( N: _2 G$ A, y$ p) E shiroFilterFactoryBean.setFilterChainDefinitionMap( shiroConfig.loadFilterChainDefinitionMap() );
' U. ^$ q7 B' a9 W- A return shiroFilterFactoryBean;0 j s5 {+ w( H! t: Z
}
- X" U8 Y" b7 {9 X2 ~$ C& P /**2 L: U6 W; q5 o" Z7 Z
* 安全管理器
. o8 Y. S# L; a/ z4 ? */3 e1 Z0 c/ k7 t" ]" d
@Bean
, l# i* L/ u) \* o/ |5 \5 e public SecurityManager securityManager() {
8 P4 A$ D/ {1 K& w0 O9 o DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
& E0 c& w' a5 R8 z( ^. x // 自定义session管理
4 e$ f& w! [" o* q, ~# s6 B/ K securityManager.setSessionManager(sessionManager());* }& n/ J) g ], K' N& o
// 自定义Cache实现缓存管理3 }# Q& r% R6 L5 @. |5 S
securityManager.setCacheManager(cacheManager());
# S3 e5 C2 G k8 S // 自定义Realm验证9 ^4 J$ ?/ b1 k" x- t
securityManager.setRealm(shiroRealm());
2 Z( Y! \1 }6 I1 M8 j return securityManager;5 U' o' p" j6 a' }4 W# h
}9 Z" [5 a' q4 T9 }
/**
6 U* y* b. {7 A5 k! _$ w1 [ * 身份验证器
0 `, I: z* K7 r- H */6 P6 ]" g$ A, j. B" m- {
@Bean
2 ~7 U( v I, O public ShiroRealm shiroRealm() {
, u. J7 m5 @' ^! i' ]$ `5 N ShiroRealm shiroRealm = new ShiroRealm();
9 }7 r/ V7 g# y. P- Z a shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());* x" `! b. Y8 G" b C. ~2 p% V+ I& N
return shiroRealm;
! v2 Z/ V& N# H: `3 k }; r3 ]2 x; H. [- ^+ f' ~% A
/**: G/ L U3 |) q) h
* 自定义Realm的加密规则 -> 凭证匹配器:将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置0 y, R! ~ n* V* N/ K" f9 F$ O: Y
*/0 `$ k0 b) v7 O/ U
@Bean$ q+ d7 S3 u) R5 q9 J1 _5 ^/ z9 d
public HashedCredentialsMatcher hashedCredentialsMatcher() {
# M9 M9 g8 A k( P$ C HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();: \8 r- X9 f8 ?* }' z- y& g
// 散列算法:这里使用SHA256算法;
+ R' a t `9 o! g. [+ b, W shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
4 h0 q7 ]) O# T& C // 散列的次数,比如散列两次,相当于 md5(md5(""));+ T5 l$ ]9 a$ w m
shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);# Q$ ~1 A7 v) E! U
return shaCredentialsMatcher;' F+ G Q4 e3 n6 a) `/ Q
}
6 V! B+ Y5 |# g# \) @$ m3 F0 T /**
1 n7 W! x G$ D# y9 d" N0 G * 配置Redis管理器:使用的是shiro-redis开源插件
% X3 N' F: r- a! q */8 x# S" T4 W- N& ~/ S( l
@Bean1 D9 y. {+ O. ]; g
public RedisManager redisManager() {
( F; g5 L1 o7 A4 ^" L( } RedisManager redisManager = new RedisManager();; H. t2 S" c" W
redisManager.setHost(host);( x& H* f9 N4 J$ X/ K! M
redisManager.setPort(port);7 e8 A% ]6 O y( R: b3 n
redisManager.setTimeout(timeout);
, J6 C% B; i c- r$ X9 o+ ?6 G // redisManager.setPassword(password);
2 `7 ?; o. h* y% k. G# S return redisManager;( [& `& y) F; i7 C3 h
}
# ^. t' I" M9 `! }( a /**
' r6 u. t6 Z/ l& `* s4 q) i * 配置Cache管理器:用于往Redis存储权限和角色标识 (使用的是shiro-redis开源插件)/ t/ Y1 R/ u" ^* j a/ W
*/
5 E. ^' f9 _ P$ T; V @Bean
, Q2 O1 x' a* [! S* D# x public RedisCacheManager cacheManager() {+ q! }5 s2 Q, h2 p
RedisCacheManager redisCacheManager = new RedisCacheManager();- h; \6 h v, J2 G
redisCacheManager.setRedisManager(redisManager());; [. P# E3 ?% s
redisCacheManager.setKeyPrefix(CACHE_KEY);+ h5 ]2 T3 a1 `5 q
// 配置缓存的话要求放在session里面的实体类必须有个id标识 注:这里id为用户表中的主键,否-> 报:User must has getter for field: xx8 |9 [+ e- X: j3 i2 U3 |6 x$ B
redisCacheManager.setPrincipalIdFieldName("id");
- J1 _' D3 L. k" a; m return redisCacheManager;
& G% H* k. z( a% Y3 k% Y, L }
2 I+ p1 h2 @; j$ z, x1 ]% [; W /**4 }: m: c# x$ f6 y
* SessionID生成器# `: X- u) [: a$ ?9 X' V
*/' H+ |& X- M5 O' M, E
@Bean
, _ Y( L4 p, A: Q5 K* D public ShiroSessionIdGenerator sessionIdGenerator(){/ m' C6 l. n& ^! M/ i
return new ShiroSessionIdGenerator();
! a, g s5 p1 ]6 z- q }8 M( F* C" M) w e
/**
# I5 `( K5 s) I p8 ]" ` * 配置RedisSessionDAO (使用的是shiro-redis开源插件)) a( g2 I( y2 W3 ]& ~; I- R- D
*/
6 I* G S% y7 C/ B @Bean+ O: |! T a! J2 M. R
public RedisSessionDAO redisSessionDAO() {
6 ]* L* r2 G6 D! `: ]7 Q. v" M/ A RedisSessionDAO redisSessionDAO = new RedisSessionDAO();6 O4 N) I3 o+ l9 k$ X+ \6 s6 M( t$ z
redisSessionDAO.setRedisManager(redisManager());
' ]! A+ R9 u) r redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
' t% m4 u6 q, n redisSessionDAO.setKeyPrefix(SESSION_KEY);
E4 U6 }" J0 u0 P6 H redisSessionDAO.setExpire(EXPIRE);% G) G2 Y# ^& y8 W
return redisSessionDAO;/ I# b* l6 i$ t1 h: D
}7 ]$ H0 B8 l, V( c* @) O
/**# W* J7 \4 Y/ m4 n7 J
* 配置Session管理器
8 I' a- }( \% D */
- S* W: F. q% [' w6 s @Bean
% k( y7 m0 ~: O9 V2 k+ @ public SessionManager sessionManager() {
7 m6 P3 _, V: a) j1 i ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
! O3 ?! k& z1 a- b0 z% Q9 t5 _8 u shiroSessionManager.setSessionDAO(redisSessionDAO());0 X$ J8 [: _/ a- N$ {- {0 ^' `; h
return shiroSessionManager;
9 ]% G2 H- [% _8 B Q& O }0 z2 r0 [( O0 T0 l( I/ A1 H+ b
}
`5 d1 ^: T B% d6 ^4 c2 h _" Q3 e: M* A) S
三、shiro动态加载权限处理方**oadFilterChainDefinitionMap:初始化权限ex: 在上面Shiro配置类ShiroConfig中的Shiro基础配置shiroFilterFactory方法中我们就需要调用此方法将数据库中配置的所有uri权限全部加载进去,以及放行接口和配置权限过滤器等
3 I# \% |% W6 W/ s) G: Q 【注:过滤器配置顺序不能颠倒,多个过滤器用 , 分割】ex: filterChainDefinitionMap.put("/api/system/user/list", "authc,token,zqPerms[user1]")updatePermission:动态刷新加载数据库中的uri权限 -> 页面在新增uri路径到数据库中,也就是配置新的权限时就可以调用此方法实现动态加载uri权限
# H# y Y: f; e( {% g updatePermissionByRoleId:shiro动态权限加载 -> 即分配指定用户权限时可调用此方法删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限
& w7 J& a( r. F+ Q' r- C0 h. U public interface ShiroService {; N0 m( U3 X. t; G
/**
, A0 H, n6 Z( U) ` * 初始化权限 -> 拿全部权限) E, J* `, R1 x* h
*
0 L# D8 {4 `8 U" U * @param :- F: Y$ s8 s- i" Z. L
* @return: java.util.Map
/ f$ y, k6 z) F6 U9 U */
1 B7 I2 L7 r0 o* p' Q+ z* w7 [ Map loadFilterChainDefinitionMap();0 h, `, b9 k: N9 `+ M3 C
/**+ S. \. F" G, O0 ?. V8 W' Z
* 在对uri权限进行增删改操作时,需要调用此方法进行动态刷新加载数据库中的uri权限, X. M+ _5 ^! w8 H, J
*
* [/ r9 }; x f- M9 i. k/ o * @param shiroFilterFactoryBean
9 u: h1 v8 {3 a% a8 ~2 U! H * @param roleId
6 W) \4 H' o; z% w: {: d8 ^ * @param isRemoveSession:
+ q( R8 a8 y! i' T$ [( _& d# ] * @return: void
6 V, ], ]3 _* I) N }. T6 w1 \9 | *// c" |; y! F) c4 z1 s7 i
void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession);, e8 J C& `; A
/**
! v( v9 O. Z2 T- p4 `( f$ k * shiro动态权限加载 -> 原理:删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限
& C) k" J* x) A% ?" ? *
6 u0 k. J4 k" A% w1 f3 |- F * @param roleId
% C/ C L+ |& b* L! I, L7 A$ L+ p * @param isRemoveSession:0 F8 M$ l: @1 l$ g% S
* @return: void. w* j3 a6 L9 Y0 p1 X( A8 ~
*/1 V' b5 W O9 f% k6 ]8 h
void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession);/ I. D5 _2 a& o5 u6 R
}. u* j6 x3 g. g2 D7 H6 ^) l
@Slf4j; D% ]+ p9 J9 m4 ^, _2 Z& F4 y
@Service7 v# I# O2 `. L _) c' F
public class ShiroServiceImpl implements ShiroService {
& V' t1 {) j( \7 [& s$ Z @Autowired: m7 W: A% u5 V7 @$ M+ ?) ~% k* k
private MenuMapper menuMapper;
0 T9 Z3 E2 p T6 M @Autowired9 F% O* b( X9 M# x3 H
private UserMapper userMapper;
9 R9 h8 ^& f9 N- A1 e @Autowired
3 m: v5 o g- a private RoleMapper roleMapper;9 i: h' k5 z! K* k# ]$ c: [
@Override
8 B2 i) f* i5 m6 H6 H; K8 r public Map loadFilterChainDefinitionMap() {
5 A; I/ M. y& R, i1 O // 权限控制map2 U. e! Q2 |0 ?! W u
Map filterChainDefinitionMap = new LinkedHashMap<>();
2 U" k% S' o) t5 L" K& p+ E // 配置过滤:不会被拦截的链接 -> 放行 start ----------------------------------------------------------
8 d* J: @5 \' N2 M' A // 放行Swagger2页面,需要放行这些
" S6 x5 V- S* r& {6 K% p$ d" D filterChainDefinitionMap.put("/swagger-ui.html","anon");
: c* O4 B& g! _3 ?0 ` F" ?7 Z' L filterChainDefinitionMap.put("/swagger/**","anon");* t- @0 J7 H4 N" _; T
filterChainDefinitionMap.put("/webjars/**", "anon");$ c& d" l: t2 E0 k
filterChainDefinitionMap.put("/swagger-resources/**","anon");# {$ ~9 e2 H: R+ r
filterChainDefinitionMap.put("/v2/**","anon");
( S9 a5 I$ _4 t- F8 C& ]/ M4 o filterChainDefinitionMap.put("/static/**", "anon");
0 k4 K* ?1 W y9 G F- B& ^' B% o // 登陆
1 O( S8 F* r' X! i filterChainDefinitionMap.put("/api/auth/login/**", "anon");
6 E( N- J0 F* ~+ U% v; K // 三方登录' \4 A9 ^) i2 i9 ]2 N
filterChainDefinitionMap.put("/api/auth/loginByQQ", "anon");3 H8 }( u' u, c1 J* D6 g2 R+ F
filterChainDefinitionMap.put("/api/auth/afterlogin.do", "anon");
% ~! E0 R U( j/ @$ t, E // 退出
2 K; T0 j2 t1 m* N" L filterChainDefinitionMap.put("/api/auth/logout", "anon");
6 ~+ k' I. V4 ~( p0 e // 放行未授权接口,重定向使用, M7 p" I) y) L+ W9 ]. q) M
filterChainDefinitionMap.put("/api/auth/unauth", "anon");' [4 K5 R) B k& U
// token过期接口
- @7 J1 P2 k4 b9 ^ filterChainDefinitionMap.put("/api/auth/tokenExpired", "anon");
% H6 G6 w0 \% j9 R; c# q; k7 u // 被挤下线
% y' R; _5 U! |' L c5 D% N filterChainDefinitionMap.put("/api/auth/downline", "anon");6 j' R. Q6 ^/ {! Q3 n
// 放行 end ---------------------------------------------------------- i% E" }) W8 s. @+ L2 M
// 从数据库或缓存中查取出来的url与resources对应则不会被拦截 放行
S4 r3 E+ u2 t List permissionList = menuMapper.selectList( null );
7 d$ Q- v" y5 z4 {3 v: j Y# j if ( !CollectionUtils.isEmpty( permissionList ) ) {
& q0 K- s8 b$ }& m& \4 a permissionList.forEach( e -> {
5 K9 [" W) B# |& H2 O if ( StringUtils.isNotBlank( e.getUrl() ) ) {
/ l1 L( x) }. `3 R/ ^ // 根据url查询相关联的角色名,拼接自定义的角色权限
6 s0 Q: c( E5 V5 E0 |* l" J8 I# L List roleList = roleMapper.selectRoleByMenuId( e.getId() );' V N+ |, {5 V
StringJoiner zqRoles = new StringJoiner(",", "zqRoles[", "]");5 ?- `5 M1 [) ]0 Q( F/ C; C* J3 S
if ( !CollectionUtils.isEmpty( roleList ) ){# P' @5 g" x" h% h
roleList.forEach( f -> {
6 E7 D" T" q( F8 O% D9 v" p4 Z zqRoles.add( f.getCode() );! G* o' d# b- w) m/ i5 ?
});8 i Z% m1 C- @" z8 D# d
}
" F* v9 D8 Q$ l3 z" t. E8 @ // 注意过滤器配置顺序不能颠倒 M+ L# H0 W6 f- E( m
// ① 认证登录
9 B' N5 J5 Z0 K: Y7 H // ② 认证自定义的token过滤器 - 判断token是否有效" Y- O6 H, v: R# o5 P, t+ l- `- ~
// ③ 角色权限 zqRoles:自定义的只需要满足其中一个角色即可访问 ; roles[admin,guest] : 默认需要每个参数满足才算通过,相当于hasAllRoles()方法7 q( A% T3 F* w+ I5 r( Y8 g( Q
// ④ zqPerms:认证自定义的url过滤器拦截权限 【注:多个过滤器用 , 分割】0 J# K9 X! M# T$ Z: X
// filterChainDefinitionMap.put( "/api" + e.getUrl(),"authc,token,roles[admin,guest],zqPerms[" + e.getResources() + "]" );# m! a" V5 F) ?# M
filterChainDefinitionMap.put( "/api" + e.getUrl(),"authc,token,"+ zqRoles.toString() +",zqPerms[" + e.getResources() + "]" );1 O9 N* R/ f# y4 [: k( w
// filterChainDefinitionMap.put("/api/system/user/listPage", "authc,token,zqPerms[user1]"); // 写死的一种用法$ k; e0 Y$ c& k# Z. Y5 D4 y. G5 `' O' `
}
3 C" v6 e8 F7 g5 ` });: w3 g7 |& g/ }& H
}" @, }& n0 R; i
// ⑤ 认证登录 【注:map不能存放相同key】2 G$ _$ \( B. L' I. |
filterChainDefinitionMap.put("/**", "authc");
% A6 S3 E9 Y, V& W' k( A return filterChainDefinitionMap;4 }$ a3 Q7 M+ s6 W, V# X# p
}! {* z9 W6 s( N$ X: S- Q, U- ^& V
@Override
% t, r7 L, r% B9 k8 n public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession) {6 r2 S# a# [+ j* d: q) [0 i
synchronized (this) {
3 Z, E( R8 H" @ AbstractShiroFilter shiroFilter;3 c6 Z' U4 _' ^0 l, n/ \. p
try {5 `( \" G& O& U0 z* q0 B
shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject();
: N, _1 i1 d: M5 Q2 M0 r } catch (Exception e) {# [2 [1 Z2 `. }" k; V2 L ]/ r
throw new MyException("get ShiroFilter from shiroFilterFactoryBean error!");: [+ }6 j+ _" }$ d; E# u% N
}
/ C3 A) @$ @. G+ x0 z3 Q PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();* S8 E& P) F1 e% K' e! a9 |5 K- D5 j) Y
DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();0 a% _1 r' ?2 B) j& T) n
// 清空拦截管理器中的存储
, R+ X& e2 {" T2 R$ z R L manager.getFilterChains().clear();5 _8 U8 b# b* C4 t8 B# X& P$ G
// 清空拦截工厂中的存储,如果不清空这里,还会把之前的带进去
& Z2 {; n+ Y( W3 p* _8 J // ps:如果仅仅是更新的话,可以根据这里的 map 遍历数据修改,重新整理好权限再一起添加' Q* q n! F6 f! H4 j" W9 G
shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
0 o# l% `; b7 b1 f' O // 动态查询数据库中所有权限
6 |; H9 P* S9 s' c9 l shiroFilterFactoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitionMap());( k: V( |4 R0 g! X: T
// 重新构建生成拦截
5 B+ P/ l4 E5 J% h+ i; y8 p) E Map chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
" W( }" Q+ H6 Z/ m- E5 z7 i for (Map.Entry entry : chains.entrySet()) {
% [ D) d. k$ [# a# J! y$ i. p; J( ] manager.createChain(entry.getKey(), entry.getValue());( `' q5 l5 f4 p6 n. r5 V. b
}
- j. N) ^6 b) S( q/ A( Q3 t log.info("--------------- 动态生成url权限成功! ---------------");
( ~! o$ L& J) L4 Q$ ? // 动态更新该角色相关联的用户shiro权限6 F$ O: G T; C% A7 l# Y
if(roleId != null){
* F- b3 e9 _4 O( @; L3 m& z& e0 u updatePermissionByRoleId(roleId,isRemoveSession);8 N2 Z% I4 S1 {- k
}
6 {2 C. n8 ?. E+ e }
* o% g$ s( U2 B& g+ @/ P! _( \ }- Y, K' m& X5 r/ Y. D
@Override
( u. x m$ [0 Q2 E, r public void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession) {
# V3 z- v# X0 I' N, n# z+ u6 j* _$ w // 查询当前角色的用户shiro缓存信息 -> 实现动态权限
& j8 t3 _& r l' o: S0 o- D List userList = userMapper.selectUserByRoleId(roleId);
/ d6 u% Y3 Z0 B, F( A; x // 删除当前角色关联的用户缓存信息,用户再次访问接口时会重新授权 ; isRemoveSession为true时删除Session -> 即强制用户退出
% V) \" _; o+ ?% t. F; }9 h if ( !CollectionUtils.isEmpty( userList ) ) {: `; N! ~- T1 P$ P
for (User user : userList) {
' w# ]6 u5 M9 w6 q! N& b& h5 j9 s ShiroUtils.deleteCache(user.getUsername(), isRemoveSession);, k% ~- ^: y) O; y2 q
}
% s) Y3 N5 Y6 q3 _2 P }. H- _9 ]% _- o
log.info("--------------- 动态修改用户权限成功! ---------------");
" v0 ~& t3 H# J% r9 a, n, g2 z7 @ }
! b% t: I$ b1 y! v4 f' s- b6 `) u }
e. M2 C5 D4 O) a1 o) p! v6 \ ! O# T4 V0 X( }
四、shiro中自定义角色、权限过滤器1、自定义uri权限过滤器 zqPerms@Slf4j
/ f3 f8 \% o3 @# v public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {( y# ^! J! M1 X# ^
@Override
6 Q( U$ Q: s [0 {& n8 m/ B protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {) D1 A, A, m) m# h8 j
HttpServletRequest httpRequest = (HttpServletRequest) request;
7 L! l! j2 N2 n# O g. L HttpServletResponse httpResponse = (HttpServletResponse) response; n; K- z% Y! G. \
String requestUrl = httpRequest.getServletPath();! N: w) R, W7 X4 f# m4 ^4 @4 {
log.info("请求的url: " + requestUrl);
4 h* C/ o7 e/ { K // 检查是否拥有访问权限% W) {& p4 |. p, X$ V E2 O r
Subject subject = this.getSubject(request, response);1 g5 Q) f, X5 m4 W n. H
if (subject.getPrincipal() == null) {
/ {$ E6 d' {% m- T C( ^ this.saveRequestAndRedirectToLogin(request, response);1 R4 Y$ v! X. B, a9 t& r$ z- w
} else {8 U( Q$ p6 x E7 j8 K8 ]( y) x$ m ]' l
// 转换成http的请求和响应
, N6 o4 W; ]9 Z HttpServletRequest req = (HttpServletRequest) request;
* x4 |( F V# K( l9 E HttpServletResponse resp = (HttpServletResponse) response;
- `% r+ ~1 E- c# E) ^1 B; m // 获取请求头的值0 @" X& ?: c: i s" h+ `* V! H
String header = req.getHeader("X-Requested-With");+ B3 ^, _6 W: Q, Z$ V i5 u
// ajax 的请求头里有X-Requested-With: XMLHttpRequest 正常请求没有% ~7 h% f8 b0 X
if (header!=null && "XMLHttpRequest".equals(header)){
. T K* X$ _8 V5 Q9 y- `7 I" T8 k resp.setContentType("text/json,charset=UTF-8");
5 P( ] {$ t( J. E7 e/ ~8 c; S resp.getWriter().print("{\"success\":false,\"msg\":\"没有权限操作!\"}");0 h2 \3 U3 R: C5 `- U- I! K
}else { //正常请求* X# k2 z' C+ I/ N6 U& V$ p! ~
String unauthorizedUrl = this.getUnauthorizedUrl();
# R9 g+ F S, c/ Q if (StringUtils.hasText(unauthorizedUrl)) {' W* q" }3 V$ ~. v7 @4 a( ^. l4 n5 |+ M% @
WebUtils.issueRedirect(request, response, unauthorizedUrl);! {! O2 {9 {$ D3 g# _' T
} else {$ j2 ~- ~: I3 w3 a8 d" m( s
WebUtils.toHttp(response).sendError(401); r9 x& X+ S0 L( }' k
}5 K" S8 u& r7 {$ w
}
; z8 U- a4 k3 @' j0 [4 s. Q0 Q }- v, [/ J; D6 F1 b" G4 x
return false;
/ A1 w; K' Y6 _3 \. B7 l }1 k* k# ]; y6 q( y [
m% K" ? R: P }3 M6 I3 |; N9 ?) J0 j9 a1 B" m
+ b0 Z/ n: q$ P; a8 f3 N. G 2、自定义角色权限过滤器 zqRolesshiro原生的角色过滤器RolesAuthorizationFilter 默认是必须同时满足roles[admin,guest]才有权限,而自定义的zqRoles 只满足其中一个即可访问 2 i% y+ ] {7 s0 N( ~; e
ex: zqRoles[admin,guest]public class MyRolesAuthorizationFilter extends AuthorizationFilter {
B, Y- l( ~) C- c9 j) C a: I @Override
& T" C7 f: ~6 n protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {
$ b8 m5 ?+ W% Z" f Subject subject = getSubject(req, resp);
6 `3 J: e9 H; k0 Q String[] rolesArray = (String[]) mappedValue;" b# x, P" M" l+ J( i& g
// 没有角色限制,有权限访问
8 ? d5 t; \0 r if (rolesArray == null || rolesArray.length == 0) {/ R! Q2 ?5 L( E! J l. W- o
return true;: T3 O+ g. s4 l5 m
}
- Y: f7 a1 _$ d w* e) C for (int i = 0; i < rolesArray.length; i++) {
$ y8 ^0 _7 @* h# a/ Q //若当前用户是rolesArray中的任何一个,则有权限访问( p$ l. z) ?" Z" D
if (subject.hasRole(rolesArray[i])) {
2 v$ o1 E# j% m; L return true;
% Q. E! x! U9 m4 E% P }; I- @9 w1 ~# C" g( q
}3 |& l. q' s* a |9 z- \" g
return false;" X& x: [- ]- p
} H* C4 E' z. n8 l/ }8 R- h
}$ ^9 q. @5 u1 S7 a
; U- o6 p% F2 @2 P* q1 w2 r' I8 i
3、自定义token过滤器 token -> 判断token是否过期失效等@Slf4j+ h2 d" r; O! d; x
public class TokenCheckFilter extends UserFilter {- w: ~ x! c5 W- g ?& s: C
/**- p$ |9 x8 z, M( K7 [6 i* q, H
* token过期、失效
3 _" I+ ?4 x4 L5 X+ s */3 @. Z/ p9 X* ?5 D, H% }" o
private static final String TOKEN_EXPIRED_URL = "/api/auth/tokenExpired"; P C7 ]# N9 D% ]; W$ T. l
/**
8 Z% @, x: \( v" }, _ * 判断是否拥有权限 true:认证成功 false:认证失败
$ O1 R" C6 W' L$ U& p2 Q * mappedValue 访问该url时需要的权限) `6 |* a" F% A. z' B8 M
* subject.isPermitted 判断访问的用户是否拥有mappedValue权限
/ ~3 Q+ n f, D5 s& d! b */
7 e# R: e' O. F; {, |1 e @Override) f$ O* ~9 t( W/ D
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {: L9 [# t3 L. d/ ]$ ]6 ^: P. x* c
HttpServletRequest httpRequest = (HttpServletRequest) request;
0 ]/ t6 H& h. F0 { HttpServletResponse httpResponse = (HttpServletResponse) response; z4 z% P [+ b: x! R% w% g, D7 g: x
// 根据请求头拿到token K6 Y7 B# X) S7 l( r
String token = WebUtils.toHttp(request).getHeader(Constants.REQUEST_HEADER);
3 D8 o$ _6 q% `' Q8 S+ W log.info("浏览器token:" + token );
( ?: D% \2 H- \ User userInfo = ShiroUtils.getUserInfo();
+ o3 q5 V! o1 n String userToken = userInfo.getToken();, w, b2 q0 [; D7 u* G
// 检查token是否过期1 a' n& |2 N* G/ Z e* Y; z
if ( !token.equals(userToken) ){
9 k& O( a' N6 U O" A8 \& A/ ` return false;
. ?; Y4 O, p4 ]0 ? }$ h# o2 c, M7 d0 T" p3 ~/ {
return true;' N+ C' j# u; A+ X
}
' D, t& E; l4 Q3 E. t: L# G8 U0 f' C /**
9 B6 e4 n$ y, X3 x * 认证失败回调的方法: 如果登录实体为null就保存请求和跳转登录页面,否则就跳转无权限配置页面
) I4 [9 H! s) ^& } p */
2 r6 A+ b7 j3 ^$ k* Z8 F' B( M @Override
* ]- ~! r# e3 u' P protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
& m' U! J6 o c- d User userInfo = ShiroUtils.getUserInfo();6 P2 I- d+ r% f; p7 B
// 重定向错误提示处理 - 前后端分离情况下5 u. T: t: F3 U+ {0 i; r9 B
WebUtils.issueRedirect(request, response, TOKEN_EXPIRED_URL);
! m* l! B4 C6 m# X; A3 l return false;! g* P# C1 x4 t7 h3 }4 S& g
}2 f# [7 a! g$ H9 E
}
/ `0 ~3 v1 P- U0 J- v4 j$ `4 v ; G7 p! S! R# I( b5 @- S% g
五、项目中会用到的一些工具类、常量等温馨小提示:这里只是部分,详情可参考文章末尾给出的案例demo源码1、Shiro工具类public class ShiroUtils {
" A! g8 G5 P8 ] /** 私有构造器 **/, i9 A7 c4 \) z, u& q, g
private ShiroUtils(){ }! D1 C3 I3 I: C+ `7 \
private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
/ a$ k' J! S6 U S5 d* r A /**
# k/ e7 H' ]; R1 I * 获取当前用户Session
j' [/ b4 ~, @- j4 ] * @Return SysUserEntity 用户信息4 _/ b* E- i! {% V$ ^/ D
*/; Z$ F" T4 N, X
public static Session getSession() {* w$ E% C! X* v; E. K" D4 I) s& D/ H
return SecurityUtils.getSubject().getSession();0 P7 P* [/ Q3 h$ P
}
& }0 H) h2 I+ L/ W1 B; |3 E /**+ n* `. l b7 _* L
* 用户登出
) o2 i$ j$ I* [# M& r */. u+ p J3 ]. R- D' |* a! Y
public static void logout() {
5 X; S. ? d4 \! Y SecurityUtils.getSubject().logout();/ k# z6 _: h( U! w1 N. g' G
}
. Q8 i, |& S: ~2 V& ~ /**
. S5 I; J% A" _. p8 ] * 获取当前用户信息
7 u& T3 ^! C" c, Q0 r * @Return SysUserEntity 用户信息2 }% v( B$ ~9 L/ y
*/
1 y( r; U. _0 z# j0 J! h" O public static User getUserInfo() {! T2 i6 N7 F3 T2 h- J7 o
return (User) SecurityUtils.getSubject().getPrincipal();
% O4 x$ T& Z- P( J) V& |. u }
) F/ A* x* n) E% p2 M6 Z /**; J! j+ y3 R! E
* 删除用户缓存信息
2 H( Y, m3 j$ B * @Param username 用户名称2 p% Z) p8 G, x9 \9 I8 Y
* @Param isRemoveSession 是否删除Session,删除后用户需重新登录7 R+ a$ L4 k0 @6 A2 k3 ?' G
*/
' V* z+ \" q/ k* P) P public static void deleteCache(String username, boolean isRemoveSession){9 s; j: o4 A6 c( V
//从缓存中获取Session
4 N1 h: v9 r5 a c# C! ^ Session session = null;
/ z# t9 X' m1 I1 F // 获取当前已登录的用户session列表6 H- E) a b& h: o, n
Collection sessions = redisSessionDAO.getActiveSessions();
' k' O6 o9 W. t5 o User sysUserEntity;! L. L# |% W6 P) r* k6 y `/ q0 x7 N q
Object attribute = null;$ }, @ w$ y; g/ l- L$ W
// 遍历Session,找到该用户名称对应的Session4 u9 m2 q0 T2 `9 B( k9 o- E
for(Session sessionInfo : sessions){
: u) G6 x. _* o" e5 X attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);& G! ]1 d" n$ J0 m4 {$ R0 C
if (attribute == null) {/ g i% Z6 u! ?
continue;
8 x9 t; I9 \: j& G }
# ~$ r' i" ^2 \0 B sysUserEntity = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();0 [; C/ Y2 D. q8 y: _0 O
if (sysUserEntity == null) {1 C4 ^& u% V" [1 y$ L
continue;6 |1 [8 o4 y) @- \
}4 C4 T4 m# p$ i1 Q" |6 O
if (Objects.equals(sysUserEntity.getUsername(), username)) {
; Z. Q. x+ b# v& i- N: ?) `) W4 y session=sessionInfo;
; W" J% m' Q$ ` // 清除该用户以前登录时保存的session,强制退出 -> 单用户登录处理
$ G% l8 b9 n5 ?: C, g& k- u if (isRemoveSession) {
7 g [+ p4 t4 {* B6 F7 w$ s redisSessionDAO.delete(session);( R8 E& i8 s3 X0 S) U0 P$ S! o( B
}) w+ }+ Z6 L1 Y; W, L9 r6 H9 m
}
~( w) x) v/ @$ T6 O$ S3 g" w N: Z4 U }5 A0 X1 n# g6 C# n
if (session == null||attribute == null) {9 L' U8 R/ Y; j: ?8 m: l& l
return;
- b0 N5 x4 g4 E( G6 o9 W) } }+ | X7 B& U' [: s
//删除session
5 y9 w8 L T/ Y; a" o% x if (isRemoveSession) { L [: T9 A; n2 @
redisSessionDAO.delete(session);
: a7 I/ N6 k. B }: h! K# G, W+ E1 o' J7 l+ @9 J! L
//删除Cache,再访问受限接口时会重新授权 y X J. u5 G$ o$ J4 j' N3 I
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
L2 t6 \: M% W0 c( N! J- r8 w Authenticator authc = securityManager.getAuthenticator();
/ `' B* o6 N1 d- Q; l5 } ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);2 H% q( ~9 U/ D. B5 {' Y
}3 S0 b; n, H* e$ D+ O: p {$ N/ b
/**) f) G H# i& b
* 从缓存中获取指定用户名的Session9 G: d1 g* x" i, m# C9 |
* @param username
, T$ @. D1 t) j, O* ]7 `3 f */
* g3 d0 F* e5 u( _& F( t private static Session getSessionByUsername(String username){2 E& r9 G0 d" K5 z
// 获取当前已登录的用户session列表
" y2 t' p5 O) j8 Y- q' F9 B Collection sessions = redisSessionDAO.getActiveSessions();# n+ }( H/ d) B' m% S5 y
User user;* ~" G3 D3 ~# ]6 R. ?2 F
Object attribute;9 s+ w( F0 {: ]5 _* J
// 遍历Session,找到该用户名称对应的Session2 _2 ~' p( P) f
for(Session session : sessions){
& ]$ F. p: X# B, k+ x& N, t attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
! d: X0 k2 T5 w6 X if (attribute == null) {
; a" ?# g$ Z; W; l8 _$ x continue;
w* y% i" Q4 u; i# ~ }% Q9 ?9 e2 x" X6 Z, Z5 j
user = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
& R+ L# s9 D4 O q) a8 u if (user == null) {" w# ~: r2 R( @1 l5 N b# T
continue;
9 |9 `1 }- `2 l. v, X4 Y3 H* x. D3 w" | }: Z- {$ s# p! { @
if (Objects.equals(user.getUsername(), username)) {
y: G6 `+ |: V o* R1 e return session;0 f6 K# m- |# {2 L; N
}
6 } o( |% n1 ?5 H# m }' r r) v1 X1 G' g4 E$ h
return null;
' f) @8 v4 [9 R& w* T+ \$ H# r }- C' [- }) ^" p% e
}) O5 e. O* |( O
; }* P" O+ Z0 `& X; p4 x; P& m 2、Redis常量类public interface RedisConstant {
# R/ C, p: Y% g6 ^) n) W5 h /**0 p0 |% [, u$ G# n9 Z# x: M% e1 D
* TOKEN前缀
- H5 Q. [: L7 U, ? */$ m1 @( i5 [+ f! W
String REDIS_PREFIX_LOGIN = "code-generator_token_%s";
2 A# l" g0 X7 e, ?6 N8 a v }, _, X$ B* _3 K' {; n
# ?& j4 g/ b8 d' V0 L. w 3、Spring上下文工具类@Component+ R+ l9 ?/ k( A" v! z5 h0 l+ L/ p, B
public class SpringUtil implements ApplicationContextAware {5 [! ~* ^8 A7 _7 z9 t: D6 ?
private static ApplicationContext context;/ R* Q6 v1 d5 R4 U( u, U
/**
+ o3 q* i% }* n$ v * Spring在bean初始化后会判断是不是ApplicationContextAware的子类7 ?* Q& W( ~5 O$ M" } |3 i1 m
* 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去
' g8 f7 S" O, S( [) v: E */
( B) @- T$ i9 D ~4 B0 ~ @Override7 i8 M/ B- r2 f5 \& u
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {6 [2 @$ f2 }$ d _
context = applicationContext;
- ?% d9 ~% [2 h& c6 i% W+ ]3 ^ }7 F+ a1 O7 S/ Z& t6 r
/**- w( s5 X5 Q- C$ @
* 通过Name返回指定的Bean
4 b6 A$ v' B |' @1 { */
3 r/ y) r8 v! L8 R5 x- F public static T getBean(Class beanClass) {7 d9 m2 L) R. m+ i; b4 N4 r5 F: i
return context.getBean(beanClass);5 d+ Q( W$ J2 h) w' Y) d
}% b. U% f9 j% _5 x
}
. v, D" H. e g( \9 j
9 t3 V: o2 p5 d+ _ 六、案例demo源码GitHub地址:https://github.com/zhengqingya/code-generator/tree/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro
7 l% A s4 `( c* P% h" F; C 码云地址:https://gitee.com/zhengqingya/code-generator/blob/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro
9 u8 p! V g! s# v( }$ w4 X5 n$ j/ i+ j% e/ [; C6 N4 c) } }& N
! J2 O3 j+ A' W4 H& M7 S# W1 {& o
7 r: y) B3 B/ |2 V4 L! E8 r0 G* r$ G
|