资讯动态

Apache Shiro认证授权实战:从Realm到权限模型设计

发布时间:2026/10/24 1:25:25 来源:尧图企业网站定制
1. 从登录到授权先搞清楚Shiro到底在帮你做什么做了几年Java后端只要涉及用户登录和权限控制几乎绕不开Apache Shiro。很多初学者刚接触Shiro时容易陷入一种误区把它当成一个“登录框架”来学配个登录接口、存个session就算完事了。实际上Shiro的核心价值在于一套完整的安全控制链路认证Authentication解决“你是谁”的问题授权Authorization解决“你能干什么”的问题两者连在一起才是项目权限体系的全部。我最早用Shiro是因为那个阶段项目技术栈偏轻量不想引入Spring Security那种庞然大物。Shiro的优点是非常容易嵌入任意Java应用——不限于Spring Boot连纯Servlet项目、JFinal这类小众框架也能集成API设计直观概念少且清晰。它的核心动作优势在于你只需要关心如何告诉Shiro“用户是谁”和“用户拥有哪些权限”剩下如何拦截请求、如何校验权限、如何创建会话Shiro自己处理。这个标题提到“permission管理”重点其实在授权侧。但权限管理不是孤立的功能点它建立在认证之上。比如你给某个接口配置了user:add的权限要求如果一个用户根本没登录Shiro连“这个人是谁”都不知道又何谈校验权限所以理解Shiro必须把认证和授权当成一条链路上的两个环节来看。适合阅读这篇文章的读者我假设你已经写过基本的Spring Boot接口至少知道如何建表、写Service、调用Mapper但对Shiro还是半懂不懂。下面我用一套可落地的代码配置结合具体的权限模型设计把认证和授权的完整链路拆开讲透。2. 认识Shiro三件套Subject、SecurityManager、Realm2.1 核心组件到底各管什么Shiro的设计精炼到可以用一句话概括应用层只和Subject打交道SecurityManager是调度中心Realm是你和业务数据的桥梁。这三个组件是所有Shiro应用的基石。Subject代表当前“正在操作的用户”或者“正在执行的代码”。它封装了SecurityManager的所有功能暴露给业务层的API是login()、checkPermission()、hasRole()、getPrincipal()等。代码里随处可以用SecurityUtils.getSubject()拿到当前用户态的句柄但注意这不是从数据库查出来的用户实体而是当前会话上下文中的安全主体。SecurityManager这是Shiro内部的核心负责协调认证、授权、会话管理等一切内部逻辑。它不直接暴露给业务层但是你在做配置时所有的东西最终都要组装进SecurityManager。比如new DefaultWebSecurityManager()然后把Realm、SessionManager、RememberMeManager一并set进去。Realm这是唯一需要你自己写代码的地方。它做的事很纯粹认证时负责根据用户名找到用户信息和凭证授权时负责根据用户身份加载角色和权限列表。简单说Realm就是Shiro和数据库之间的一层“查询器”。2.2 用户视角下的完整请求链路一个典型的请求处理链路大致是这样的用户访问一个受保护的资源 → Shiro的过滤器链拦截请求 → 如果发现是一个需要权限的URL先检查当前Subject是否已经认证过没有认证就跳转登录认证过了再检查授权信息没有权限就抛异常或返回403。我在实际项目里给新同事讲这个流程时经常用一个类比Subject相当于员工工牌SecurityManager是门禁中控Realm则是员工档案系统。你刷卡进门门禁先查档案确认你是本公司的人认证再根据你的职位级别决定放行到几楼几区授权。档案系统只负责提供信息决定权在门禁中控手里。这样理解就非常清晰。3. 认证阶段从“用户是谁”到“凭证校验通过”3.1 认证的核心链路拆解Shiro的认证流程走的是标准令牌模型代码层面的核心链路如下用户在登录页面输入用户名和密码业务层把这两个值包装成一个UsernamePasswordToken。调用subject.login(token)Shiro会将token交给SecurityManager。SecurityManager找到配置好的Realm调用其doGetAuthenticationInfo(token)方法。这个方法是自己写的根据token中的用户名去数据库查用户记录查不到就抛UnknownAccountException查到了就取出数据库存储的密码和salt连同其他信息一起封装成SimpleAuthenticationInfo返回给Shiro。Shiro拿到返回的AuthenticationInfo后调用配置好的CredentialsMatcher凭证匹配器做最终校验就是把你输入的密码和数据库查出来的密码进行比对。匹配成功认证通过失败则抛IncorrectCredentialsException。第5步要特别讲清楚。很多人误以为doGetAuthenticationInfo里面要做密码验证其实不用。Shiro设计了一个CredentialsMatcher接口目的就是为了把“查询用户信息”和“校验凭证”这两个职责分离。你可以自定义匹配逻辑也可以用Shiro内置的几种实现。3.2 密码校验加盐哈希是必须的不是可选的在我参与过的项目里最常见的安全问题就是明文存密码甚至有人只做一次MD5就存库。这种做法在现在动辄拖库的安全环境下等于裸奔。正确做法是密码存储必须加盐后哈希推荐使用MD5/SHA-256加盐迭代多次。Shiro内置了HashedCredentialsMatcher配置一下就能完成加盐校验非常省事。注册用户时生成密码摘要的代码大致是这样注意salt是随机生成的// 随机生成盐值 String salt new SecureRandomNumberGenerator().nextBytes().toHex(); // 摘要算法MD5哈希迭代次数2次 SimpleHash hash new SimpleHash(MD5, password, salt, 2); String encodedPassword hash.toHex(); // 入库user表存储 encodedPassword 和 salt user.setPassword(encodedPassword); user.setSalt(salt);这里的关键参数要解释一下SimpleHash构造方法四个参数分别是算法名、明文密码、盐值、迭代次数。迭代次数越多破解成本越高但也不是越大越好太大会拖慢登录速度。我一般选2次有人在合规要求高的系统里用1024次甚至更多那是另一套性能取舍。认证时自定义Realm里要做的就是把salt从数据库取出来装配到AuthenticationInfo中Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upt (UsernamePasswordToken) token; String username upt.getUsername(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } // 参数principal主体、数据库中的密码、数据库中的盐值、当前realm名称 SimpleAuthenticationInfo info new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); return info; }再配合ShiroConfig里的匹配器配置Bean public HashedCredentialsMatcher credentialsMatcher() { HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(MD5); matcher.setHashIterations(2); return matcher; }这样Shiro在拿到你返回的AuthenticationInfo后会用配置好的匹配器把用户输入的原始密码用同样的算法和salt再算一遍比对结果是否一致。整个过程中Realm不需要自己写校验逻辑Shiro全帮你干了。3.3 登录成功后业务层要做什么认证成功不等于什么事都完了。实际项目中登录成功后我还习惯做这几件事更新用户最后登录时间、清理旧的登录缓存、签发自己的业务Token如果前后端分离以及把用户核心信息放入Subject的session方便后续获取当前用户。Shiro中获取当前登录用户的方式是Subject subject SecurityUtils.getSubject(); String username (String) subject.getPrincipal();如果你在doGetAuthenticationInfo中把principal设置成了用户实体对象而不是用户名那么这里可以直接强转成User对象使用。我建议不要把整个用户对象塞进principal因为principal会保存在session里如果加载了不该序列化的字段会导致潜在的性能和安全隐患。放个用户ID或用户名足矣要用完整信息再去数据库查。4. 授权模型权限、角色、资源之间的关系4.1 权限与角色的数据库设计授权模块的核心是权限模型通常用五张表搞定用户-角色-权限的关系sys_user用户表sys_role角色表sys_menu/sys_permission权限表权限点可以精确到按钮操作sys_user_role用户角色关联表sys_role_permission角色权限关联表这个模型对应经典的RBAC基于角色的访问控制设计。通俗解释用户不直接持有权限而是通过“角色”间接获得权限。现实中角色代表岗位职责比如“运营”、“管理员”、“审计员”权限代表具体操作比如“用户新增”“订单导出”。分工清晰后期调整权限时只需要改角色和权限的绑定关系不用一个个改用户。4.2 Shiro中权限字符串的构建规则Shiro的权限字符串本质上就是你自己定义的一种“权限通配符”没有强制规范但业界主流是采用资源:操作或资源:操作:实例的三段式结构。比如user:add新增用户user:edit编辑用户user:delete删除用户order:export:2024导出2024年的订单数据实例级权限这种结构的好处是和URL路径容易对应比如你接口的路径是/user/delete那你配置URL权限时直接写user:delete符合直觉。且Shiro天然支持通配符如果你给某角色配置了user:*那么它对user下所有操作都有权限给角色配置*:*那基本就是超级权限了。这比传统的单一字符串权限比如只写一个userDelete灵活得多因为后面加一个实例范围也无需改动整体模型。4.3 授权查询要做哪些事对应到代码Shiro的授权入口是自定义Realm中的doGetAuthorizationInfo(PrincipalCollection principals)方法。这个方法的执行时机要特别注意并非每次请求都调用Shiro会缓存授权信息。默认不缓存的话每次验证权限都查数据库性能会有严重折损。所以先配置好授权缓存然后再写查询逻辑。我通常在doGetAuthorizationInfo里这样实现Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 查角色 SetString roles roleService.findRoleCodesByUsername(username); info.setRoles(roles); // 查权限 SetString permissions permissionService.findPermsByUsername(username); info.setStringPermissions(permissions); return info; }有个细节角色编码我习惯用ROLE_ADMIN、ROLE_OPERATOR这种前缀格式因为后面做注解式授权时RequiresRoles(ROLE_ADMIN)匹配起来语义更清晰。权限则直接用user:add这种通配符字符串。5. 三种拦截方式URL过滤器、注解与标签5.1 URL过滤器配置是权限控制的第一道闸门Shiro在Web应用中最常用的方式是ShiroFilterFactoryBean把URL模式和对应的过滤器链定义清楚。比如一个典型的配置MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/logout, logout); filterChainDefinitionMap.put(/css/**, anon); filterChainDefinitionMap.put(/js/**, anon); filterChainDefinitionMap.put(/user/**, authc, perms[user:view]); filterChainDefinitionMap.put(/admin/**, authc, roles[ROLE_ADMIN]); filterChainDefinitionMap.put(/**, authc);这段配置要理解几个关键词anon匿名可访问不拦截。authc必须登录认证通过才能访问。perms[user:view]登录后还必须有user:view这个权限字符串。roles[ROLE_ADMIN]登录后必须具备指定角色。logout内置的退出过滤器会清除session和RememberMe信息。链式匹配顺序很重要。Shiro按定义顺序从上到下匹配一旦某个规则匹配就停止继续匹配。所以**/这种通配规则一定要放最后否则所有URL都被它拦下来后面的权限规则全都不生效。这个坑我踩过一次当时把/** authc写在了前面结果所有接口都能登录访问但权限校验永远没生效排查了半天才意识到是顺序问题。URL过滤器的优点是集中管理、直观可见适合粗粒度的接口模块级控制。缺点是精确到每一个接口方法时配置会很臃肿。所以实际项目中我习惯用它做“登录验证模块级粗粒度拦截”细粒度的按钮级权限交给注解。5.2 注解式权限控制写在方法上的精准约束Shiro提供了几个注解配合Spring AOP可以在方法级别做权限控制RequiresAuthentication要求当前Subject已认证。RequiresUser要求已认证或者通过RememberMe登录比上面宽松。RequiresRoles(ROLE_ADMIN)要求拥有指定角色。RequiresPermissions(user:add)要求拥有指定权限字符串。在Spring Boot中使用注解式授权需要单独配一个AuthorizationAttributeSourceAdvisor这也是新手容易漏的一步。配完这个Shiro才能代理带注解的Service或者Controller方法。实际使用如下RestController RequestMapping(/user) public class UserController { PostMapping RequiresPermissions(user:add) public Result addUser(RequestBody User user) { // 新增用户逻辑 } DeleteMapping(/{id}) RequiresPermissions(user:delete) public Result deleteUser(PathVariable Long id) { // 删除用户逻辑 } }这样写的好处是权限和接口业务绑定在一起可读性高。而且如果一个方法既有URL级权限又有注解权限两个条件都要满足才能执行形成双重保险。5.3 标签授权JSP/前端页面的按钮级控制Shiro还提供了JSP标签库可以用来在前端控制显示。但在云端分离成为主流后这个场景用得越来越少。如果你还在维护服务端渲染的老项目一个典型用法是shiro:hasPermission nameuser:add button idaddUserBtn新增用户/button /shiro:hasPermission shiro:hasRole nameROLE_ADMIN a href/admin/config管理配置/a /shiro:hasRole这种做法的价值在于未授权用户连页面上的按钮都看不到避免前端传一个请求然后惨遭403。现在前后端分离的项目里前端拿到的权限列表可以来自登录时返回的权限集合或者独立的/user/perms接口然后用路由守卫和按钮级v-if控制思路异曲同工。6. 自定义过滤器和安全配置用代码织起一张保护网6.1 自定义FormAuthenticationFilter处理前后端分离登录用Shiro做传统服务端渲染时默认的FormAuthenticationFilter会在未认证时重定向到登录页面。但前后端分离场景下重定向是致命的——前端拿到的不是JSON而是一个302。这时候需要自定义过滤器重写onAccessDenied等方法让未认证返回统一格式的JSON。我的实现思路是这样继承FormAuthenticationFilter重写isAccessAllowed判断是否登录重写onAccessDenied直接输出401 JSON响应同时支持从JSON请求体读取用户名密码而不是只从request parameter读取。核心代码如下public class JsonAuthFilter extends FormAuthenticationFilter { Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { // 已登录用户直接放行 if (isLoginRequest(request, response)) { return true; } return super.isAccessAllowed(request, response, mappedValue); } Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 前后端分离时返回JSON提示未登录 response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; } }同时登录接口也不再直接调用subject.login()后依赖重定向而是自己捕获异常把成功或失败信息以JSON形式返回给前端由前端决定跳转逻辑。6.2 Session与RememberMe的取舍Shiro默认使用自己的SessionManager。如果应用是单体的、且没有用到分布式session用Shiro的默认session管理问题不大。会话超时时间默认是30分钟Web应用里我一般改为60分钟看业务需要。在Shiro中配置DefaultWebSessionManager的setGlobalSessionTimeout()注意单位是毫秒。RememberMe这是一个双刃剑。开启后用户关闭浏览器再打开只要cookie没过期Shiro会认为用户是“记住的”状态可以跳过登录直接访问部分资源。它的原理是把用户信息序列化后加密写入cookie这个cookie一旦被盗等于账户被盗。我建议只在低敏感系统上开RememberMe金融、后台管理类系统一律默认关闭。配置RememberMe需要一个密钥而且这个密钥要固定不能重启一次换一次否则用户那边的cookie全部失效。我之前就踩过这个坑密钥用随机值生成每次部署重启所有用户都要重新登录。后来改成从配置中心读取一次性解决。6.3 并发登录控制同一账号只允许一个会话在一些内部管理系统里常有“同一账号只允许一处登录”的需求。Shiro没有直接内置这个功能需要结合SessionManager监听器实现。思路是在用户登录成功时查询该用户已有的session如果存在则强制下线或踢出。一个容易实现的方案是继承DefaultWebSessionManager重写getSessionId()时维护用户和sessionId的映射关系登录时检查是否有旧session存在。不过这是侵入性较强的做法。我后来用的一个较为稳妥的方式在登录业务逻辑中自己维护一张在线用户表每次登录前将同一用户的其他记录置为失效同时利用Shiro的session监听器监听Session销毁事件同步清理在线表。这种方式虽然多了一张表但逻辑透明排查问题也方便得多。7. 常见问题梳理认证与授权中的坑我逐个踩给你看7.1 登录失败但没抛任何异常这个情况通常不是Shiro本身的问题而是你catch住了AuthenticationException又没有输出日志或者过滤器链配置里把异常吞掉了。排查技巧在登录方法里分步catchUnknownAccountException、IncorrectCredentialsException、LockedAccountException每种异常分别打印日志并返回不同的提示信息。7.2 密码一直校验不通过九成原因是盐值处理不一致。注册时用的salt是什么认证时从数据库取出来的一定要是同一个值。很多人注册时把salt忘存了或者认证时压根没把salt放进AuthenticationInfoShiro用随机salt去对固定salt的密码做哈希自然永远对不上。另外一个原因是迭代次数不一致配置里的setHashIterations()和注册时的SimpleHash迭代次数必须保持一个值。7.3 doGetAuthorizationInfo 没有被调用这个坑出现频率极高。常见原因有三个URL过滤器链只配了authcSwagger文档上的所有接口都属于正常登录用户可访问的/**范围。此时按需调用subject.hasRole()或subject.isPermitted()才会触发授权查询。要强制每个URL都查权限需要像第5章那样给对应URL加上perms[...]或roles[...]过滤器。注解式授权没有注册AuthorizationAttributeSourceAdvisorBean。配置了缓存但缓存key设计有问题例如principal不稳定导致权限信息虽然在缓存里但查不到被拒绝。7.4 授权缓存导致角色权限变更后不生效Shiro授权缓存默认存在内存中权限更新之后用户要等缓存过期才会生效。如果要做权限即时生效最干净的方式是在角色权限修改接口里调用org.apache.shiro.subject.SimplePrincipalCollection得到该用户对应的principal然后执行subject.getSession().removeAttribute(...)或者直接securityManager.getCacheManager().getCache(authzCacheName).clear()将相关用户的授权缓存清掉。如果系统里用户量大这种精准清理比全局clear要合理得多。7.5 过滤器链顺序错误导致权限形同虚设这是新手最容易忽略的一个问题我在5.1节提过顺序性。更为隐蔽的是配置了多个类似/user/**与/user/edit/**的路径时精确路径必须放在通配路径之前。Shiro的匹配规则不是“最长匹配优先”而是“先定义先匹配”。所以你把/user/**放在/user/edit/**之前后面的权限过滤永远不会生效。7.6 Session失效后前端只拿到重定向HTML这就是6.1节说的前后端分离适配没有被做好的典型症状。未登录状态下Shiro默认的FormAuthenticationFilter会向/login发起重定向浏览器在请求受保护接口时收到的其实是登录页HTML而不是JSON。前端拿到这个HTML去JSON.parse自然报错。解决办法就是自定义过滤器重写onAccessDenied返回JSON。同时前端需要做统一拦截接口返回401就清空登录态并跳转登录页。8. 基于Realm设计的一套完整安全扩展实践如果项目里的权限需求比较单纯默认的Realm写法已经够用。但真实业务中权限模型往往会超出五张表的范围比如要做机构级数据权限、多租户隔离、临时授权到期等。这些需求如果全部塞进doGetAuthorizationInfo方法很快会膨胀成几百行的“上帝方法”。我自己的扩展实践是在Realm层只做“数据聚合”将具体查询逻辑按业务拆分为独立的Provider组件例如Component public class RolePermissionProvider { // 负责查角色编码集合 public SetString findRoles(String userId) { ... } // 负责查权限字符串集合 public SetString findPermissions(String userId) { ... } }doGetAuthorizationInfo只负责调用Provider并组装SimpleAuthorizationInfo各模块的权限细节被隔离后续你接入新的权限模型比如从配置中心同步权限规则时只需要替换Provider实现即可。另外一个容易被忽视的点是权限变更的“实时性”。有些系统对权限时效要求很高比如给某个用户临时开放24小时的数据导出权限到期后权限必须自动收回。我的做法是在Provider中先判断权限是否为“永久授权”再判断时间是否在有效范围内。如果时间过期虽然数据库里还有记录但Provider不会把它加入权限集合。这种做法把时间敏感逻辑收敛在Provider内部不需要额外写定时任务去清理数据库记录省事且可靠。9. 从一次压测暴露出的性能问题说起Shiro在认证授权这块的性能损耗主要来自三个方面查库次数、缓存命中率、Session序列化开销。我之前负责的一个系统上线前压测发现单个请求的平均响应时间有80ms是花在权限查询上的原因就是授权缓存没有开启每个请求都触发了一次角色查询和一次权限查询。开启授权缓存后效果立竿见影// 在SecurityManager的配置中注入缓存管理器 securityManager.setCacheManager(new MemoryConstrainedCacheManager());但内存缓存有个问题多实例部署时每个节点各存各的缓存权限变更后节点间缓存不一致。如果要严格一致就需要引入Redis作为分布式缓存或者使用Shiro的AbstractCacheManager自定义实现将权限缓存放到Redis中。这个取舍要看你系统对权限即时生效的需求强度内部系统可以容忍几分钟的延迟面向用户的C端系统建议尽量实时。Session序列化开销这块主要影响点在Session存储。如果session属性塞了大量自定义对象每次读写session都会有序列化消耗。优化方向有两个一是session里只放必要数据比如用户ID、昵称二是把session序列化策略配置为磁盘存储或者Redis存储而不是默认的内存存储。Clustered环境下正确做法是直接用Shiro的RedisSessionDao替换默认的SessionDAO。10. 最后再分享一个实战设计心得我接手过一套维护了三四年的项目Shiro相关的配置分散在七个类中Realm里塞了各种业务判断Filter里还耦合了特定第三方库的登录逻辑最后梳理时几乎没法动。后来我们做的第一件事不是加功能而是把所有分散的Shiro配置收敛到一个ShiroConfig中把Realm的职责限定在“查用户、查角色、查权限”把登录校验逻辑移入独立的AuthService才真正把Shiro这套东西用明白。回头看自己用Shiro的这些年一个比较明显的感受是Shiro其实不复杂真正复杂的是业务对权限模型的抽象能力。很多项目卡壳不是因为没学会它的API而是没想清楚到底需要什么粒度的权限控制——是只要登录就能用还是需要限制到按钮需不需要数据范围想清楚这些边界再回头看Shiro你会觉得它的每个模块都恰到好处。如果你刚开始在项目里落地Shiro不要一上来就追求大而全的权限体系。先把登录认证、URL拦截、注解授权这三板斧打扎实跑通之后再加缓存、RememberMe、分布式Session。按这个顺序走基本不会在某一步卡死。Shiro的文档虽然老但代码结构非常清晰遇到问题直接看源码比看二手博客靠谱得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑