☰
SpringBoot整合Shiro权限框架实战:从认证授权到常见问题排查
2026/10/14 1:15:39 网站建设 项目流程

1. 为什么在SpringBoot里折腾Shiro

1.1 从一次权限失控的事故说起

前两年我接手过一个后台管理系统,功能不复杂,就是给内部运营团队用的数据看板。当时项目赶得急,登录校验直接用了个拦截器加Session的方案,权限控制基本靠前端按钮隐藏。上线第三周就出事了——一个离职员工的账号还能登录,而且通过直接拼URL的方式访问到了管理员才该看到的财务数据页面。事后复盘,问题就出在权限校验只做在了前端,后端接口几乎是裸奔状态。

那次之后我开始认真找一套能快速集成、配置直观、又足够灵活的权限框架。试过自己手写过滤器链,也看过Spring Security,最后落到Shiro上。原因很实在:Spring Security功能确实强大,但学习曲线陡,配置复杂,对于中小型项目来说有点杀鸡用牛刀的感觉。Shiro的API设计非常直白,Subject、SecurityManager、Realm三个核心概念搞清楚,基本就能跑起来,而且和SpringBoot的整合成本极低。

这篇内容就是把我从零搭建Shiro的完整过程拆开来讲,包括依赖选型、配置思路、认证授权流程、常见坑点。适合正在做后台系统、需要快速加上登录和权限控制的开发者,也适合之前只用过拦截器方案、想升级到专业权限框架的朋友。

1.2 Shiro到底解决了什么问题

用一句话概括:Shiro帮你把"谁在操作"和"他能操作什么"这两件事从业务代码里剥离出来。

没有权限框架的时候,你可能会在每个Controller方法里写一堆if-else判断当前用户角色,代码又臭又长,改一个权限规则要翻遍整个项目。Shiro的做法是:通过过滤器链在请求进入Controller之前就完成校验,业务代码只需要关注业务逻辑本身。它还提供了Session管理、密码加密、记住我等功能,这些都是后台系统的刚需。

和Spring Security对比,Shiro的优势在于概念少、上手快。Spring Security的过滤器链和配置体系对新手来说确实不太友好,而Shiro的核心就三个东西:Subject代表当前用户,SecurityManager是幕后管理者,Realm负责从数据源拿用户和权限信息。理解这三个,剩下的就是配置的事。

注意:Shiro本身不依赖Spring,它是一个独立的安全框架,和SpringBoot整合只是让它更容易融入现有项目。这一点和Spring Security有本质区别。

2. 环境准备与依赖选型

2.1 版本选择的门道

搭Shiro第一步就是选版本。这里有个很多人踩过的坑:Shiro的版本和SpringBoot的版本之间存在兼容性问题。我实测下来比较稳的组合是SpringBoot 2.7.x配Shiro 1.11.0以上,或者SpringBoot 3.x配Shiro 2.0.x。如果你用的是SpringBoot 3,一定要注意Shiro 1.x是不支持的,因为SpringBoot 3把javax.servlet换成了jakarta.servlet,Shiro 1.x还在用旧的包名,直接整合会报ClassNotFoundException。

我自己的项目用的是SpringBoot 2.7.18加Shiro 1.13.0,这个组合经过多个项目验证,稳定性没问题。如果你是新项目,建议直接上SpringBoot 3加Shiro 2.0,虽然Shiro 2.0的资料相对少一些,但长远来看是趋势。

依赖方面,核心就两个:

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

用starter的好处是它自动帮你装配了ShiroFilterFactoryBean和SecurityManager,省去了大量手动配置。如果你不用starter,就得自己写@Bean来创建这些对象,代码量会多不少。

2.2 项目结构规划

在动手写代码之前,我习惯先把包结构规划好。Shiro相关的类不要散落在各个包里面,集中管理后期维护会轻松很多。我的做法是建一个config包放Shiro的配置类,一个shiro包放自定义Realm和过滤器,一个entity包放用户和角色实体。

数据库表设计也得提前想清楚。最基础的是三张表:用户表、角色表、权限表,再加上两张关联表用户角色关联和角色权限关联。如果你觉得五张表太重,也可以简化成用户表里直接存角色字段,权限用角色来映射。但我不建议这么做,后期扩展会很痛苦。权限控制这件事,表结构设计得越规范,后面加需求越轻松。

3. Shiro核心概念拆解

3.1 Subject:站在用户视角看一切

Subject是Shiro对外暴露的门面。你在代码里调用SecurityUtils.getSubject()拿到的就是当前请求对应的Subject对象。它封装了当前用户的所有信息,包括是否已认证、拥有哪些角色、具备哪些权限。

打个比方,Subject就像是你去公司前台办事时拿到的那张访客卡。卡片上记录了你是谁、你能去哪些楼层、你能进哪些会议室。每次你要去某个区域,保安看的就是这张卡。Shiro里的Subject也是这个角色,它不关心你背后是怎么登录的、数据从哪来的,它只负责回答"当前这个人能不能做这件事"。

Subject的几个核心方法必须记住:login()用于登录认证,logout()退出,isAuthenticated()判断是否已登录,hasRole()和isPermitted()用于权限判断。日常开发中,你用得最多的就是这几个。

3.2 SecurityManager:幕后的总调度

SecurityManager是Shiro的心脏,所有认证、授权、Session管理都要经过它。但在实际开发中,你几乎不会直接和它打交道,因为Subject已经帮你把大部分操作封装好了。你只需要在配置类里把它创建出来,注入Realm就行。

可以把它理解成公司安保系统的总控室。Subject是前台那张卡,但卡能不能刷开某扇门,最终是总控室在背后做判断。总控室需要知道每个员工的权限信息,这些信息就是从Realm来的。

3.3 Realm:连接数据源的桥梁

Realm是Shiro和你的数据库之间的桥梁。Shiro本身不知道你的用户存在哪、密码是什么、权限怎么分配,这些都需要你通过Realm告诉它。你需要继承AuthorizingRealm类,实现两个核心方法:doGetAuthenticationInfo()负责认证,doGetAuthorizationInfo()负责授权。

我见过很多新手在这里犯迷糊,把认证和授权混在一起写。记住一个原则:认证解决的是"你是谁"的问题,授权解决的是"你能干什么"的问题。登录的时候走认证,访问受保护资源的时候走授权,两条线是分开的。

4. 完整搭建流程实操

4.1 配置文件与基础参数

先在application.yml里把Shiro的基础配置写好:

shiro: enabled: true web: enabled: true loginUrl: /login successUrl: /index unauthorizedUrl: /unauthorized

loginUrl是未登录时跳转的地址,unauthorizedUrl是已登录但权限不足时跳转的地址。这两个配置很关键,不配的话Shiro会用默认值,可能导致跳转行为不符合预期。

还有一个容易忽略的点:Shiro默认使用自己的Session管理,如果你项目里已经用了Spring Session或者Redis做分布式Session,需要额外配置SessionManager,否则会出现Session不一致的问题。这个坑我在分布式部署的时候踩过,本地测试一切正常,上了集群就频繁掉登录。

4.2 自定义Realm实现认证逻辑

下面是我项目中实际使用的Realm实现,做了简化处理:

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) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("账号不存在"); } if (user.getStatus() == 0) { throw new LockedAccountException("账号已被锁定"); } return new SimpleAuthenticationInfo( username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName() ); } }

这里有几个细节值得展开说。第一,密码不要明文存储,我用的是加盐哈希的方式,salt存在用户表里,每次认证时传给Shiro做比对。第二,认证失败要抛具体的异常类型,UnknownAccountException和LockedAccountException会被Shiro捕获并转换成对应的错误信息,前端可以根据不同异常给出不同提示。第三,doGetAuthorizationInfo里的权限查询要做好缓存,不然每次请求都查数据库,性能扛不住。

4.3 ShiroConfig配置类编写

配置类是整个整合的核心,我把它拆成几个@Bean来写:

@Configuration public class ShiroConfig { @Bean public CustomRealm customRealm() { CustomRealm realm = new CustomRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); return realm; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("MD5"); matcher.setHashIterations(1024); return matcher; } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl("/login"); factoryBean.setUnauthorizedUrl("/unauthorized"); Map<String, String> filterChain = new LinkedHashMap<>(); filterChain.put("/login", "anon"); filterChain.put("/register", "anon"); filterChain.put("/static/**", "anon"); filterChain.put("/admin/**", "roles[admin]"); filterChain.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChain); return factoryBean; } }

filterChain的顺序非常重要,必须用LinkedHashMap保证有序,因为Shiro是按顺序匹配的,第一个匹配上的规则就生效。anon表示匿名访问,authc表示需要认证,roles[admin]表示需要admin角色。我一般把静态资源和登录注册放最前面,然后是特定角色的路径,最后用/**兜底。

注意:filterChain.put("/**", "authc")这行必须放在最后,否则后面的规则永远不会被匹配到。这个坑我见过太多人踩了。

4.4 登录接口与密码加密

登录接口的写法很直接:

@PostMapping("/login") public Result login(String username, String password) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); return Result.success("登录成功"); } catch (UnknownAccountException e) { return Result.error("账号不存在"); } catch (IncorrectCredentialsException e) { return Result.error("密码错误"); } catch (LockedAccountException e) { return Result.error("账号已锁定"); } }

密码加密这块,我建议在用户注册的时候就做好。用Shiro的SimpleHash工具类:

public static String encryptPassword(String password, String salt) { return new SimpleHash("MD5", password, ByteSource.Util.bytes(salt), 1024).toHex(); }

salt建议用随机生成的字符串,每个用户不一样。这样即使两个用户密码相同,存到数据库里的哈希值也不同,能有效防止彩虹表攻击。哈希迭代次数设1024次是常见做法,次数越高越安全但计算越慢,1024是一个平衡点。

5. 权限控制实战技巧

5.1 注解式权限控制

除了在filterChain里配置URL级别的权限,Shiro还支持注解级别的细粒度控制。在Controller方法上加@RequiresPermissions或@RequiresRoles:

@RequiresPermissions("user:delete") @DeleteMapping("/user/{id}") public Result deleteUser(@PathVariable Long id) { userService.deleteById(id); return Result.success(); }

要让注解生效,必须在ShiroConfig里开启AOP支持,加上这两个Bean:

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

不加这两个Bean,注解就是摆设,一点效果都没有。这是新手最容易忽略的一步。

5.2 权限缓存优化

每次请求都去数据库查权限,QPS一上来数据库就扛不住了。我的做法是用Redis做权限缓存,在doGetAuthorizationInfo里先查缓存,缓存没有再查数据库并写入缓存。缓存的key用用户名,value是角色和权限的集合,设置一个合理的过期时间,比如30分钟。

用户权限变更的时候要记得清缓存,不然会出现改了权限但不生效的情况。我一般是在角色分配、权限修改的接口里主动删除对应的缓存key。这个逻辑虽然简单,但漏掉的话排查起来很费时间,因为现象是"有时候生效有时候不生效",很容易误判成偶发bug。

5.3 前后端分离下的Session处理

现在很多项目是前后端分离的,前端用Vue或React,后端只提供接口。这种情况下传统的Session-Cookie模式就不太适用了,因为跨域请求默认不带Cookie。解决方案有两种:一种是用Token替代Session,另一种是配置CORS允许携带Cookie。

我倾向于用Token方案,具体做法是登录成功后生成一个JWT返回给前端,前端每次请求在Header里带上这个Token,后端写一个自定义过滤器解析Token并构建Subject。这样Shiro的Session管理就不参与了,整个认证流程更加轻量。但要注意,用Token方案的话,Shiro的logout和Session相关功能需要自己实现,不能直接用框架提供的。

6. 常见问题与排查实录

6.1 登录后仍然跳转登录页

这是最高频的问题。现象是明明登录成功了,访问其他页面还是被重定向到loginUrl。排查思路如下:

排查项可能原因解决方法
filterChain配置/**规则放在了具体规则前面调整顺序,具体规则在前
Session共享分布式环境下Session不一致配置RedisSessionManager
跨域问题前后端分离Cookie未携带配置CORS或改用Token
缓存问题权限缓存未更新清除缓存或缩短过期时间

我遇到最多的是filterChain顺序问题。很多人写配置的时候习惯把通配符放前面,觉得这样"覆盖面广",结果导致后面的精细规则全部失效。

6.2 密码校验一直失败

密码校验失败但确认密码没输错,大概率是加密方式不匹配。检查三个地方:注册时用的哈希算法和Realm里配置的是否一致,salt是否传对了,迭代次数是否相同。这三个参数任何一个不一致,校验都会失败。

还有一种情况是数据库里存的密码是明文,但Realm配置了HashedCredentialsMatcher,这样Shiro会拿密文去比对明文,自然对不上。要么把数据库里的密码改成加密后的,要么把CredentialsMatcher换成SimpleCredentialsMatcher。我建议前者,安全性更高。

6.3 注解权限不生效

前面提到过,注解不生效九成是因为没有配置AuthorizationAttributeSourceAdvisor和DefaultAdvisorAutoProxyCreator。另外还要确认@EnableAspectJAutoProxy是否开启,SpringBoot默认是开启的,但如果你手动关掉了就要加回来。

还有一个隐蔽的问题:如果Controller方法是被内部调用的(比如A方法调用B方法),B方法上的注解不会生效,因为AOP代理只对外部调用起作用。这个属于Spring AOP的通用限制,不只是Shiro的问题。

6.4 退出登录后Session未清除

调用subject.logout()之后,Shiro会清除Session并重定向到配置的地址。但如果你用的是自定义SessionManager或者Token方案,logout可能不会按预期工作。我的做法是在logout接口里手动清除Token或Session,不依赖Shiro的默认行为,这样更可控。

7. 一些实操心得与扩展思路

7.1 关于Realm的拆分

项目初期我把认证和授权都写在一个Realm里,后来用户体系变复杂了,出现了多种登录方式(账号密码、手机验证码、第三方授权),一个Realm就扛不住了。这时候可以拆成多个Realm,每个Realm负责一种认证方式,通过ModularRealmAuthenticator来协调。Shiro支持配置多个Realm,认证时会依次尝试,直到有一个成功。

7.2 关于权限粒度的把握

权限设计不要太细也不要太粗。太细的话,一个页面要配十几个权限码,维护成本极高;太粗的话,又起不到控制作用。我的经验是按"模块:操作"的格式来定义,比如user:view、user:edit、user:delete,一个模块三到五个权限码就够了。角色和权限的映射关系在后台管理界面里配置,不要硬编码在代码里。

7.3 关于Shiro的替代方案

如果你的项目权限逻辑特别复杂,比如需要支持数据级别的权限控制(不同用户看到不同的数据行),Shiro可能就不太够用了,需要考虑更专业的权限系统或者自研。但对于大多数中小型后台系统来说,Shiro的URL级别加注解级别控制已经能覆盖90%的场景。

我在实际使用中发现,Shiro最大的价值不是它提供了多少功能,而是它把权限控制这件事标准化了。团队里任何人接手项目,看到Shiro的配置就知道权限是怎么控制的,不需要去翻业务代码找拦截器。这种标准化带来的维护效率提升,比功能本身更重要。

最后分享一个小技巧:在开发阶段可以把Shiro的日志级别调到DEBUG,这样每次请求的认证授权过程都会打印出来,排查问题非常方便。上线前记得调回INFO,不然日志量会很大。

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

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

立即咨询