
聊到Java后端的安全框架Shiro绝对是一个绕不开的名字。我第一次接触它是在一个后台管理系统的权限改造里业务方只提了一个需求实现登录认证和菜单权限控制。当时我在Spring Security和Shiro之间犹豫最后敲定了Shiro原因很朴素——它的学习曲线更平缓文档虽然旧但案例足够多代码侵入性小而且认证、授权、会话管理、加密这几件安全领域最常见的杂事它打包得特别干净。Shiro本质上是Java生态里一个轻量级的安全框架解决的核心问题是“你是谁、你能干什么、你的状态怎么维持”。它不像某些安全方案那样强行改变项目结构而是通过几个核心组件把安全逻辑从业务代码里抽离出来让开发人员不用在每一个接口里手写校验逻辑。这篇文章不打算泛泛罗列概念我会从项目定位、核心架构、认证授权、Realm实践、加密处理、会话管理、Spring Boot整合到问题排查这几条线完整走一遍重点说清楚每个环节背后的设计原因和实操中会踩的坑。适合谁来读如果你是Java后端开发刚接触Shiro或者已经用过一段时间但被各种配置文件搞得很晕这篇内容可以帮你把零散的知识点串成一条线。即使你之前只做过简单的登录功能也可以照着后面的代码配置把手里的项目跑起来。1. 项目定位与选型评估为什么不少项目最终选了Shiro1.1 Shiro到底解决了什么问题很多新人第一次看到Shiro容易把它和“登录拦截器”画等号。实际上Shiro的价值远不止拦截未登录请求那么简单。平时在后端项目里安全相关的需求通常有五个方面登录认证、权限校验、会话跟踪、数据加密和潜在的并发登录控制。如果每个接口都自己写一套校验逻辑代码会迅速腐化到处都是重复的判断和散落的权限字符串。Shiro把这些统一抽象成一套机制开发人员只需要声明“这个路径需要什么权限”或者“这个方法需要哪个角色”剩下的事框架来处理。具体来说Shiro提供了四个核心能力域。第一个是认证负责验证用户的身份比如用户名的密码是否正确、验证码是否匹配第二个是授权负责判断当前用户有没有权限执行某个操作第三个是会话管理它不依赖Servlet容器可以独立管理用户会话甚至支持把会话数据放到Redis里实现多节点共享第四个是加密提供了一套封装好的哈希、加盐工具。这四个能力结合起来基本覆盖了一个中小型后台系统90%的安全需求。1.2 和Spring Security相比如何取舍这是我在写后台系统时经常被问到的问题。从功能完整度来说Spring Security确实更强大和Spring生态的整合也更紧密在复杂的企业级权限模型、OAuth2.0、CAS单点登录等场景下表现更好。但Spring Security的学习曲线明显更陡峭它内部有一套复杂的过滤器链和大量的配置对象新人经常会陷入“不知道哪个过滤器拦截了请求”的迷茫。Shiro的优势在于概念模型简单核心组件只有几个源码读起来也轻松。它的Configuration可以集中在几个Bean里过滤链规则直白权限注解和JSP标签都很好理解。对于大多数内部管理系统、后台运营平台、中小型单体应用来说Shiro完全够用而且能让团队快速上手。如果项目本身是微服务架构、大量使用Spring Cloud Gateway或需要深度的OAuth2集成那我会建议优先考虑Spring Security。1.3 适用场景与选型建议从我接触过的项目来看Shiro最舒服的场景是传统单体后台、带管理端的前后端一体项目以及一些对权限模型要求不复杂的互联网应用。凡是“登录后访问受限资源、不同角色看不同菜单、接口按照权限字符串控制”这一类需求Shiro都能快速落地。不太适合的场景是需要高度定制化的安全协议、需要与第三方IdP做复杂联合登录、或者你的团队本来就非常熟悉Spring Security。选型没有绝对的谁比谁好关键是看团队的学习成本和项目的实际复杂度。Shiro最大的优势是“够用且简单”你不需要为了一个简单的登录功能引入一套庞大的安全基础设施。2. 核心架构与认证授权原理2.1 认准这三个核心组件就够了Shiro的架构看起来有一堆术语但真正需要深入理解的只有三个组件Subject、SecurityManager和Realm。Subject是“当前用户”的抽象。这个用户不一定是自然人也可能是第三方进程、定时任务等。在代码里我们通过SecurityUtils.getSubject()拿到当前请求对应的Subject对象然后调用它的login()方法、isPermitted()方法、getSession()方法等。可以把Subject看作是一张门禁卡它代表当前操作者的身份和权限状态但卡片本身不负责校验它只是把请求转交给背后的管理者。SecurityManager是Shiro的心脏它负责协调所有安全操作。Subject的所有委托最终都汇聚到SecurityManager里。它管着多个子组件比如Authenticator负责认证、Authorizer负责授权、SessionManager负责会话。实际开发中我们很少直接操作SecurityManager但它必须在Spring配置里被注册否则整个安全体系转不起来。Realm是我的理解里最关键的组件它是一层“数据桥”Shiro并不关心你的用户数据放在数据库、LDAP还是配置文件里Realm就是专门干这个的。认证时它负责根据用户名查到密码和相关凭证授权时它负责查出当前用户的角色和权限列表。写Shiro项目时大部分开发工作量其实都在写自定义Realm。2.2 认证流程拆解从Token到AuthenticationInfoShiro的认证流程可以用“提证、核实、放行”三个阶段来理解。用户提交账号密码后Controller里会创建一个UsernamePasswordToken把账号和密码放进去。然后调用subject.login(token)。这一步只是个开始Token会经过SecurityManager被转交给内部的AuthenticatorAuthenticator再根据配置找到对应的Realm把Token丢给Realm。Realm收到Token后第一件事是从Token里取出身份信息比如用户名。接着查数据库或缓存拿到这个用户真正的密码、盐值等数据。这里有一个容易踩的坑如果查不到用户Realm应该返回null而不是抛异常Shiro会把null理解为“查无此人”然后抛出UnknownAccountException。如果用户被锁定或禁用可以抛LockedAccountException。查到了之后Realm需要把这些数据封装成一个AuthenticationInfo对象里面至少包含身份标识和凭据。等Realm返回了AuthenticationInfo真正的密码校验才开始。Shiro使用CredentialsMatcher来比对用户提交的明文密码和系统中的凭证。如果配置了加盐哈希这一步会先把用户提交的密码用同样的盐值、同样的算法和迭代次数计算一遍再和库里存的值做比对。比对通过subject.login()返回Subject被标记为已认证状态比对失败抛出IncorrectCredentialsException。整个流程下来业务代码里不用写任何密码比较的逻辑全部交给框架处理。2.3 授权机制拆解角色、权限与通配符授权往往比认证更容易让人困惑因为很多项目把角色和权限混为一谈。我习惯用一个比喻来解释角色是职位头衔权限是具体动作。比如“管理员”是角色“删除用户”是权限。Shiro同时支持基于角色和基于权限的两种判断方式但实际开发里我更推荐基于权限判断因为权限字符串的粒度更细也更灵活。Shiro里最常用的权限字符串用冒号分隔基本格式是“资源:操作”例如user:view表示查看用户user:create表示创建用户user:*表示对用户的所有操作。这样写的好处是权限粒度可以动态调整不用为了一个操作去改代码。有两个判断方法要区分hasRole(admin)是判断角色isPermitted(user:delete)是判断权限。注意hasRole是全等判断用户必须有指定的角色才返回true而权限字符串支持通配符和部分匹配。授权信息的获取由Realm的doGetAuthorizationInfo方法完成。这个方法会在需要判断权限时被触发它返回一个SimpleAuthorizationInfo里面可以塞入角色集合和权限字符串集合。这里有个性能隐患默认情况下Shiro每做一次权限判断都可能调用Realm去查库所以实际项目中一定要配置授权缓存让权限数据在内存里缓存起来否则高并发下数据库压力会非常大。3. Realm实践与密码加密的正确姿势3.1 Realm的设计思路安全框架与业务数据的桥梁很多人在第一个Shiro项目里就一个Realm走到底这本身没错但理解Realm的设计思路会帮你更好地应对复杂场景。Realm的核心职责是“把业务数据翻译成Shiro能理解的安全数据”。业务数据是用户名、密码字段、状态标记安全数据是AuthenticationInfo和AuthorizationInfo。翻译工作全部在Realm内部完成。继承AuthorizingRealm是最常见的做法因为AuthorizingRealm同时实现了认证和授权两个接口一套类就能搞定。AuthorizingRealm本身还内置了凭据匹配器默认会通过简单的equals比较提交密码和存储密码。我们通常需要配置一个HashedCredentialsMatcher来支持哈希加盐的密码校验。如果项目里密码存储方式比较特殊比如需要通过另一个服务远程校验也可以重写doCredentialsMatch方法直接把匹配逻辑替换掉。我实际做项目时还会做一个小小的设计在Realm里不直接写数据库查询语句而是调用Service层。这样做的好处是权限数据变化的时可以在Service层做缓存处理避免Realm和持久层耦合。如果你的项目里有多种用户类型比如后台管理员和前台用户可以考虑配置多个Realm并在ModularRealmAuthenticator里指定各自的认证策略不过对大多数后台系统来说一个Realm足够。3.2 自定义Realm从零写一个AuthorizingRealm直接看代码最直观。下面是一个典型的自定义Realm在模拟项目X里我按这个模板改一改就能跑通。先看认证部分public class MyRealm extends AuthorizingRealm { private UserService userService; Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { return null; } if (user.getStatus() 0) { throw new LockedAccountException(账号已被锁定); } return new SimpleAuthenticationInfo( username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); SetString roles userService.findRolesByUsername(username); SetString permissions userService.findPermissionsByUsername(username); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(permissions); return info; } }两个方法对应两条链路doGetAuthenticationInfo负责“取数据”返回AuthenticationInfo之后Shiro的CredentialsMatcher会自动比对密码doGetAuthorizationInfo负责“取权限”在权限判断时被触发。有几个细节值得注意认证返回的SimpleAuthenticationInfo构造函数里传入了getName()这是Realm的名字如果是多Realm场景它会成为区分依据密码字段我们放的是哈希后的值而不是明文这是绝不能妥协的安全底线。3.3 密码加盐存储规范与校验过程密码存储是安全框架里最容易被小看的部分。早期很多系统直接用MD5做哈希还洋洋得意实际上现在的计算能力下裸哈希非常容易被彩虹表破解。Shiro从设计上就支持加盐哈希这是让我对它加分的地方。加盐的思路不复杂每个用户生成一个随机的盐值字符串把“原始密码 盐”一起做哈希运算重复很多次然后把哈希结果和盐值一起存进数据库。校验时拿出盐值对用户提交的密码做同样的哈希和数据库里的结果比对。这样即使两个用户密码相同存出来的哈希值也不同。在Shiro中注册HashedCredentialsMatcher时三件套要配齐哈希算法、哈希迭代次数、存储格式。我在项目里常用这么一段来生成密码String salt RandomStringUtils.randomAlphanumeric(16); String hashedPassword new SimpleHash( SHA-256, rawPassword, salt, 1024 ).toHex();写入Realm配置时把同一个salt塞进SimpleAuthenticationInfo的第三参数这样HashedCredentialsMatcher就知道怎么重算了。关于迭代次数1024次是我在各项目里的保守选择。不是越高越好因为每次登录都要重复计算太高的迭代次数会明显拖慢接口响应。如果用到SHA-512或更慢的算法300到500次也比较稳妥。关键是“随机盐 足够多迭代”这两条满足以后密码存储基本是安全的。4. 会话管理与并发登录控制4.1 Shiro会话语义与默认配置Shiro的会话管理是它区别于很多拦截器方案的地方。普通的Servlet Session由Tomcat这类容器管理但Shiro可以脱离容器独立管理这意味着同样一套会话机制可以同时服务Web请求和非Web请求。在Web项目里Subject.getSession()拿到的就是Shiro会话它底层和HttpSession有关系但外层语义完全由Shiro控制。默认情况下DefaultWebSessionManager会把会话ID通过Cookie写给浏览器Cookie名称默认是JSESSIONID。有一点需要注意Shiro的Cookie是有失效时间的会话超时时间通过sessionManager.setGlobalSessionTimeout()控制单位是毫秒。假如不设置默认大概是30分钟。如果项目里希望用户长时间保持登录可以把超时时间调大但千万不要无限延长否则安全风险很高而且后端长期保存大量会话也会吃内存。这里还要提到一个常见的配置项sessionValidationInterval。它表示会话验证的周期Shiro会定期扫描过期会话并清理。默认值和全局超时相关如果测试时发现会话迟迟不失效多半是这个扫描周期太长。调整这个参数时记住一个原则扫描越频繁越及时但代价是消耗一些CPU。4.2 会话持久化把Session交给Redis只使用本地内存的会话管理有一个致命的局限应用重启后会话全部消失用户被迫重新登录。部署多实例时用户在一台机器上登录下一次请求被负载均衡转到另一台机器会话就丢了。解决这个问题的方法是实现一个自定义的SessionDAO让会话数据落到Redis里。大概思路是统一接管Session的创建、读取、更新和删除。Shiro的AbstractSessionDAO提供了骨架你需要实现doCreate、doReadSession、update和delete等方法。在Redis里存储时我习惯用“shiro:session:”前缀加会话ID作为KeyValue用JDK序列化或JSON序列化都行。需要注意Redis中的过期时间要和会话超时时间保持一致这样Redis自动淘汰过期Key不用额外写清理任务。每次请求进来时DefaultWebSessionManager会从Cookie取出会话ID通过SessionDAO从Redis加载Session。如果Redis里没有那就表示会话已失效请求会被踢到登录页。这个方案有几个坑一是序列化工具要全局一致否则CRUD混乱二是写入Redis的数据量别太大Session里不要塞大对象三是Redis连接池要高可用否则Redis宕机会导致整个系统无法登录。4.3 强制下线与并发登录控制后台系统有个高频需求管理员踢人或者限制同一个账号同时只能在一处登录。Shiro原生没有直接提供“踢人”API需要基于Session的机制自己实现。最直接的做法是维护一个在线用户列表把用户登录后的SessionID记录到某个可查找的地方比如Redis里的一个Hash结构字段是用户ID值是SessionID。当需要踢人的时候找到对应的SessionID然后执行停止会话的操作。在Shiro里可以这样做从SecurityManager获取SessionManager再通过SessionDAO读取对应的Session对象调用stop()方法。会话停止后用户下一次请求就会因为会话失效被打回登录页。如果控制台同时限制单用户登录在用户新登录时先查一下Hash结构里这个用户是否已有Session有的话就先停掉旧会话再让新会话上线。实际踩坑提醒用Java原生的Map存userId - sessionId在单体测试没问题但一旦部署多实例就失效了因为每个实例的Map是独立的。多实例环境建议直接把在线关系也放到Redis并给Key设置合理的过期时间防止用户长期不操作时留下脏数据。5. Spring Boot整合实操过程5.1 依赖引入与配置类编写要点Spring Boot引入Shiro很简单关键在版本兼容。目前主流的做法是引入shiro-spring包我建议查看官方最新的版本列表选一个和当前Spring Boot版本兼容的稳定版。你不需要引入太多模块对Web项目来说shiro-spring和shiro-web加在一起就覆盖了大部分功能。配置类要管三件事创建Realm、创建SecurityManager、创建ShiroFilterFactoryBean。Realm的Bean要注册到Spring容器里SecurityManager要接收Realm作为依赖ShiroFilterFactoryBean获取SecurityManager后配置过滤链。我通常还会配上LifecycleBeanPostProcessor和AuthorizationAttributeSourceAdvisor前者负责管理Shiro内部组件的生命周期后者是启用RequiresPermissions这类注解的关键。漏掉AuthorizationAttributeSourceAdvisor注解权限会静默失效这是排查问题时第一个要检查的地方。过滤链的顺序千万不能搞错。Shiro的表单过滤器和支持匿名访问的匿名过滤器有匹配顺序问题代码里其实已经体现出来了。比如将要过滤的URL规则放入LinkedHashMap中每次匹配时它会从上往下找第一个匹配的规则。如果把/** authc写在前面后面的/login anon就永远没有机会生效登录页也被拦截了。5.2 ShiroFilterFactoryBean与过滤链的正确配置过滤链是Shiro穿插进请求过程的核心机制。每个请求经过Shiro过滤器时会被一个过滤器链处理链上的每个过滤器决定放行、拦截或跳到登录页。Shiro内置了多个过滤器anon允许匿名访问authc要求登录认证logout主动注销roles要求角色perms要求权限字符串。我在项目里常用下面这份配置发现它非常直观Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl(/login); factoryBean.setSuccessUrl(/index); factoryBean.setUnauthorizedUrl(/403); MapString, String filterChainMap new LinkedHashMap(); filterChainMap.put(/login, anon); filterChainMap.put(/logout, logout); filterChainMap.put(/css/**, anon); filterChainMap.put(/js/**, anon); filterChainMap.put(/captcha, anon); filterChainMap.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChainMap); return factoryBean; }两个经验教训第一静态资源一定要放在所有规则的最前面并标记anon否则用户还没登录就什么都看不到页面全被拦截进登录页了第二把登录接口标记为anon是必要的但登录接口不能同时被authc拦截否则就形成了死循环。另外logout过滤器是一个特殊的工具它会在执行登出逻辑后直接跳到登录页不用在代码里手动写退出接口。5.3 权限注解与接口级别的权限控制过滤链解决的是URL级别的控制注解解决的是方法级别的控制。两者可以配合使用但职责要分清楚粗粒度的页面和接口拦截用过滤链细粒度的操作权限用注解。注解方式非常简洁在Controller方法上标注RequiresPermissions(user:delete)没有权限的用户会被拒绝并跳转到未授权页面。常用注解有三个RequiresRoles、RequiresPermissions和RequiresAuthentication。用的时候注意一个坑注解只对Spring管理的Bean方法有效。Spring AOP代理没有配置好或者方法被同类内部调用都可能绕过代理导致注解不生效。对于多个权限要求“或”的关系和“与”的关系要用logical Logical.OR或Logical.AND明确指定否则默认语义可能和你理解的不一致。另外一个容易误导人的地方过滤链即使配置了权限字符串过滤比如/admin/** perms[admin:manage]它也只对精确匹配这个权限字符串的用户生效。很多新手以为配置了过滤链就可以不用注解了实际上过滤链更常用在页面级别比如整个后台功能路径都要求登录细粒度的“这个按钮只有管理才能点”还是交给注解做更清晰。5.4 前后端分离模式下的配置变通如果你做的是前后端分离项目Shiro的配置思路要跟着调整。传统后台有服务端页面跳转Shiro默认跳转loginUrl的行为是直接返回登录页的HTML。但在前后端分离下后端只返回JSON前端拿到401或403状态码自行跳转。Shiro显然也能支持这种模式只是需要做一些定制。我常用的办法是自定义一个过滤器或者重写认证失败处理。最简单的方式是保留authc过滤器但把setLoginUrl改成真正的登录接口路径同时写一个全局异常处理器捕获AuthenticationException和AuthorizationException返回统一JSON格式。另一个可选方案是继承FormAuthenticationFilter重写onAccessDenied方法。无论哪条路核心思路都是把Shiro的页面跳转行为替换成状态码或JSON返回。还有一个经常被忽略的细节跨域问题。前后端分离时前端和后端域名往往不同浏览器会先发一个OPTIONS预检请求。如果Shiro过滤链把所有未登录请求都拦截那么预检请求也会被拦截导致前端拿不到响应。解决办法是在过滤链最前面显式放行所有OPTIONS请求或者自定义过滤器直接跳过OPTIONS请求只对真正的业务请求做安全判断。这个坑我见过太多次了排查半天最后竟然是拦了预检请求。6. 常见问题与排查技巧实录6.1 认证相关的坑与排查认证阶段的问题通常集中在登录跳转和服务端报错上。一个非常常见的问题是登录成功后无限重定向或退回登录页。这往往不是密码错误引起的而是过滤链顺序错了。举个例子如果/** authc出现在/login anon之前那登录页本身也被拦截系统会一直把用户往登录页赶。要做的第一件事就是检查过滤链用LinkedHashMap保存的顺序。第二个常见问题是登录时报IncorrectCredentialsException但密码明明没输错。这种情况八成是认证信息里的密码类型对不上。比如数据库里密码字段存的是十六进制字符串但Realm里返回的是字节数组凭据匹配器一比较就发现类型不匹配。排错时可以临时在Realm里打印出两边数据的类型和内容确认完全一致后再做哈希校验。第三个问题是用户被锁定却还是能登录。如果数据库里有状态字段那必须在doGetAuthenticationInfo里主动判断并抛出LockedAccountException。Shiro不会自动读取用户状态你可以不抛异常它就不管。养成一个习惯在Realm里把所有账号状态相关的判断都做掉别等到Service层才发现状态有问题。6.2 权限不生效的典型场景注解权限不生效是我收到问题里频率最高的。从经验来看50%的情况是AuthorizationAttributeSourceAdvisor没有注册到Spring容器里导致RequiresPermissions注解被完全忽略。另外30%是方法没有被Spring代理比如在同一个类内部调用带注解的方法绕过了代理。还有20%是Realm返回的权限字符串和注解里的字符串对不上比如大小写不一致、冒号写成了点号。排查思路先确认注解能不能生效再确认数据层返回的权限列表。有一种情况我确实经常看到用户有某个角色但过滤链配置的是权限字符串两者对不上页面就怎么都进不去。记住一条规则角色和权限在Shiro中是两套独立的体系roles过滤器判断角色perms过滤器判断权限不要混着用否则检查半天都找不到原因。还有一种隐蔽的错误缓存了授权信息导致权限修改后不生效。一旦配置了CacheManagerdoGetAuthorizationInfo返回的权限集合会缓存起来。用户在后台改了权限理论上需要重新登录或者清理缓存才能看到效果。排查这类问题的方法是在代码里显式调用subject.getSession().removeAttribute或者通过SecurityManager清理该用户的认证缓存。如果项目对权限变更要求实时生效就必须设计缓存清理方案。6.3 会话与并发问题速查表会话和并发问题的定位相对容易我把常见的现象、原因和处理方式整理成一个速查表平时排错先看这张表能省很多时间。现象可能原因处理方式应用重启后所有用户都要重新登录会话保存在本地内存未持久化配置自定义SessionDAO存储到Redis多实例部署下会话随机失效会话没有在多节点间共享引入Redis会话共享方案会话超时时间不生效会话验证周期太长或配置位置错误调整sessionValidationInterval与globalSessionTimeout用户被强制下线后还能操作旧的会话没有被同步停止执行踢人时通过SessionDAO拿到旧会话并stop同一账号重复登录不受限没有做并发登录控制在登录成功后记录会话ID重复登录时踢掉旧会话跨域请求发送OPTIONS被拦截过滤链拦截了预检请求在过滤链中放行OPTIONS请求或自定义过滤器处理6.4 几个容易忽略的安全细节排查问题之余我还想提到几个平时容易忽略的安全细节。第一个是路径匹配。很多系统用/**拦截所有路径但Shiro的路径匹配有自己的规则如果一个URL包含分号或特殊编码可能导致过滤器匹配不上。在安全要求较高的场景下建议给容器配置严格的路由并自定义过滤器处理路径规范化问题。第二个是Session固定攻击。用户在登录前拿到一个会话ID登录后如果会话ID不更换攻击者可以通过固定会话ID冒充用户。Shiro默认的会话管理在登录前后会重建会话但有些自定义方案会丢掉这个特性。如果自己接管会话流程必须确保登录成功后会话ID被重新生成。第三个是权限字符串设计。建议不要用没有明确边界的权限字符串比如只用一个字符串admin表示所有权限这样后期想给某个用户开放部分权限就完全没法做。尽量拆分成“资源:操作”这样的格式配合通配符使用这样维护起来灵活得多。最后一个细节是RememberMe。它虽然方便但默认实现是序列化到Cookie里存在被篡改和反序列化的风险。如果项目必须使用它一定要配置强密钥并且不要让未登录用户也持有有效会话。实际项目中我一般建议默认关闭RememberMe真需要就单独做安全的持久化登录方案。我在实际项目里使用Shiro的体会是这个框架最让人安心的地方在于它把复杂的认证授权流程收敛成了几条明确的组件和配置路径。刚开始接触时配置文件看上去像一团乱麻但弄清楚过滤链的顺序、Realm的职责、凭据匹配的机制之后剩下的事基本就是按部就班地填业务数据。如果你手头有一个项目正在做权限改造照着这条主线一步一步梳理下来比到处搜“某个配置为什么没用”要靠谱得多。最后再分享一个习惯每次调整过滤链前先用测试用例把登录、登出、未授权三条路径全部跑一遍再小的改动都别跳过这个习惯能在后续改造里帮你省掉大量排错时间。