找回密码
 加入怎通
查看: 166|回复: 3

SpringBoot集成Shiro 实现动态加载权限(springboot集成neo4j)

[复制链接]
我来看看 发表于 2023-03-06 15:25:44 | 显示全部楼层 |阅读模式
" |5 h$ R( j6 `, }3 [) }5 z

一、前言本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,不用重启项目,以及在页面分配给用户 角色 、 按钮 、uri 权限后,后端动态分配权限,用户无需在页面重新登录才能获取最新权限,一切权限动态加载,灵活配置

( E8 N/ n6 D; ^' x1 L

基本环境spring-boot 2.1.7mybatis-plus 2.1.0mysql 5.7.24redis 5.0.5温馨小提示:案例demo源码附文章末尾,有需要的小伙伴们可参考哦 ~二、SpringBoot集成Shiro

0 i, Y* t& E! L; \

1、引入相关maven依赖 4 ]/ }. X5 @% s9 F5 d 1.4.0- c4 s- N' ? e! _* b& ` 3.1.0 q) G, m! O5 H% L' w" M$ b0 {; y5 |# D" C; P ?/ Y- Y+ J, ^8 ^" `2 a) a) b$ P; s# T1 a2 V" J6 k$ f! o5 o S$ k " V+ {; s6 D- W' J m8 V4 P+ J org.springframework.boot 3 ^+ j- ~" L( O' m2 R spring-boot-starter-aop $ C* Z9 a. v4 ^& ^5 O 1 O( u1 Z/ N6 Q+ t8 d/ E9 s- x/ i0 M2 f' D4 e 1 W4 }9 f3 C/ x org.springframework.boot7 F3 Y0 t1 \7 o2 V7 W$ x7 V: Z spring-boot-starter-data-redis-reactive5 O# E0 H5 Z, x4 X/ s$ e% n ( F3 \2 u, C; r$ V: e, B 4 W* e! U) K: ^5 e3 |% I; M' d. w0 e" \! L$ g$ C org.apache.shiro ' ^2 j1 f/ n( i- t2 D- [/ D shiro-spring 1 d) n1 Z6 U! c1 W$ L- Z ${shiro-spring.version} - s( n( {. s( D) G; A ( d- {6 \6 Y; e+ U3 u# d' w - O/ Q) O1 l# w# e $ r# w6 x- N N' U5 a org.crazycake ( W% x0 O0 F5 U2 A% o! j shiro-redis; x+ G+ } \9 X7 ^9 {- i" q ${shiro-redis.version} ) H0 b) C8 z4 ] u. {" R4 d( {5 U( ^4 T( [: b( g T& c& z! Y( v6 B' Y

0 E# |% i0 `1 M6 }+ v

2、自定义RealmdoGetAuthenticationInfo:身份认证 (主要是在登录时的逻辑处理)doGetAuthorizationInfo:登陆认证成功后的处理 ex: 赋予角色和权限【 注:用户进行权限验证时 Shiro会去缓存中找,如果查不到数据,会执行doGetAuthorizationInfo这个方法去查权限,并放入缓存中 】 -> 因此我们在前端页面分配用户权限时 执行清除shiro缓存的方法即可实现动态分配用户权限

1 ?, ?- }/ O! F: v# \! U/ n) W

@Slf4j" p: G, E/ `7 V8 ?3 f4 z public class ShiroRealm extends AuthorizingRealm { , T4 l! |0 T+ X7 E3 h k @Autowired( g8 w& J4 M: r( H private UserMapper userMapper;% \5 E1 B" o) p) P @Autowired 3 F. |1 V( `+ j private MenuMapper menuMapper;7 }" D3 g2 j- S7 g @Autowired / _+ V1 U2 S' @ N7 H. b& }+ C private RoleMapper roleMapper;6 G$ C+ o0 ~% P9 F% }$ i0 _ @Override # P+ S1 `) ]' e+ {9 K public String getName() {0 `( l6 E$ A6 P4 H0 d {- u D return "shiroRealm"; . m- C* {$ v1 d/ ] } 4 ?" n: ]( C j /** % u: v0 B6 A7 r9 V: \: Y; Q/ v * 赋予角色和权限:用户进行权限验证时 Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中 3 k) i3 U' j j1 G */8 o0 o; O2 w$ o. a: Y& i @Override/ g7 x6 H3 q: x9 S9 Q: H: Q" U protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {% h' i+ X/ c* ^1 b SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();0 ]$ ~8 ?+ h/ j( H( G // 获取用户% h& X; e/ Y; ^3 C User user = (User) principalCollection.getPrimaryPrincipal(); z! {& n) m5 r1 ` Integer userId =user.getId();5 a: ^. O! e# ?8 n A2 M* ? // 这里可以进行授权和处理: z+ m' G1 p, ] Set rolesSet = new HashSet<>();9 l- T2 U! j4 s; Q. E3 t& R2 p T Set permsSet = new HashSet<>(); ! E) V i8 w# }, I // 获取当前用户对应的权限(这里根据业务自行查询) + L" F) ^+ A: U- \( w$ c# f$ | List roleList = roleMapper.selectRoleByUserId( userId ); # G B3 y4 l/ r- ]6 x for (Role role:roleList) {. m" s& \0 l2 k6 W, ?/ B rolesSet.add( role.getCode() ); 6 T7 B z6 N% ~4 [( Q List menuList = menuMapper.selectMenuByRoleId( role.getId() ); 6 c3 P! Z# c3 g for (Menu menu :menuList) { : V4 `; W3 D, @6 y7 R permsSet.add( menu.getResources() ); ; z+ t( n7 I# d) G& ` }: Q+ |, y" T' p$ E } ! S: h+ k/ t: c8 C5 R //将查到的权限和角色分别传入authorizationInfo中 & u: O' y) _+ ? authorizationInfo.setStringPermissions(permsSet);! ], Q# i. F k K authorizationInfo.setRoles(rolesSet);0 h1 k- Y9 K; h; Z; D log.info("--------------- 赋予角色和权限成功! ---------------"); 0 ]9 M0 r! k( C return authorizationInfo; - A. ^% _& _( N& n! y! Y9 q* i }" \& C/ L9 g0 `% _ /**' Z" R/ J/ e. _5 [: Q s4 i$ | * 身份认证 - 之后走上面的 授权 3 u" ]* j% o" D" Z+ u4 K */6 M( i4 U5 s& w8 H: ] @Override6 f+ |6 G( k. O4 h protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { ~( l. h& x9 Y# I7 j$ A$ j7 t UsernamePasswordToken tokenInfo = (UsernamePasswordToken)authenticationToken; ) ?2 @! ~- p9 d$ l0 b) i // 获取用户输入的账号 `* h3 W) t0 J$ C& \2 H String username = tokenInfo.getUsername();& K' O4 E% t+ ~1 m0 ` // 获取用户输入的密码 1 j$ [) T- S3 G String password = String.valueOf( tokenInfo.getPassword() );) U7 b2 f" S( c1 z8 E // 通过username从数据库中查找 User对象,如果找到进行验证 ; C3 B1 D$ x, q; f% H // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 1 {1 N/ D# L. c3 |) g1 T, d& m User user = userMapper.selectUserByUsername(username); ; g! v0 q e' D0 R! H% o9 X6 M // 判断账号是否存在) p; q6 W. I& Z1 y4 X7 ? if (user == null) { 3 r$ a$ ~: A! { //返回null -> shiro就会知道这是用户不存在的异常* T, J& @" c, p! r return null;4 U' e F ], ^! f3 h } $ i9 s' c- G- | // 验证密码 【注:这里不采用shiro自身密码验证 , 采用的话会导致用户登录密码错误时,已登录的账号也会自动下线! 如果采用,移除下面的清除缓存到登录处 处理】9 {9 n; e9 `1 ~; o3 T, j- u0 W if ( !password.equals( user.getPwd() ) ){: G9 k( A/ S8 `5 s) [! x& V( @ throw new IncorrectCredentialsException("用户名或者密码错误"); , o5 X( Y% `# U0 Q }+ @% W8 A- ^' D0 S. ]# r+ j: _ // 判断账号是否被冻结' g3 g; `! b8 h- u" Q, j; h if (user.getFlag()==null|| "0".equals(user.getFlag())){1 z: K. \+ z8 o throw new LockedAccountException(); * t$ V- r/ m+ G } % r9 m4 H8 n! c& R A" ?1 Q1 e2 w& l /** : Z- p6 E$ s& j! p" J* L * 进行验证 -> 注:shiro会自动验证密码) D9 q; ]5 g0 G; | * 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值2 {* a- j! j4 q2 { * 参数2:hashedCredentials -> 密码5 ^. W. j2 S8 \8 A2 q * 参数3:credentialsSalt -> 设置盐值, o5 q1 f( A P2 ?8 e9 B; ~ * 参数4:realmName -> 自定义的Realm 0 q0 `5 y1 g5 b: u */; _1 u! R* }: Y SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); : W8 S @% q; e! I8 M( m // 验证成功开始踢人(清除缓存和Session)1 E9 I- ~+ G( e1 U ShiroUtils.deleteCache(username,true);& \( i- x6 N+ M5 M5 M& T* s* _ // 认证成功后更新token 6 o# A' K8 r: I0 y, j String token = ShiroUtils.getSession().getId().toString();' O7 w, }- s. g- i1 M0 L( O. @ user.setToken( token );" ] D8 c) x$ C- S userMapper.updateById(user); 6 }- d. ?1 U5 ^) G7 B& I; P return authenticationInfo;7 {3 l; _( N! h4 J6 Z# i' {& p" { } _$ X1 m3 y1 P7 p* U1 W+ h }1 M i5 |6 d; z/ v8 {- G. a

1 H5 f" Y% n' g3 R* a/ z* o* D9 y

3、Shiro配置类@Configuration 3 ] d3 Y$ O% o5 V/ }+ R public class ShiroConfig { / k6 ?* b5 r3 x0 p" m6 o private final String CACHE_KEY = "shiro:cache:";8 M# J* v; H# L- S) o1 |6 \ private final String SESSION_KEY = "shiro:session:"; + K; M' n: Y+ D: L/ g% O /** $ X, e* K, q( z" x6 v% P * 默认过期时间30分钟,即在30分钟内不进行操作则清空缓存信息,页面即会提醒重新登录% y3 O) C& C4 p/ h */ 5 G% a6 r5 r' y& Q- ]) l8 ~ private final int EXPIRE = 1800;* q- b/ ~' O K8 p& l# p /** ; Q1 c6 N, ^, Q* ^3 i * Redis配置4 L: u4 u6 |- D9 s/ v7 } */0 _3 ^, V% c4 J7 c# s @Value("${spring.redis.host}")- a' u! @+ c- `5 L) r private String host;. a' k8 _- k& `: m9 W1 Z6 C# F1 A/ k @Value("${spring.redis.port}")9 y q4 |% k m9 l) F4 U( O5 S private int port; * ` t) {* t _9 F& B! [8 w @Value("${spring.redis.timeout}") 8 _ n! V! u3 p5 k' ` private int timeout; + q$ U- p( P- @& Z$ r$ W // @Value("${spring.redis.password}")! u; N8 G0 l! X! s0 a. o% d // private String password; 5 a9 C; p8 R$ o1 `. Y- i# z+ o /**/ C7 L7 A+ O6 A0 A4 }5 e/ Z * 开启Shiro-aop注解支持:使用代理方式所以需要开启代码支持 6 T- V* \( x0 m* g, l! a* V% v: a */ l. k0 w. H/ B' B0 G% V7 v' x' X @Bean 9 R2 ^" q+ J1 N) a5 O5 r6 o public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {- a) z' s. n. ? AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();' j2 k" ~! [7 F1 z7 V authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);& s: m9 o1 y1 c5 x8 a3 R return authorizationAttributeSourceAdvisor;1 E$ p G* y$ y0 ?0 n$ w- k ? } . Q- @3 Q9 b2 O; W /**2 ], C: d6 t5 V7 J L# t) w * Shiro基础配置3 E2 b; \7 w7 f% l */# I+ t: a2 t1 z0 ? ?! \ @Bean . R" x9 r6 w7 I9 F- u' S public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager, ShiroServiceImpl shiroConfig){ + s* {$ M* A7 N. s- Y7 _8 @ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();. n# i/ w9 S1 m' f3 y shiroFilterFactoryBean.setSecurityManager(securityManager); ; N4 M# L0 L1 b. x // 自定义过滤器 e0 L0 Y4 [9 ]- s( H0 R Map filtersMap = new LinkedHashMap<>();5 z% l2 M' K4 f) B // 定义过滤器名称 【注:map里面key值对于的value要为authc才能使用自定义的过滤器】 b4 u* C: G8 [; W4 u$ q" B filtersMap.put( "zqPerms", new MyPermissionsAuthorizationFilter() ); 8 i1 L! L2 e8 E( q p* m* T! g8 @ filtersMap.put( "zqRoles", new MyRolesAuthorizationFilter() ); ' t9 D( I0 c$ K- w% h1 f! T filtersMap.put( "token", new TokenCheckFilter() ); P" V0 `7 n+ f K# d shiroFilterFactoryBean.setFilters(filtersMap); 2 a; U R4 R- O4 t5 o4 }3 n // 登录的路径: 如果你没有登录则会跳到这个页面中 - 如果没有设置值则会默认跳转到工程根目录下的"/login.jsp"页面 或 "/login" 映射2 R. v" L( Z1 d! Y; Y shiroFilterFactoryBean.setLoginUrl("/api/auth/unLogin"); % Y2 Z; x" k. e+ ?$ q1 z: C // 登录成功后跳转的主页面 (这里没用,前端vue控制了跳转) % n4 U. ^8 K! z# k# { // shiroFilterFactoryBean.setSuccessUrl("/index");* ~9 I2 L0 R; E/ E R8 ?' ~& d // 设置没有权限时跳转的url/ g- O/ ]$ [2 B+ u* c shiroFilterFactoryBean.setUnauthorizedUrl("/api/auth/unauth"); ' I" k- v$ X- I3 K shiroFilterFactoryBean.setFilterChainDefinitionMap( shiroConfig.loadFilterChainDefinitionMap() );1 `/ f/ ]6 ^* e s+ V: U( p5 h- V return shiroFilterFactoryBean; 2 ~; m9 X" S& p' w w) f& Z! @6 K }8 q f$ C3 B: t /**; H& M- {' K2 n5 i4 ?7 g" R+ V * 安全管理器 + I8 u- S% M [2 _ */8 f9 L3 N, u+ ` @Bean 1 D n1 V" _& O) g6 @ public SecurityManager securityManager() { # L5 E- q2 F- i, z x2 t, W DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();7 j4 l! w& J& c' I // 自定义session管理+ D* t m7 F3 x9 W- \, Y* e securityManager.setSessionManager(sessionManager());+ d; O4 I, s3 |; f* ] // 自定义Cache实现缓存管理 / O. O1 L& P4 }% |6 b. [+ a( \$ z( ] securityManager.setCacheManager(cacheManager()); ' T. `4 M; m! E9 w, U // 自定义Realm验证 m% c# `/ m9 G3 F1 j& x& X+ | securityManager.setRealm(shiroRealm()); a( p! ?) |5 v. O return securityManager;5 X5 h" R- }( }, w( W }( `, S( y, V- h+ @+ l* K /**& D& T5 z* v* j- }" i * 身份验证器* p* t8 I; P0 R */ ; Y4 z# w3 h& D2 D @Bean ; _0 M3 m: x( E0 Y( e& m public ShiroRealm shiroRealm() {, i7 ] ~2 i: _* H# D2 o ShiroRealm shiroRealm = new ShiroRealm(); + U6 L& t; e7 o4 H- r8 M2 r$ Y+ C shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());) W. ?8 v: B% b. W return shiroRealm; ; P' B* {# t2 R: x1 b5 a } l* P6 v" f+ X2 y$ y. B- m /**2 r! c, S- {7 y& O * 自定义Realm的加密规则 -> 凭证匹配器:将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置! Q* Y. C4 G; S# ] */& E; g, \3 ^1 {/ K1 X @Bean ) \# @ r* l- p public HashedCredentialsMatcher hashedCredentialsMatcher() {8 P, e" ]# I+ j HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();& \5 D) }7 g& ^& a8 H1 s0 s // 散列算法:这里使用SHA256算法;# S7 U6 @9 Z0 n! H% T& Z shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME); { a3 o$ O" K // 散列的次数,比如散列两次,相当于 md5(md5("")); A$ z% n3 m+ C7 q L# ^4 x shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);: A" g# O" a" l return shaCredentialsMatcher;8 B. ~0 m; L3 S. j) W/ K } & L1 n, w5 c; p8 Q; } y /** & g5 w; X, E/ a2 c * 配置Redis管理器:使用的是shiro-redis开源插件 % `& @: f. k! k R' E */ % `- K+ Q& H, W @Bean6 u% l7 p1 _) [ public RedisManager redisManager() {8 n. `$ s! a8 x* r, A6 {* ? RedisManager redisManager = new RedisManager(); ( J; I0 V1 v; X3 j redisManager.setHost(host);8 ], U" N7 p. C7 L redisManager.setPort(port);# _7 `& l) Q8 K8 C redisManager.setTimeout(timeout);# R5 g. D$ R- m // redisManager.setPassword(password); I; a7 q" j( T( a) Z: K. M return redisManager;, I) c: s) u2 F9 }7 u) v } 6 |& \8 B) ?1 V: X* @) Q) S* E /**+ E, V3 J8 z9 i4 ~. }. v; ` b' }5 \ * 配置Cache管理器:用于往Redis存储权限和角色标识 (使用的是shiro-redis开源插件)2 m2 C" A0 C6 o4 M */+ O8 R6 X0 g4 U; z+ A$ _; K: o @Bean* K3 b1 N7 g; L& L5 Q0 w public RedisCacheManager cacheManager() {/ O5 W0 C, `, C1 c/ g RedisCacheManager redisCacheManager = new RedisCacheManager(); , L! k) u( { Y3 f/ T redisCacheManager.setRedisManager(redisManager());. E( t' A* B$ i0 R redisCacheManager.setKeyPrefix(CACHE_KEY);' O, o# n1 s @+ g: K // 配置缓存的话要求放在session里面的实体类必须有个id标识 注:这里id为用户表中的主键,否-> 报:User must has getter for field: xx 2 x" y: m0 b* D4 d. Y: \ redisCacheManager.setPrincipalIdFieldName("id"); / ~, R+ e, S! w( d& s9 Q; ? return redisCacheManager;" P) p' Z2 R2 h/ [$ j& h }7 E$ ~! ~( R7 a& k; F2 j /** 1 }8 s9 T6 z: ^ * SessionID生成器 , t+ E: ~% L% E$ [1 l */ ' u3 h; l; z& D4 T, y @Bean ! r; M$ Q. L$ }3 p; j: c0 q public ShiroSessionIdGenerator sessionIdGenerator(){ K/ J/ s, ^1 E3 Y return new ShiroSessionIdGenerator(); 7 r2 H/ h) y0 y; @4 _4 ^ }& i; p# g" Z: A' K6 \ /**0 G# [6 k( U6 p: a * 配置RedisSessionDAO (使用的是shiro-redis开源插件) 3 h% _1 J! Q+ d/ N0 k */5 c! Q5 W1 H I$ N: P/ z @Bean 2 [& ?" k% z$ |& s' W public RedisSessionDAO redisSessionDAO() { & a) `# v* u* u X- c RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); ) B, V* n4 m' h0 h- l redisSessionDAO.setRedisManager(redisManager()); 2 L3 f/ m( ^4 Z' L& e" x redisSessionDAO.setSessionIdGenerator(sessionIdGenerator()); H, ^0 L) H- l8 f0 M. v( ~$ ` redisSessionDAO.setKeyPrefix(SESSION_KEY); r4 i6 _3 V$ _5 z redisSessionDAO.setExpire(EXPIRE); 7 j+ m$ u. V5 o% ~ return redisSessionDAO;7 U* G/ ?* r% N1 m; |% { }# D' }* w9 q, i: I /*** H1 {) t2 Y i/ j/ `4 ` * 配置Session管理器 3 q. S! c$ V/ ?1 z- q; H! m0 i, f */! Q2 q! [: g, K& H7 W$ X7 { @Bean 6 g p$ Z E" Q/ G8 a3 A public SessionManager sessionManager() {+ z/ ?! j& D" I9 ~3 S1 ] ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); 7 C7 }- J* H y' Z" t/ X2 | shiroSessionManager.setSessionDAO(redisSessionDAO());( O0 Z3 V- o, r+ y- } L return shiroSessionManager; 0 C) q, \, ] M }9 @9 }! h0 x0 l/ B }7 [0 J8 G9 f7 D( T/ x

, B$ d9 r2 E r( w& X' d+ O

三、shiro动态加载权限处理方**oadFilterChainDefinitionMap:初始化权限ex: 在上面Shiro配置类ShiroConfig中的Shiro基础配置shiroFilterFactory方法中我们就需要调用此方法将数据库中配置的所有uri权限全部加载进去,以及放行接口和配置权限过滤器等

2 M9 O% h* ]" T" `, v, m

【注:过滤器配置顺序不能颠倒,多个过滤器用 , 分割】ex: filterChainDefinitionMap.put("/api/system/user/list", "authc,token,zqPerms[user1]")updatePermission:动态刷新加载数据库中的uri权限 -> 页面在新增uri路径到数据库中,也就是配置新的权限时就可以调用此方法实现动态加载uri权限

1 O5 k- q( M4 v

updatePermissionByRoleId:shiro动态权限加载 -> 即分配指定用户权限时可调用此方法删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限

! d1 J8 n; A* E) `( v2 @. `

public interface ShiroService { 3 h1 H! |) `, d/ G /*** ~% ]* w4 ^5 M8 _7 l; p# T( A% X * 初始化权限 -> 拿全部权限% ]3 J) @, F) G* [( L ~4 }2 { * % ~# G7 @1 h5 q# L" O! k! ^9 z * @param : # c# A& V: j& n( H8 ]. h * @return: java.util.Map , b' [/ a, q1 R) L X4 `$ k% c */2 H5 D* W( B- p! E" K Map loadFilterChainDefinitionMap();; l! z2 E/ b9 N8 G4 N, l# }4 N /**) J3 H8 c9 |' ~5 k * 在对uri权限进行增删改操作时,需要调用此方法进行动态刷新加载数据库中的uri权限+ X5 ^9 b% B0 b; B' e( D9 q2 P * $ c7 V) O0 `8 q5 {& y * @param shiroFilterFactoryBean 6 p; [( E7 _ P0 N1 y * @param roleId0 E. z1 M% l0 ?# ~8 g * @param isRemoveSession:6 y/ ?8 e/ Y- ^- B * @return: void 8 y. o' P. O5 E3 x/ v, C1 r+ ` */1 C$ u8 Y) K7 J6 z7 _' O" d void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession);8 G) u: Z+ \0 D" ~+ R4 n' ^2 | /** 6 H1 {* [$ t5 r. Q8 c( j& I& g# `7 v4 v * shiro动态权限加载 -> 原理:删除shiro缓存,重新执行doGetAuthorizationInfo方法授权角色和权限- O5 K& m: d: a+ N+ u3 L * 1 x! J5 s6 Z& w8 z * @param roleId9 l% w4 `: S/ C$ h, ]1 t+ H- Z; h& O/ P * @param isRemoveSession:3 a' C, ^& o2 J, J& C" K$ r( i( E * @return: void5 ^5 f/ o7 o# X& r1 m */0 k. d7 [" Q; [, c. M void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession); - {9 Z/ g5 _- Q: L7 p) a! o }2 n8 y) b3 Y4 t6 F @Slf4j - ^/ y' y1 x4 L @Service ! k& y8 x. f8 g+ ]1 n public class ShiroServiceImpl implements ShiroService { : v# o; h( }% P6 k( ^ @Autowired ' O9 m( s6 _$ P+ X" X private MenuMapper menuMapper; ' W- {* \9 e, |3 j2 F$ x, ^' G6 |( I @Autowired: A( f" m+ [2 M, q) i9 G( ` private UserMapper userMapper; % e( @7 Y& S1 m0 o @Autowired 5 x8 h3 Y" B9 z/ C, G private RoleMapper roleMapper; : _! S! D/ ]3 n) g/ |& Q/ p" j }( ~9 j @Override 6 e, ^8 u% p2 z, i+ d public Map loadFilterChainDefinitionMap() { $ d5 ]* z- b; Y) k8 Q // 权限控制map5 W! H/ {* |0 Q, D Map filterChainDefinitionMap = new LinkedHashMap<>(); " \& ~: K% \6 F // 配置过滤:不会被拦截的链接 -> 放行 start ----------------------------------------------------------* `% J0 m. Q; ~" g- `, v // 放行Swagger2页面,需要放行这些 # k( h, d6 s, D: b filterChainDefinitionMap.put("/swagger-ui.html","anon"); 0 u2 c, T( Z$ S! b- s filterChainDefinitionMap.put("/swagger/**","anon"); % a2 g- Z$ ~+ O6 N8 a. v filterChainDefinitionMap.put("/webjars/**", "anon"); ( r: V* M& D& k6 g8 C2 _ filterChainDefinitionMap.put("/swagger-resources/**","anon");, ^8 K/ S/ h7 `' W filterChainDefinitionMap.put("/v2/**","anon");4 u( w1 ?% v2 v, F/ N! @) N filterChainDefinitionMap.put("/static/**", "anon"); % P# `$ Q- R6 P) i5 s // 登陆& f$ g' ]3 L {2 p filterChainDefinitionMap.put("/api/auth/login/**", "anon");7 K; M+ H# H7 a' {7 M1 A" n // 三方登录 l& b* j: ?9 {- Y( L7 \5 b% E filterChainDefinitionMap.put("/api/auth/loginByQQ", "anon"); # V- L& e6 |: T9 ?8 I2 W2 n3 @ filterChainDefinitionMap.put("/api/auth/afterlogin.do", "anon");" `8 {- i' I' Z // 退出/ e: @9 {6 q5 ^ L$ H filterChainDefinitionMap.put("/api/auth/logout", "anon");& N5 F( d. S- n5 J: B I // 放行未授权接口,重定向使用 $ A7 e9 F/ B7 x5 q- C8 ^ filterChainDefinitionMap.put("/api/auth/unauth", "anon"); 6 Q' }. C% c$ j // token过期接口 9 P3 z; @: Q2 p% q; P: T, G$ y filterChainDefinitionMap.put("/api/auth/tokenExpired", "anon"); 8 f1 B" q' l6 n" e5 |! |, j // 被挤下线 ( F# ~& r' D1 P filterChainDefinitionMap.put("/api/auth/downline", "anon");/ X/ d0 p% X* `/ d A( C% H% A4 o // 放行 end ---------------------------------------------------------- ! I; C) q& p# O$ ]8 i- m // 从数据库或缓存中查取出来的url与resources对应则不会被拦截 放行 $ Z) j; X- N7 w; w: z+ m List permissionList = menuMapper.selectList( null );) m6 r. b1 H- h/ U4 X0 O if ( !CollectionUtils.isEmpty( permissionList ) ) {; }; d- A4 I( |8 D$ U+ f ^) c permissionList.forEach( e -> {: A; f l$ ]2 i if ( StringUtils.isNotBlank( e.getUrl() ) ) { y& \3 Q. U8 }; \- _% c // 根据url查询相关联的角色名,拼接自定义的角色权限 & G9 r, w+ G6 h! F List roleList = roleMapper.selectRoleByMenuId( e.getId() ); / I7 z$ a4 v. p8 x, } StringJoiner zqRoles = new StringJoiner(",", "zqRoles[", "]");$ E: O. S4 ?% k. d! w9 Q3 K7 F( W if ( !CollectionUtils.isEmpty( roleList ) ){# y7 D/ y& E/ c/ S5 M- W% d roleList.forEach( f -> {( e0 [" |) K0 m# \6 { zqRoles.add( f.getCode() ); 3 E6 e: T: E1 |5 o* ^" {* @0 | }); 4 A6 \6 W/ _& R9 a' o7 t } }4 [$ k- f# S' g6 m7 H" W // 注意过滤器配置顺序不能颠倒 1 e4 D7 i2 M5 F$ n, f // ① 认证登录: _+ M6 X/ S+ Z$ s B# @ // ② 认证自定义的token过滤器 - 判断token是否有效( a% f. w/ y0 P3 D // ③ 角色权限 zqRoles:自定义的只需要满足其中一个角色即可访问 ; roles[admin,guest] : 默认需要每个参数满足才算通过,相当于hasAllRoles()方法 & F4 i/ Z- @' W. M, ^0 W // ④ zqPerms:认证自定义的url过滤器拦截权限 【注:多个过滤器用 , 分割】6 s7 y0 _$ i% _3 G1 _; o // filterChainDefinitionMap.put( "/api" + e.getUrl(),"authc,token,roles[admin,guest],zqPerms[" + e.getResources() + "]" ); / i$ \ v2 t/ `' q! g; L filterChainDefinitionMap.put( "/api" + e.getUrl(),"authc,token,"+ zqRoles.toString() +",zqPerms[" + e.getResources() + "]" ); m- a' b2 Z) K( U4 \) f // filterChainDefinitionMap.put("/api/system/user/listPage", "authc,token,zqPerms[user1]"); // 写死的一种用法 - h1 Z# W5 l' q; p4 s( D+ c } ) x% A! A7 a" F' T6 M! p. a; p });$ ]3 `" x4 B' x2 V } ) P* W. E7 q4 x // ⑤ 认证登录 【注:map不能存放相同key】 " L) k. i/ g" P4 {& Y, g$ L( o filterChainDefinitionMap.put("/**", "authc"); $ s7 K) ]6 g. x! Q return filterChainDefinitionMap; 5 I2 l4 t# h4 z" t' s: V- w' H1 B }- _3 W& `+ f5 i. b7 `8 C& d6 w @Override - _9 I( R7 d3 i8 S public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean, Integer roleId, Boolean isRemoveSession) { 9 N' {" f [- i# ^' W3 z synchronized (this) { ) K. z- U& z5 d2 p6 S AbstractShiroFilter shiroFilter; 6 i. j5 }: @ D$ ]+ ` try { " k3 ^, O2 D7 V+ t) C! o" Y' A shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean.getObject(); ; F2 r0 Z/ E5 Y) P |" R } catch (Exception e) {: F! u2 {( t) Y z/ ~5 V( V; i throw new MyException("get ShiroFilter from shiroFilterFactoryBean error!"); / G+ [7 D/ N9 o4 H5 z, ]4 S }2 [- }& r- S. ~5 I. U PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();5 g' m: Q# ]5 w) j, `, H1 e DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager(); $ m2 `( \/ {( E8 g/ b5 ]' ^ // 清空拦截管理器中的存储 4 \$ q1 M8 T$ ]8 g6 g' ] manager.getFilterChains().clear(); * C) p0 k [- B5 P1 G' z( f // 清空拦截工厂中的存储,如果不清空这里,还会把之前的带进去- O9 T9 x5 ^+ u // ps:如果仅仅是更新的话,可以根据这里的 map 遍历数据修改,重新整理好权限再一起添加 ) N K6 m5 u x0 h/ A: \ E shiroFilterFactoryBean.getFilterChainDefinitionMap().clear(); - G' J1 ^1 q$ b$ B8 C' T( S // 动态查询数据库中所有权限5 B* s. O& P& M% ] shiroFilterFactoryBean.setFilterChainDefinitionMap(loadFilterChainDefinitionMap()); ; V+ u5 \8 x/ i // 重新构建生成拦截1 G. g* i: K: B, E( T" N7 s Map chains = shiroFilterFactoryBean.getFilterChainDefinitionMap(); # f8 L* v0 X* A2 p. P$ R for (Map.Entry entry : chains.entrySet()) {; H9 w1 {0 o7 ~; k manager.createChain(entry.getKey(), entry.getValue());6 {- }+ |0 |! c$ U. c' s; s4 k }0 c @+ r0 V& q) x& Q( h" [ log.info("--------------- 动态生成url权限成功! ---------------");8 c7 Y( h6 M/ y# a$ o/ L // 动态更新该角色相关联的用户shiro权限 ! \+ ]5 D8 y4 t if(roleId != null){ % j0 Q, d+ e" |3 Q& t updatePermissionByRoleId(roleId,isRemoveSession);; B0 g! `# m: Y# J% m } 0 j1 `9 ~8 @0 E4 y; A7 S }+ `& Q8 S; @/ {% b1 u7 D0 W7 p }0 o5 V a3 h6 a% s4 m1 W( v @Override , }% |# l! I* O5 x5 S1 g( k/ ? public void updatePermissionByRoleId(Integer roleId, Boolean isRemoveSession) {3 O. l0 j0 d4 x2 ~8 f5 K: A- F/ i // 查询当前角色的用户shiro缓存信息 -> 实现动态权限 $ E' Z" O, P1 B( {- F3 B List userList = userMapper.selectUserByRoleId(roleId); \ e" M' A1 h$ s5 ?! A // 删除当前角色关联的用户缓存信息,用户再次访问接口时会重新授权 ; isRemoveSession为true时删除Session -> 即强制用户退出 ' c, Y( X1 T( I- i6 `# P if ( !CollectionUtils.isEmpty( userList ) ) { ) d1 f5 Q+ e- u7 [5 e for (User user : userList) {9 d* {/ R* X9 b5 u7 b ShiroUtils.deleteCache(user.getUsername(), isRemoveSession); 7 ^ w6 E) o2 f9 n7 { } - D! X3 O, N8 G' }" }1 \ } . U5 z' p5 E/ N ^" `9 ~, M" J log.info("--------------- 动态修改用户权限成功! ---------------");2 [; V1 o) j+ x } 5 M, v' G L5 X }6 }; r, `5 @% X$ g; S5 b& W" f

, _& h6 G4 U ]' J+ ]% C8 [

四、shiro中自定义角色、权限过滤器1、自定义uri权限过滤器 zqPerms@Slf4j ; j/ `& o l4 D# O6 ?7 i public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {, `( y5 y2 ]3 C @Override. [$ `9 h1 }* u+ X protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {( B4 b( |! b* I HttpServletRequest httpRequest = (HttpServletRequest) request;0 m5 q8 L3 u& `) a) w& Q HttpServletResponse httpResponse = (HttpServletResponse) response;' O a& |7 X! j3 O% v9 n String requestUrl = httpRequest.getServletPath();: S, V( Y5 V+ G8 h" t5 j4 { log.info("请求的url: " + requestUrl);; D, D, y# e* h( P3 i9 A% O5 L+ K8 r: I# v // 检查是否拥有访问权限9 V( R. O6 U7 F/ B2 w$ z/ N Subject subject = this.getSubject(request, response);8 Z8 D6 V5 q9 l if (subject.getPrincipal() == null) {" r" b" N x; I ` y8 y P" n this.saveRequestAndRedirectToLogin(request, response); 1 y9 \8 W8 l6 n } else { ! ?4 A* y6 ~; f6 g* |8 ^; q // 转换成http的请求和响应 $ N1 h6 D& B: k7 l7 _0 p& m) h/ k HttpServletRequest req = (HttpServletRequest) request;+ j( m" u8 B6 @8 m1 h HttpServletResponse resp = (HttpServletResponse) response;6 V3 ?1 X2 _7 d- h6 Z Q3 l // 获取请求头的值* N8 n0 ^$ I0 }- m# d5 M String header = req.getHeader("X-Requested-With");( g. G. v9 ^9 D, P // ajax 的请求头里有X-Requested-With: XMLHttpRequest 正常请求没有 $ g5 L+ [; f2 r' E# p; o' p' I if (header!=null && "XMLHttpRequest".equals(header)){* |% w* N/ L" p# J. h resp.setContentType("text/json,charset=UTF-8");- ] h% J4 q. U& r& @ resp.getWriter().print("{\"success\":false,\"msg\":\"没有权限操作!\"}"); 4 O; g w1 E# C5 z% f l }else { //正常请求 / N# }* G& U; |2 L [/ B String unauthorizedUrl = this.getUnauthorizedUrl();8 S' g; K% }+ l) `$ Q6 G [ if (StringUtils.hasText(unauthorizedUrl)) { , H, B) v5 B1 _) w3 ?+ w WebUtils.issueRedirect(request, response, unauthorizedUrl);) a5 A: B& e3 X+ s) Q } else { : |3 l% Q+ I4 ^ z' _. b WebUtils.toHttp(response).sendError(401); W' Q3 L$ Z+ } } $ h9 }" h: S5 k } 9 S0 E% c6 l) O( O% B$ |& X } 3 l) I6 z6 o$ O( ?* [3 m; e return false;, H: n$ L9 n; B; N) h2 o5 y& R } $ a' o; X8 y1 Q$ ^+ z4 u: W& p9 P o8 J2 S. \9 c } 2 N; E) h- i8 i7 Q

: r( _: S7 x$ u8 v/ ^6 l

2、自定义角色权限过滤器 zqRolesshiro原生的角色过滤器RolesAuthorizationFilter 默认是必须同时满足roles[admin,guest]才有权限,而自定义的zqRoles 只满足其中一个即可访问

7 e8 S# T! y4 q* I+ A- J! \

ex: zqRoles[admin,guest]public class MyRolesAuthorizationFilter extends AuthorizationFilter { - q# w' Y$ v6 H' x$ d1 j- e @Override$ T& Z" F" ?8 K; g: I5 R: c protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) throws Exception {: l7 x" E% W7 \& I' x; L Subject subject = getSubject(req, resp); 8 K- O6 z1 }: q4 }2 d6 H% p String[] rolesArray = (String[]) mappedValue;( s# O' C3 J( {5 X // 没有角色限制,有权限访问 7 T2 I ^0 r+ |1 W* x if (rolesArray == null || rolesArray.length == 0) { - c! z4 L- J; b return true;# V6 P1 z, i: W1 | V0 P }# Z6 { |$ N1 l for (int i = 0; i < rolesArray.length; i++) { 8 h* Z( m3 C/ T# m8 i //若当前用户是rolesArray中的任何一个,则有权限访问3 d4 M) t$ N' q' R if (subject.hasRole(rolesArray[i])) {8 q0 B; J8 Z; ` return true; 7 c! W4 t/ E" ^+ p( x6 `7 f+ D }7 E% T# g7 O0 r# a' d2 u2 ^0 O5 ?6 o } 5 D1 c: U# F) s U6 w9 f. y return false; ; g: v0 e0 s. ?' ^$ g }+ ?3 Q; q# Q. N6 c- Q, ~/ j }" i- @. Y1 u2 l# }) z c- h7 Y

( i# u7 z7 ]3 v/ d1 w

3、自定义token过滤器 token -> 判断token是否过期失效等@Slf4j 1 \& m, g+ q- P4 f ^/ u public class TokenCheckFilter extends UserFilter { 6 p9 v/ Q8 I% H* { /** 6 W4 T5 N) X& n1 G0 v" ^8 a * token过期、失效 ) m% e) |" F& w4 y$ d */5 a3 i; @- J% ^+ g8 ]8 k% B private static final String TOKEN_EXPIRED_URL = "/api/auth/tokenExpired"; ; o' |: S# `6 v# _* M9 f& R /**: Y7 @ ?7 n" [! O' k * 判断是否拥有权限 true:认证成功 false:认证失败" h; |/ B% x% T. ?% M' M) Y5 T: o6 Q# g * mappedValue 访问该url时需要的权限( J2 G Y' O* c2 v/ ^ * subject.isPermitted 判断访问的用户是否拥有mappedValue权限 + v- E9 f: V8 e, L5 V, z5 ~ */ : k/ q1 r! e! `& o4 `' u& Z# N @Override 4 B7 {, [# P- _, b( @' N3 n3 N public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { / W! j* F9 b. m' |' Q HttpServletRequest httpRequest = (HttpServletRequest) request; 6 ^: a$ Q2 I& o+ } HttpServletResponse httpResponse = (HttpServletResponse) response; N) l1 \: E1 y- ?, ~' u // 根据请求头拿到token0 z: s1 _$ `. j) x; @ String token = WebUtils.toHttp(request).getHeader(Constants.REQUEST_HEADER); 4 _3 D! ~7 v5 ?# Q. d log.info("浏览器token:" + token ); 8 V* S2 A, G, s' i/ i/ P! D6 M) ^ User userInfo = ShiroUtils.getUserInfo(); 9 q7 |. W3 \6 k5 O% q9 X1 w4 v String userToken = userInfo.getToken(); y) Y7 m* z/ m3 K W3 B$ I // 检查token是否过期7 N& R! A# p% P) }0 z) h# e- m if ( !token.equals(userToken) ){: |6 j: Q- v' Y1 d8 z" h return false; 6 ]/ P2 R; o$ J ^ } ! D( j1 s2 d% ^2 ] return true;$ i# L5 z: L0 z } ! z; @/ k' ^3 R6 D /**: a6 Q; e, W# a# X5 b- n * 认证失败回调的方法: 如果登录实体为null就保存请求和跳转登录页面,否则就跳转无权限配置页面 4 a, }, I0 _! B% w) C, ]2 y9 `0 a4 e */ 6 ~8 n8 P) T6 ?+ c& _ @Override: Y4 i( |2 T& S/ X+ C protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { 2 [ a1 \% T/ M User userInfo = ShiroUtils.getUserInfo();1 |4 \0 P- P Y // 重定向错误提示处理 - 前后端分离情况下 5 B! _4 @" G6 i: J' F0 I4 K WebUtils.issueRedirect(request, response, TOKEN_EXPIRED_URL);' @) s. j" B/ l6 b* n8 ]9 N1 P return false; 1 D; e( s: g' w }5 @ e( z$ X2 `% ~ }6 ]6 r6 X, N4 z" @. q/ M

7 {6 x, B+ R8 v" `6 f. U

五、项目中会用到的一些工具类、常量等温馨小提示:这里只是部分,详情可参考文章末尾给出的案例demo源码1、Shiro工具类public class ShiroUtils { 1 F' D& s4 j8 s+ S /** 私有构造器 **/ $ y v. u5 Z& W2 G3 v& |1 V private ShiroUtils(){ } " Q9 C* i) t8 e; W1 s( T2 i8 g3 L private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);! ^! O+ w2 `- B* b) x2 O+ S& f /**9 d: C* F" O3 @ * 获取当前用户Session . \" m6 I3 C N, E2 ~1 P7 P. }7 E% h * @Return SysUserEntity 用户信息, f8 X! g0 M } Q1 c */) D. E( L% N6 `2 k+ _+ f/ A- o public static Session getSession() { , Q6 Z; `: h0 y1 i0 H9 g return SecurityUtils.getSubject().getSession();" ?! p3 p/ v3 C+ b9 J* Y1 @ } $ Y- u3 G$ B V/ [) a! M /**2 O% T! _1 M: i8 k/ J * 用户登出 9 h0 p; \% D$ G+ _+ P4 v* ^ */( g& K$ `& h; P% P# Y3 F8 Y public static void logout() {6 a+ Y" Y/ h3 P; |. M SecurityUtils.getSubject().logout();9 n- @- Z4 X2 q6 B8 o" \+ r% f } - q2 n c" [; {% Y, A q5 }8 b1 W /** . X; O9 ~: _; s0 @ * 获取当前用户信息 + h5 w' i0 e, E$ { * @Return SysUserEntity 用户信息 $ p8 T; \7 e! t */6 I* ~4 |9 h0 y% R public static User getUserInfo() {% E- T7 a* ?2 P. S, p2 M ^. j return (User) SecurityUtils.getSubject().getPrincipal(); 0 S1 m: W0 U3 ]/ O; C }0 j$ e2 [4 |3 { /** " R* ]+ h" F7 ]; h& o * 删除用户缓存信息 ! `; W# {) s! r- U' f+ F7 A * @Param username 用户名称 ' J, Q) M* U3 { * @Param isRemoveSession 是否删除Session,删除后用户需重新登录( _0 J# }6 N3 n& E8 @( T */" Z6 M* W" t5 J6 d6 L" D public static void deleteCache(String username, boolean isRemoveSession){ + q: r' b7 ]0 N# M+ @4 j //从缓存中获取Session8 | p+ ?- F. } Session session = null; / q# _' {7 y( x9 w( m% \ // 获取当前已登录的用户session列表 0 q+ u, I0 h! d; t- ^& [. N' x Collection sessions = redisSessionDAO.getActiveSessions();/ t( N4 K5 O2 i+ v q2 x User sysUserEntity; * O: t* t9 g& K" {- B& C) R# V5 K Object attribute = null; 2 F, M5 h5 K. w2 o // 遍历Session,找到该用户名称对应的Session 8 o; k$ q I' I- @7 c, J6 [ for(Session sessionInfo : sessions){ 2 D( N( a' }( _3 d attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);. l2 r' X0 w+ s- b if (attribute == null) { ' s8 d/ x- }" Q/ R( d, S+ b continue; 5 n% d" m# x# g/ k; X } 9 r7 o3 s; X' Q5 n$ H6 | sysUserEntity = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();. V) G" B" y' u1 t8 B if (sysUserEntity == null) { 6 |) \% @( g" |( q0 M8 r3 Q; r continue;0 t* ]: F( \( B% W! n! A' T! X5 x( x" d }: K9 }& @+ Y# }/ n if (Objects.equals(sysUserEntity.getUsername(), username)) { # T8 @6 t1 ]. ?- z+ P session=sessionInfo;7 d# L) c9 z \5 q4 F7 Q4 N // 清除该用户以前登录时保存的session,强制退出 -> 单用户登录处理, B2 B; ^" Z2 ^& o/ Z if (isRemoveSession) {5 d1 N8 r3 I1 F2 x7 s/ z+ i redisSessionDAO.delete(session);! g$ E9 i* F. X+ K0 h" I. z }7 f6 \4 O$ T6 r! S5 O" `5 d, u }) u( f& u3 r/ ^! c1 v$ U }& S s7 ^; D# g6 w if (session == null||attribute == null) {# w* U) e8 b4 ~ return; 0 R! D3 W0 S; W+ V; T& V- J8 x8 b } 9 S9 t9 S6 j* I( ~ _* l% q //删除session t+ {4 ?( n O/ P* i" ^4 b if (isRemoveSession) {! j8 U$ K0 \0 P redisSessionDAO.delete(session); P; r: z1 M8 H8 b6 J0 B }3 k o) v6 A1 K) q- ~2 Z2 g' y2 c //删除Cache,再访问受限接口时会重新授权8 y* d& g5 e Z$ T; ^! \& o DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();( J+ W6 ]8 B. l: B) i1 q1 b5 H Authenticator authc = securityManager.getAuthenticator(); 5 S$ t5 [ i4 _" o3 A* A ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute); 1 s" h1 N# J% U1 d* R9 m) c8 V }# t Z- A: S1 m& n0 @9 m$ L% I /**. |& v; ^0 M6 R1 p * 从缓存中获取指定用户名的Session 0 \. ]; R+ k& O' \ * @param username ' C+ v' ?7 c0 K) r P% g! W" \ */; g: U$ q" v7 `' m private static Session getSessionByUsername(String username){" X* ? T0 L s, P# ~, s // 获取当前已登录的用户session列表 8 D+ o* m3 U4 N1 }% w1 c Collection sessions = redisSessionDAO.getActiveSessions();6 s2 h& s# S; f3 j2 Q User user;% G3 k. u- b6 D; B: R% B1 e Object attribute; 7 b3 g/ Q/ p( C- i6 | // 遍历Session,找到该用户名称对应的Session7 m: |' j+ V: \; U" L- B* f for(Session session : sessions){ 6 `2 e: D, T0 n o) T attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); . `) Q/ R* z9 p- r0 k" c" A) M/ d- v2 _ if (attribute == null) {; O8 u0 t, `0 y: M7 H: C continue; 5 A% |% r: s7 E } 6 h- a$ ~% ]9 x$ k/ A user = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal(); , r' U! s$ I6 x; P- h& _ if (user == null) {2 {' K9 j; g+ d! S1 G: f continue; , N5 A$ G: M. h, J+ A+ ?6 Z/ d- H } 1 d& q0 D* M. d. V' s( Z7 V/ Y4 T3 Q if (Objects.equals(user.getUsername(), username)) { $ |& W7 G* `# O/ o7 _" k* \. } return session; 4 W: m4 Y1 l; H7 c5 y } & {# @/ p$ v f# b7 n" v0 K } 6 T: P+ ~ F" |7 N' ~ return null; * U( G/ G( g2 x. w4 N: y' x }3 D8 b: u4 b" Z e6 }; B8 q) x }8 \4 M% ?' O6 _3 h1 J* ] {3 N- z

. m7 q! ]& W7 N3 e% ^

2、Redis常量类public interface RedisConstant {9 G4 H; y y( u /** . T# C' j! u7 c3 L- }& e2 `$ d * TOKEN前缀 % I ^# w5 L8 u% { */ 5 e9 a# W' D: ` String REDIS_PREFIX_LOGIN = "code-generator_token_%s"; `# f- [, k& J$ U1 d; n2 t }$ \5 A+ t$ ~: K2 ?+ F

) B; _- d: P9 c' }5 H9 \

3、Spring上下文工具类@Component 0 Y) y5 A: ` W9 Q0 V( q' x public class SpringUtil implements ApplicationContextAware { $ z/ e& U8 z1 F$ B& ? private static ApplicationContext context; " V4 N% N: C% f& z /**9 w2 V( s. U* M, C# E * Spring在bean初始化后会判断是不是ApplicationContextAware的子类 9 |# z0 R. n& L+ L h5 V * 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去9 J9 Q z7 a+ W% _6 x1 X */ ! O6 L$ ~ M7 m2 l+ k1 p+ V9 l$ [ @Override 3 c8 S7 h5 Z8 q; K' W1 B! m public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { . I1 `. m* M0 H4 W$ ]: X, S% ? context = applicationContext;! \2 V+ M& {7 h6 u! c0 e Y } " I- Z3 [. X8 x$ a$ ^ /**! K' i+ N/ ^ U! Y* H4 K8 I L * 通过Name返回指定的Bean/ j; [* W! z: l */, b- t, d( S% k0 ~) P7 {) F public static T getBean(Class beanClass) {1 S& a7 `9 D3 w$ O- r5 P return context.getBean(beanClass);7 Y* a5 w N) a+ b6 V0 V } ( }4 B. i1 c i. c0 t& e9 V- E. j% w }: G6 E7 h8 X+ [. ^- F# W! j

- M, A9 z1 Q4 [8 @

六、案例demo源码GitHub地址:https://github.com/zhengqingya/code-generator/tree/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro

# ~+ s [* O5 s6 V8 l2 E3 F

码云地址:https://gitee.com/zhengqingya/code-generator/blob/master/code-generator-api/src/main/java/com/zhengqing/modules/shiro

7 W, d/ I+ v0 q) G, O" H- m+ h / G: {8 t9 E1 n$ Z# B4 c) f1 @! E8 f. l' @! k" z 9 s- B% o3 N, H/ ^ 8 `# [: g# b' Q; {

暂时无法加载帖子列表

回复

使用道具 举报

一品郎中 发表于 2026-10-09 17:03:06 | 显示全部楼层
这个思路很新颖,打开了新世界的大门,谢谢分享
回复

使用道具 举报

牛尔 发表于 2026-10-11 08:53:27 | 显示全部楼层
这个分享太实用了,刚好能用到,感谢楼主!
回复

使用道具 举报

guanjiesj 发表于 2026-10-19 00:59:01 | 显示全部楼层
分析得很透彻,很多细节都说到点子上了~
回复

使用道具 举报

    您需要登录后才可以回帖 登录 | 加入怎通

    本版积分规则

    QQ|手机版|小黑屋|网站地图|真牛社区 ( 苏ICP备2023040716号-2 )

    GMT+8, 2026-10-31 15:23 , Processed in 0.077875 second(s), 25 queries , Gzip On.

    免责声明:本站信息来自互联网,本站不对其内容真实性负责,如有侵权等情况请联系420897364#qq.com(把#换成@)删除。

    Powered by Discuz! X3.5

    快速回复 返回顶部 返回列表