☰
SpringBoot整合Shiro权限框架实战:核心配置与避坑指南
2026/10/14 4:48:49 网站建设 项目流程

1. 为什么权限框架选型时绕不开Shiro

做过后端项目的人都有一个共识:用户认证和授权这块代码,写起来不难,但写好、写稳、写得能扩展,非常费劲。很多团队在项目初期觉得"不就是个登录拦截吗",随手写个拦截器加Session判断就上线了,结果后面要加角色、加权限、加记住我、加多端登录控制的时候,发现原来的代码已经烂成一团,改一处崩三处。

Shiro这个框架,在权限管理领域算是老资历了。它的核心定位很清晰:帮你把认证(Authentication)、授权(Authorization)、会话管理(Session Management)、加密(Cryptography)这四件事统一管起来。和Spring Security相比,Shiro的学习曲线明显更平缓,配置更直观,对于中小型项目或者需要快速搭建权限体系的场景来说,性价比很高。

SpringBoot搭Shiro这个组合,本质上解决的是"如何用最少的配置量,让一个SpringBoot应用具备完整的权限控制能力"这个问题。适合谁看?如果你已经会写SpringBoot的Controller和Service,但对权限框架还停留在"听说过"的阶段,或者你之前用过Shiro但每次配置都要翻旧代码,那这篇内容就是给你准备的。

我自己的经验是,Shiro的入门门槛低,但它的坑集中在几个特定位置:过滤器链的配置顺序、Realm的注入方式、Session和缓存的关系、以及前后端分离场景下的适配。这些坑官方文档不会重点提醒你,但实际项目里踩一次就够记一辈子。下面我把整个搭建过程拆开讲,重点放在"为什么这么配"和"哪里容易出错"上。

2. 依赖引入与版本选择的那些门道

2.1 shiro-spring-boot-web-starter和原生shiro-spring的区别

很多人第一次搭Shiro的时候,在网上搜到的教程用的是shiro-spring这个依赖,然后自己手动配置一堆Bean。但如果你用的是SpringBoot,更推荐直接用shiro-spring-boot-web-starter。这两个的区别在于:前者需要你自己声明ShiroFilterFactoryBean、DefaultWebSecurityManager、Realm等Bean,后者通过自动配置帮你把大部分基础Bean都注册好了,你只需要关注Realm的实现和少量定制配置。

不过这里有个现实问题:shiro-spring-boot-web-starter的版本更新节奏比较慢,和最新版SpringBoot之间偶尔会有兼容性摩擦。我实测下来,SpringBoot 2.7.x配合Shiro 1.13.0是比较稳的组合。如果你用的是SpringBoot 3.x,需要注意Shiro对Jakarta EE命名空间的支持情况,部分版本可能还需要手动排除一些传递依赖。

<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.13.0</version> </dependency>

注意:如果你在依赖树里同时看到了shiro-core、shiro-web、shiro-spring多个版本,大概率是传递依赖冲突了。用mvn dependency:tree查一下,把多余的排除掉,否则会出现类加载顺序不确定导致的诡异问题。

2.2 版本兼容性检查清单

在正式写代码之前,花五分钟确认下面这几项,能帮你省掉后面几个小时的排查时间:

  • SpringBoot版本与Shiro版本的对应关系,建议查一下Shiro官方发布的兼容性说明
  • 如果项目里用了Redis做Session共享,确认shiro-redis插件的版本是否匹配
  • 如果用了JWT做无状态认证,Shiro的Session管理需要做相应调整,不能直接用默认的DefaultWebSessionManager
  • 模板引擎如果是Thymeleaf,需要额外引入shiro-thymeleaf方言包,否则页面上的Shiro标签不生效

这些检查看起来琐碎,但每一条背后都是真实踩过的坑。我见过一个项目因为没检查Thymeleaf方言包,前端页面上的权限控制标签全部原样输出,用户直接看到了<shiro:hasRole>这样的源码,虽然不涉及安全问题,但体验很差。

3. 核心配置类的拆解与Bean装配逻辑

3.1 ShiroConfig的三块核心内容

一个典型的Shiro配置类,不管怎么简化,都绕不开三块内容:Realm的创建、SecurityManager的装配、FilterChainDefinitionMap的定义。这三者的关系可以这样理解:Realm是"数据源",负责告诉Shiro用户的身份和权限信息从哪里来;SecurityManager是"总管家",把Realm和其他组件串起来;FilterChainDefinitionMap是"规则表",定义哪些URL需要什么级别的访问控制。

@Configuration public class ShiroConfig { @Bean public Realm customRealm() { CustomRealm realm = new CustomRealm(); // 这里可以设置凭证匹配器 realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } @Bean public DefaultWebSecurityManager securityManager(Realm customRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(customRealm); // 如果用了Redis做Session共享,这里需要替换SessionManager return manager; } @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 设置登录页、成功页等 factoryBean.setLoginUrl("/login"); factoryBean.setUnauthorizedUrl("/unauthorized"); // 定义过滤器链 Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login", "anon"); filterMap.put("/static/**", "anon"); filterMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterMap); return factoryBean; } }

这段代码看起来简单,但有几个细节值得展开说。

3.2 为什么filterChainDefinitionMap必须用LinkedHashMap

这是一个非常经典的坑。Shiro在匹配URL时,是按照Map的迭代顺序从上往下匹配的,一旦匹配到就停止。如果你用的是普通的HashMap,它的迭代顺序是不确定的,可能导致/login被/**拦截,或者静态资源被要求登录。用LinkedHashMap保证插入顺序,才能让"先具体后宽泛"的规则生效。

提示:规则的定义顺序应该是:先放行匿名访问的路径(如登录页、静态资源、验证码接口),再放行需要特定角色的路径,最后用/**兜底。顺序反了,整个权限体系就形同虚设。

3.3 自定义Realm的doGetAuthorizationInfo到底怎么写

Realm是整个Shiro的数据核心。它有两个必须实现的方法:doGetAuthenticationInfo负责认证(验证用户名密码),doGetAuthorizationInfo负责授权(返回用户的角色和权限)。

public class CustomRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 从数据库或缓存中获取角色和权限 Set<String> roles = userService.getRolesByUsername(username); Set<String> permissions = userService.getPermissionsByUsername(username); info.setRoles(roles); info.setStringPermissions(permissions); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); } }

这里有一个性能相关的经验:doGetAuthorizationInfo在每次需要鉴权时都会被调用。如果你的权限数据是从数据库查的,而且没有加缓存,那在高频接口上会出现大量重复查询。我的做法是在这一层加一个本地缓存或者Redis缓存,key用用户名,value是角色和权限集合,设置一个合理的过期时间。这样既保证了权限变更后能及时生效,又避免了每次请求都打数据库。

4. 认证流程的完整链路与常见断点

4.1 从登录请求到Subject.login的每一步

Shiro的认证流程,表面上看就是一行subject.login(token),但背后经历了好几个步骤。理解这个链路,对排查登录失败问题非常关键。

第一步,你的Controller接收到登录请求,构造UsernamePasswordToken,然后通过SecurityUtils.getSubject()拿到当前Subject,调用login方法。第二步,Subject会把认证请求委托给SecurityManager。第三步,SecurityManager遍历所有配置的Realm,找到支持该Token类型的Realm。第四步,调用Realm的doGetAuthenticationInfo方法获取认证信息。第五步,用CredentialsMatcher对Token中的密码和Realm返回的密码进行比对。第六步,比对成功则认证通过,把Principal信息绑定到Subject上;失败则抛出相应的异常。

这个链路里,最容易出问题的是第四步和第五步。第四步的典型问题是Realm没有被Spring管理,导致里面注入的Service是null。第五步的典型问题是密码加密方式不匹配,比如数据库存的是MD5加盐,但CredentialsMatcher配置的是明文比对。

4.2 凭证匹配器的配置细节

@Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("MD5"); matcher.setHashIterations(1024); matcher.setStoredCredentialsHexEncoded(true); return matcher; }

这段配置里,hashIterations的值必须和注册用户时加密用的次数一致。我见过有项目注册时用了1024次,但匹配器里写的是1次,结果所有用户都登录失败,排查了半天才发现是次数不匹配。另外,storedCredentialsHexEncoded如果设为true,数据库里存的密码必须是十六进制字符串,如果是Base64编码的,这个值要设为false。

注意:如果你在Realm中返回的SimpleAuthenticationInfo带了salt(盐值),那CredentialsMatcher会自动用这个salt去加密Token中的密码再比对。如果salt传错了或者没传,同样会导致认证失败。

4.3 登录失败异常的处理策略

Shiro在认证失败时会抛出多种异常,常见的有:UnknownAccountException(用户不存在)、IncorrectCredentialsException(密码错误)、LockedAccountException(账户锁定)、ExcessiveAttemptsException(尝试次数过多)。这些异常如果不做处理,直接抛到前端,用户体验很差,而且会暴露系统信息。

我的做法是在Controller层统一捕获AuthenticationException,然后根据具体类型返回友好的提示。但这里有个安全细节:对于"用户不存在"和"密码错误",最好返回同样的提示文案,比如"用户名或密码错误",避免攻击者通过错误信息枚举出系统中存在哪些用户名。

5. 授权控制的三种粒度和落地方式

5.1 URL级别的过滤器链控制

这是最粗粒度的控制,在ShiroFilterFactoryBean的filterChainDefinitionMap里定义。常用的过滤器有:

过滤器名称作用适用场景
anon匿名访问,不做任何拦截登录页、静态资源、公开接口
authc需要认证才能访问大部分业务接口
authcBasicHTTP Basic认证内部服务间调用
roles需要指定角色管理后台入口
perms需要指定权限具体操作按钮对应的接口
logout注销当前用户退出登录接口
user需要"记住我"或已认证允许记住我用户访问的页面

配置示例:

filterMap.put("/admin/**", "roles[admin]"); filterMap.put("/user/delete", "perms[user:delete]"); filterMap.put("/logout", "logout");

5.2 方法级别的注解控制

URL级别的控制有个局限:它只能控制到接口路径,没法控制到Service方法。如果你的权限逻辑需要在Service层做判断,就需要开启注解支持。

@Configuration public class ShiroAnnotationConfig { @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } }

配置好之后,就可以在Service方法上使用@RequiresRoles、@RequiresPermissions等注解了。这里有个坑:DefaultAdvisorAutoProxyCreator的setProxyTargetClass(true)如果不设置,在某些情况下会导致注解不生效,因为Shiro默认用的是JDK动态代理,而你的Service可能没有实现接口。

5.3 页面标签级别的控制

如果前端用的是JSP或Thymeleaf,Shiro提供了标签库来做页面元素的权限控制。以Thymeleaf为例,引入方言包之后可以这样写:

<div shiro:hasPermission="user:delete"> <button>删除用户</button> </div>

这样没有user:delete权限的用户,页面上根本看不到删除按钮。但必须强调:页面标签控制只是提升用户体验,真正的安全边界必须在后端接口上。我见过有项目只做了页面标签控制,后端接口没有任何权限校验,结果用户直接调接口就能越权操作。

6. 前后端分离场景下的适配与改造

6.1 默认Session模式为什么不适用

Shiro默认使用Session来保存认证状态,这在传统的服务端渲染项目里没问题。但在前后端分离的场景下,前端可能是移动端App、小程序或者SPA,这些客户端不一定支持Cookie,或者跨域场景下Cookie的传递会有各种限制。这时候就需要把Shiro的认证状态从Session迁移到Token上。

6.2 无状态Token方案的实现思路

核心思路是:自定义一个Filter,替代默认的authc过滤器。这个Filter从请求头中读取Token,验证Token的有效性,然后构造一个已认证的Subject绑定到当前线程。

public class JwtFilter extends AuthenticatingFilter { @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { return null; } return new JwtToken(token); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { // 返回未登录的JSON响应 return false; } return executeLogin(request, response); } private String getRequestToken(HttpServletRequest request) { return request.getHeader("Authorization"); } }

然后在ShiroConfig中注册这个Filter,并把authc替换成自定义的Filter名称。同时,SecurityManager的SessionManager需要替换成一个不创建Session的实现,避免Shiro在无状态场景下仍然尝试写Session。

6.3 跨域场景下的几个注意事项

前后端分离必然涉及跨域。Shiro的过滤器链是在CORS过滤器之后执行的,所以CORS的配置要放在Shiro之前。另外,对于OPTIONS预检请求,需要设置为anon放行,否则预检请求会被Shiro拦截,导致跨域失败。

filterMap.put("/**", "anon"); // 临时放行OPTIONS,实际项目中应精确配置

更精确的做法是自定义一个CorsFilter,在ShiroFilter之前注册,并在CorsFilter中处理OPTIONS请求。

7. 缓存与Session共享的实战配置

7.1 为什么需要缓存

Shiro的授权信息查询在每次鉴权时都会触发。如果每次都要查数据库,接口的响应时间会明显增加。引入缓存之后,第一次查询走数据库,后续从缓存读取,性能提升非常明显。

Shiro的缓存抽象是CacheManager,你可以用EhCache做本地缓存,也可以用Redis做分布式缓存。对于单机应用,EhCache够用;对于多实例部署,必须用Redis,否则会出现"在A实例登录,请求打到B实例时未认证"的问题。

7.2 Redis缓存管理器的配置要点

@Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) { RedisCacheManager cacheManager = new RedisCacheManager(); cacheManager.setRedisManager(redisManager(factory)); // 设置缓存key的前缀,避免和其他业务key冲突 cacheManager.setKeyPrefix("shiro:cache:"); return cacheManager; }

配置Redis缓存管理器时,有几个参数需要根据实际情况调整:过期时间、key前缀、序列化方式。特别是序列化方式,如果用的是默认的JDK序列化,缓存里的内容在Redis客户端里看是一堆乱码,排查问题很不方便。建议改成JSON序列化。

7.3 Session共享的配置

Session共享的配置和缓存类似,核心是把DefaultWebSessionManager替换成RedisWebSessionManager,并设置Redis连接信息。配置好之后,多个应用实例共享同一个Redis,Session数据自然就同步了。

提示:Session共享配置完成后,一定要做多实例的登录测试。我见过有项目配置了Redis Session,但忘记把SessionManager设置到SecurityManager上,结果还是用的默认Session,多实例部署时登录状态时有时无。

8. 踩坑排查的完整链路记录

8.1 登录后立即跳回登录页

这是最常见的问题。排查链路是这样的:先看浏览器控制台,确认登录请求是否返回了正确的Set-Cookie;再看服务端日志,确认认证是否通过;如果认证通过但后续请求仍然被拦截,检查SessionManager的配置,看Session是否被正确创建和读取;最后检查过滤器链的顺序,确认/login是否被/**提前拦截了。

我遇到过一次比较隐蔽的情况:登录接口返回了成功,但Set-Cookie的Path属性设置成了/login,导致后续请求根本带不上这个Cookie。这个问题出在SessionManager的setSessionIdCookiePath配置上,默认值是/,但有人手动改成了其他值。

8.2 权限注解不生效

@RequiresPermissions注解不生效,通常有三个原因:一是没有配置AuthorizationAttributeSourceAdvisor;二是DefaultAdvisorAutoProxyCreator的代理模式不对;三是注解加在了Controller的private方法或者非Spring管理的类上。排查时可以先在注解方法里打一个断点,看请求是否进入了这个方法,如果进入了但没触发鉴权,那就是AOP代理的问题。

8.3 静态资源被拦截

静态资源被Shiro拦截,表现为页面样式丢失、JS加载失败。原因通常是过滤器链里没有放行静态资源路径。解决方法是把/static/**、/css/**、/js/**、/images/**等路径配置为anon。但要注意,SpringBoot的静态资源默认路径和你的项目结构有关,配置之前先确认实际的资源路径。

8.4 自定义Filter不生效

自定义Filter注册到Shiro之后不生效,最常见的原因是Filter的名称和filterChainDefinitionMap里引用的名称不一致。Shiro是通过名称来查找Filter的,名称对不上就不会执行。另外,自定义Filter如果是Spring Bean,需要确保它被正确注册到ShiroFilterFactoryBean的filters属性中。

9. 一些让配置更稳的个人习惯

我在多个项目里搭Shiro之后,慢慢形成了一些固定的习惯,分享出来供参考。

第一,永远把filterChainDefinitionMap的配置单独抽成一个方法,并且用LinkedHashMap。这样规则顺序一目了然,后续加规则也不容易插错位置。

第二,Realm里的doGetAuthorizationInfo一定要加缓存,而且缓存的key要包含用户名和租户标识(如果有的话)。我见过多租户项目里缓存key只用用户名,导致A租户的用户拿到了B租户的权限。

第三,登录失败异常统一处理,对外只返回模糊的提示信息,详细的失败原因只记日志。这既是安全考虑,也是用户体验考虑。

第四,每次修改Shiro配置之后,至少做三轮测试:未登录访问受保护接口、已登录但无权限访问、已登录且有权限访问。这三轮覆盖了大部分配置错误。

第五,如果项目后期要接入单点登录或者OAuth2,Shiro的Realm层是可以复用的,但Filter层需要做较大调整。所以在初期设计时,尽量把认证逻辑和授权逻辑分离清楚,方便后续替换认证方式。

这些习惯看起来都是小事,但每一个背后都有对应的踩坑经历。Shiro这个框架本身不复杂,复杂的是各种场景下的适配和边界情况。把上面这些内容吃透,搭建一个稳定可用的权限体系基本没什么问题。后续如果遇到更复杂的场景,比如动态权限、数据权限、多端登录互斥等,可以在当前基础上逐步扩展,不用推翻重来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询