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