首页 / 资讯中心 / 文章详情

SpringBoot集成Shiro与Redis:从认证授权到会话共享的完整实践

SpringBoot集成Shiro与Redis:从认证授权到会话共享的完整实践 ★ FEATURED ARTICLE
1. 从登录到授权Shiro 帮你拆解的这条链路做 Web 开发的人都会经历这么一个阶段最开始写接口只考虑业务逻辑等到用户体系一接入马上发现登录、退出、权限控制、会话保持这一堆横切逻辑远比想象中复杂。我曾经接过一个内部管理系统的需求需求文档里只写了两个大字权限。结果前后折腾了两周大部分时间不是花在业务上而是花在怎么让登录状态安全可靠地保持住怎么让不同角色看到不同菜单、调用不同接口这些基础问题上。SpringBoot 生态里提到权限方案大家第一反应多半是 Spring Security。但我个人更偏爱 Apache Shiro原因很直接Shiro 的学习曲线平滑API 设计直观核心概念掰着手指头能数清。它把权限问题抽象成一套极其清晰的模型而不是像 Spring Security 那样每次配个过滤器链都要翻阅一堆文档。如果你了解 Shiro一定知道这三个核心概念Subject当前操作的用户或者更准确地说当前这段代码的执行主体。SecurityManager整个权限体系的调度中心负责协调认证、授权、会话管理。Realm数据访问桥接层Shiro 不关心用户数据存在数据库还是 LDAP它只认 Realm 返回的结果。认证和授权的整体流程大致是这样的用户提交账号密码后调用Subject.login()SecurityManager 接手后去触发 Realm 的认证方法Realm 从库里查出用户信息Shiro 负责比对密码认证通过后把登录状态写入会话授权则是在每次访问受限接口时由拦截器检查当前 Subject 是否拥有对应角色或权限没有就抛出异常。这套模型最大的优点是边界清晰认证、授权、会话管理各管各的互不干扰。但落到工程层面光靠 Shiro 默认的本地内存 Session 是不够的——一旦应用启动了多个实例Session 就不同步了权限数据每次都查数据库压力也不小。这时候就需要引入 Redis。Redis 在这里要干三件事存 Session、缓存授权数据、解决多实例下的会话共享。标题里的三个组件分工就是这么来的。1.1 认证、授权、会话管理三者其实可以分开看很多人把认证和授权混为一谈觉得登进去之后权限就一起搞定了。实际上这是两个完全独立的阶段。认证是验明正身你是谁证明方式通常是账号密码、验证码、证书、令牌等。Shiro 的认证结果是生成一个 Subject 主体并且把这个主体的登录状态记录到 Session 中。认证只做一次通常发生在登录那一刻。授权是确认资格你是谁已经知道了但你有没有权限执行某个操作比如普通员工不能删除订单管理员可以财务角色能看到薪资报表普通开发看不到。授权通常发生在每次请求访问受保护资源时Shiro 会基于当前 Subject 的身份去加载其角色和权限集合然后做匹配。会话管理则是把认证后的状态跨请求保持住。HTTP 是无状态的如果每次请求都要重新登录一次体验会极其糟糕。Shiro 默认基于 Servlet 容器的 Session结合 Cookie 中的 Session ID 来识别这个人之前登录过。只有把这三件事拆开理解后面配置 Shiro 时才不会晕。比如有的项目只要登录不要求权限那授权相关的代码完全可以不写只配置认证即可有的项目要求接口级权限那 URL 过滤规则和权限注解就必不可少。1.2 内存 Session 的痛点逼我把 Redis 拉进来Shiro 开箱即用的 Session 管理是基于内存的Session 对象直接存在当前 JVM 堆里。单机开发测试没问题一旦部署到多台服务器就会出现一个非常经典的问题——用户在 A 服务器登录了下一次请求被负载均衡转发到 B 服务器B 服务器的内存里根本没有这个 Session于是用户被强制重新登录。解决思路无非两种一种是做 Session 粘滞让同一个用户的请求始终落在同一台服务器上但这样做负载均衡的效果会被削弱而且服务器重启即丢失另一种就是把 Session 数据抽取出来放到一个所有实例都能访问的公共存储里这就是 Redis 的用武之地。Redis 在这里解决的不只是多实例会话共享一个问题。权限数据的缓存同样重要用户的角色和权限查询往往涉及多表关联低频变化但高频读取适合缓存。Shiro 提供 AuthorizingCache配合 Redis 可以把权限集合缓存起来避免一次接口调用触发多次 SQL 查询。这两点叠加Redis 就从一个可选项变成了必选项。2. 项目搭建从依赖到配置类的步步拆解这一节我们直接进入实操。我会拿一个模拟项目X来演示这个项目X是一个典型的管理后台包含用户管理、角色管理这两个基础模块目标是把 Shiro 的认证和授权完整跑通。项目X的技术栈是 SpringBootWeb 环境、Shiro、Redis、MyBatis-Plus数据库用 MySQL。先说搭建环境的基本前提。JDK 建议 8 以上SpringBoot 版本我用的 2.6.xShiro 版本 1.8.0Redis 客户端使用 RedisTemplate 默认的 Lettuce。这个版本组合我实测是比较稳的如果你的 SpringBoot 版本更高比如 2.7 或者 3.x可能需要稍微调整 Shiro 的依赖坐标这个后面专门说。2.1 依赖引入的版本匹配最容易在这里翻车Maven 依赖里需要加入以下内容最核心的是 shiro-spring 和 shiro-ehcache 中的某几个模块但注意这里不要引入 shiro-ehcache因为我们走的是 Redis 路线。dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring/artifactId version1.8.0/version /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-core/artifactId version1.8.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version1.2.83/version /dependency为什么要分两个 org.apache.shiro 的依赖单纯引入 shiro-spring 其实在 SpringBoot 集成时也够用但很多老的 Shiro 工具类比如org.apache.shiro.util.Assert、org.apache.shiro.codec.Base64位于 shiro-core 里为了保险我会一起引进来。这里最需要注意的是版本冲突。Shiro 1.8.0 依赖的commons-beanutils版本较老如果你的项目里有其他组件间接引用了更新的 commons-beanutils可能出现NoSuchMethodError。我的建议是遇到这类问题先看依赖树mvn dependency:tree找到冲突后排除掉保留 Shiro 需要的版本即可。2.2 Shiro 配置类的三个核心 BeanSpringBoot 整合 Shiro 的核心是写一个配置类向容器中注册三个 BeanRealm、SecurityManager、ShiroFilterFactoryBean。下面这个配置类是我常用的模板注释里写了每个 Bean 的作用Configuration public class ShiroConfig { Bean public UserRealm userRealm() { return new UserRealm(); } Bean public SecurityManager securityManager(UserRealm userRealm, RedisSessionDAO redisSessionDAO, RedisCacheManager redisCacheManager) { DefaultWebSecurityManager manager new DefaultWebSecurityManager(); manager.setRealm(userRealm); manager.setSessionManager(sessionManager(redisSessionDAO)); manager.setCacheManager(redisCacheManager); return manager; } Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); // 认证失败后跳转的页面 bean.setLoginUrl(/login); // 认证通过但授权失败跳转的页面 bean.setUnauthorizedUrl(/unauthorized); MapString, String filterChain new LinkedHashMap(); // anon 表示匿名可访问authc 表示需要认证 filterChain.put(/login, anon); filterChain.put(/captcha, anon); filterChain.put(/css/**, anon); filterChain.put(/js/**, anon); // /admin 下的所有路径都需要登录后可访问 filterChain.put(/admin/**, authc); // /admin/delete 需要拥有 delete 权限 filterChain.put(/admin/delete, perms[user:delete]); // /admin/dashboard 需要 admin 角色 filterChain.put(/admin/dashboard, roles[admin]); filterChain.put(/**, authc); bean.setFilterChainDefinitionMap(filterChain); return bean; } }这段配置里有一个很关键的细节filterChain 是有顺序的。Shiro 按照 Map 的插入顺序依次匹配 URL一旦命中就停止继续匹配。所以/admin/delete这条规则必须写在/admin/**之前否则请求在匹配到/admin/**的authc后就直接通过规则匹配了后面的perms[user:delete]永远不会生效。这是 Shiro 配置中最常见的坑之一。2.3 自定义 SessionManager 和 CacheManager上面配置类里我引用了一个sessionManager()方法和两个 Redis 相关的 Bean这是 Shiro 和 Redis 打通的关键。需要说明的是Shiro 的默认 SessionManager 是 ServletContainerSessionManager它把 Session 委托给 Servlet 容器管理。我们要做的是换成 Shiro 自己接管 Session 的 DefaultWebSessionManager并把 Session 持久化到 Redis。Bean public DefaultWebSessionManager sessionManager(RedisSessionDAO redisSessionDAO) { DefaultWebSessionManager sessionManager new DefaultWebSessionManager(); // 自定义 sessionId 从 cookie 中获取保证前后端分离场景下也能取到 sessionManager.setSessionIdCookieEnabled(true); sessionManager.setSessionIdUrlRewritingEnabled(false); sessionManager.setSessionDAO(redisSessionDAO); // 全局会话超时时间单位毫秒这里设置 30 分钟 sessionManager.setGlobalSessionTimeout(1800000); // 每隔 10 分钟检查一次过期会话 sessionManager.setSessionValidationInterval(600000); return sessionManager; }setSessionIdUrlRewritingEnabled(false)这个配置在前后端分离的项目里特别重要。如果置为 trueShiro 会在 URL 后面拼接 sessionId这样既难看又带来安全隐患URL 中的 sessionId 容易被日志系统或浏览器历史记录泄露。前后端分离时我们通常通过请求头Authorization或 Cookie 传递 sessionIdURL 重写毫无必要。RedisCacheManager 的配置代码如下它负责第二层缓存——授权信息的缓存Bean public RedisCacheManager redisCacheManager(RedisTemplateString, Object redisTemplate) { RedisCacheManager cacheManager new RedisCacheManager(); cacheManager.setRedisTemplate(redisTemplate); // Shiro 的 cache 命名前缀建议带业务标识避免和别的系统冲突 cacheManager.setKeyPrefix(shiro:cache:); // 全局缓存过期时间单位秒这里设置 10 分钟 cacheManager.setGlobalCacheExpire(600); return cacheManager; }到这里配置层的骨干已经就绪。下一节我们写逻辑代码。3. 认证流程落地从 Realm 到登录接口配置类只是骨架真正干活的是 Realm 和数据访问层。这一节我们把登录认证的完整链路走一遍自定义 Realm、密码校验方式、登录接口、以及把 Session 托管到 Redis 的细节。3.1 自定义 RealmShiro 和数据库之间的翻译官Realm 在 Shiro 中充当数据源和 Shiro 之间的适配器。我们不直接告诉 Shiro 用户表长什么样而是让它通过 Realm 来获取用户信息。自定义 Realm 需要继承AuthorizingRealm并重写两个方法doGetAuthorizationInfo授权和doGetAuthenticationInfo认证。Component public class UserRealm extends AuthorizingRealm { Autowired private UserService userService; Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); User user userService.findByUsername(username); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 这里建议一次性查出来避免每次请求都触发 N 次查询 SetString roles userService.findRolesByUserId(user.getId()); SetString perms userService.findPermissionsByUserId(user.getId()); info.setRoles(roles); info.setStringPermissions(perms); return info; } Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // token 中携带的就是登录时提交的用户名和密码 String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(账号不存在); } if (user.getStatus() 0) { throw new LockedAccountException(账号已被锁定); } // 第一个参数是 principal留给授权用第二个参数是数据库中的密码密文第三个参数是 realm 名称 return new SimpleAuthenticationInfo(username, user.getPassword(), getName()); } }这里有个很多人容易忽略的细节doGetAuthenticationInfo返回的SimpleAuthenticationInfo中的principal到底是什么。我习惯用用户名作为 principal因为授权时只需要根据用户名去查角色权限。如果你把整个 User 对象放进去作为 principal 也可以但授权时取出来的是内存引用在某些序列化/反序列化场景下会多一层开销我建议统一用用户唯一标识用户名或用户ID作为 principal。另一个细节是密码校验。Shiro 拿到token.getCredentials()用户提交的明文密码和SimpleAuthenticationInfo中的密文密码后会调用CredentialsMatcher做比对。默认的比对逻辑是简单字符串 equals这显然不安全。我们下面会配置一个加盐哈希的匹配器。3.2 密码加密存储加盐策略不能省用户表的密码字段如果是明文那项目发布出去基本等于裸奔。业内通用的做法是加盐后做多次哈希计算。Shiro 自带的HashedCredentialsMatcher支持 MD5、SHA-256 等算法配合随机盐值使用。我项目里的做法是注册时生成一个随机盐值UUID 前 8 位密码存储格式为盐值:加密后的密码这样同一个密码不同用户存出来的密文不同。登录时 Shiro 先取出用户密文拆出盐值再对提交的明文用同样的方式做哈希然后比对。Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(SHA-256); // 哈希次数常见是 2 或 3 次次数越多安全性越好但也越耗时 matcher.setHashIterations(2); return matcher; }然后在UserRealm的构造函数里setCredentialsMatcher(matcher)。注册时的密码处理代码大致如下// 注册用户时 String salt UUID.randomUUID().toString().replaceAll(-, ).substring(0, 8); String hashedPwd new SimpleHash(SHA-256, rawPassword, salt, 2).toHex(); // 保存到数据库的 password 字段 user.setPassword(salt : hashedPwd);哈希迭代次数不要选得太高。2 次对登录性能影响很小但如果选 1024 次高并发登录时会明显感觉到延迟。安全性和性能要平衡常规互联网项目 2 到 10 次之间比较合理。3.3 登录接口到底要不要自己写 Session登录接口本身不复杂但有几个细节值得展开。RestController public class LoginController { Autowired private UserService userService; PostMapping(/login) public Result? login(RequestBody LoginRequest request) { // 构造 Shiro 的 UsernamePasswordToken UsernamePasswordToken token new UsernamePasswordToken( request.getUsername(), request.getPassword()); Subject subject SecurityUtils.getSubject(); try { subject.login(token); // 认证通过后此时 Shiro 已经生成 Session String sessionId (String) subject.getSession().getId(); return Result.ok(sessionId); } catch (UnknownAccountException e) { return Result.error(账号不存在); } catch (IncorrectCredentialsException e) { return Result.error(密码错误); } catch (LockedAccountException e) { return Result.error(账号被锁定); } catch (AuthenticationException e) { return Result.error(认证失败); } } }登录成功后 Shiro 会把 Subject 的认证状态写入 Session。如果你在 sessionManager 里配置了 RedisSessionDAO这个 Session 会立刻同步到 Redis而不是留在本地内存中。前端得到 sessionId 后把它存到 Cookie 或者请求头中后续每次请求带上它Shiro 就会根据这个 sessionId 去 Redis 里查登录状态。注意这里返回 sessionId 的方式。如果是纯后端渲染的 MVC 项目sessionId 由 Cookie 自动携带不需要暴露给前端但如果是前后端分离前端可能需要手动把 sessionId 放到请求头里。此时 SessionManager 的 Cookie 机制依然存在所以即使前端不处理浏览器 Cookie 通常也能自动带上这在某些跨域场景下会遇到麻烦。3.4 RedisSessionDAO 的细节配置上面提到RedisSessionDAO这个类的配置直接决定 Session 怎么进出 Redis。我的配置如下Configuration public class RedisSessionConfig { Bean public RedisSessionDAO redisSessionDAO(RedisTemplateString, Object redisTemplate) { RedisSessionDAO sessionDAO new RedisSessionDAO(); sessionDAO.setRedisTemplate(redisTemplate); // Session 在 Redis 中的 key 前缀 sessionDAO.setKeyPrefix(shiro:session:); // Session 超时时间单位秒这里要和 sessionManager 的 globalSessionTimeout 保持一致 sessionDAO.setSessionTimeout(1800); return sessionDAO; } }这里有个非常隐蔽的问题setSessionTimeout的单位是秒而sessionManager.setGlobalSessionTimeout的单位是毫秒。如果两边不一致可能会出现Redis 里 Session 已经过期了但 Shiro 认为还没过期或者反过来。我在实践中踩过这个坑当时调试了一个晚上才想起来去对这两个值。建议两者配合使用时把数值换算成一致的时间长度写注释标记清楚。4. 授权机制从零到一RBAC 模型的落地认证搞定之后授权是下一个大活。授权要做的事情很简单判断当前 Subject 能不能访问某个接口、执行某个操作。RBAC角色-权限模型是目前最主流的设计方式用户归属于角色角色绑定权限。Shiro 对这个模型的支持很自然。4.1 授权信息的加载时机权限缓存的关键授权数据是通过doGetAuthorizationInfo返回的。但是这个方法什么时候被调用很多初学者以为是每次请求都会触发事实并非如此。Shiro 在检测到当前 Subject 尚未加载授权信息时会先检查缓存。如果缓存中没有才会调用doGetAuthorizationInfo去数据库查询查询完后结果会被存入 CacheManager。也就是说只要第一次某个用户的权限被加载过后续的权限判断都走缓存不会再触发数据库查询。这正是之前配置 RedisCacheManager 的意义所在。如果项目没有配置 CacheManagerShiro 默认不缓存授权信息每次接口权限校验都会重新调用 Realm 的 doGetAuthorizationInfo 方法你可能会在日志里看到大量的角色权限查询 SQL。虽然功能上没错但性能损耗不可忽视。4.2 注解权限和 URL 权限怎么选Shiro 提供两种授权方式编程式注解和 URL 过滤规则。注解方式适合方法级别的精细控制URL 过滤适合粗粒度的路径拦截。两者可以混用但建议团队里统一约定风格。注解方式需要在配置类里开启 Shiro 注解支持Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator new DefaultAdvisorAutoProxyCreator(); // 必须设置为 true否则 Spring AOP 代理时可能无法生效 creator.setProxyTargetClass(true); return creator; } Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }然后在 Controller 或 Service 方法上使用注解RequiresPermissions(user:delete) PostMapping(/admin/delete) public Result? deleteUser(RequestParam Long userId) { userService.deleteById(userId); return Result.ok(); } RequiresRoles(admin) GetMapping(/admin/dashboard) public Result? dashboard() { return Result.ok(); }URL 过滤规则的写法在前面的 ShiroFilterFactoryBean 里已经展示过了perms[user:delete]表示需要指定权限roles[admin]表示需要角色。URL 过滤方式是 Shiro Filter 层拦截的注解方式是 Spring AOP 切面拦截的前者控制路径能不能进后者控制方法能不能调。我个人建议是粗粒度如管理后台的所有/admin/**路径需要登录用 URL 过滤细粒度如删除操作需要user:delete权限用注解。这样代码里的权限逻辑跟业务方法放在一起可读性更好。4.3 权限字符串的命名规范权限字符串的命名虽然没人强制但一个好的命名能让项目维护性大幅提升。我常用的格式是资源:操作比如user:list查看用户列表user:create新增用户user:update修改用户user:delete删除用户order:export导出订单这样结构层级清晰后面写 SQL 或做权限点时也很容易确定某个权限资源属于哪个菜单。5. 实战过程中的三个深坑每一个都耗时半天写到这里配置和代码都通了系统也能跑起来了。但生产环境和本地跑通是两回事以下几个问题是我在实际项目中真实遇到的每个都花了不少时间排查。5.1 Redis 序列化引发的反序列化异常Shiro 把 Session 对象写入 Redis必然经过序列化。默认情况下如果 RedisTemplate 用的序列化器是 JDK 自带的JdkSerializationRedisSerializer你会发现 Redis 里的 key 是一长串不可读的二进制value 也是一堆绵延的序列化内容。更麻烦的是如果 Session 对象里的某个属性没有实现 Serializable 接口写入 Redis 时直接抛异常。我建议统一使用GenericJackson2JsonRedisSerializer或者 Fastjson 的序列化器。但注意GenericJackson2JsonRedisSerializer在反序列化时可能会因为org.apache.shiro.session.Session这种接口类型信息缺失而报错。更稳妥的方案是自定义一个 RedisTemplate指定 key 用 StringRedisSerializervalue 用 Fastjson 序列化器。Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); FastJson2JsonRedisSerializerObject serializer new FastJson2JsonRedisSerializer(Object.class); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; }Fastjson 序列化器在序列化 Session 时会把类型信息写进 JSON反序列化时能正确还原对象这是我试下来最稳定的组合。当然如果你不想引入 Fastjson用 Jackson 的序列化器加类型信息也可以原理类似。5.2 会话过期了但权限缓存还在这个场景很经典用户登录后访问了一个需要权限的接口权限被缓存到了 Redis。此时管理员在后台修改了这个用户的角色把权限收回了。按道理用户下一次请求应该被拒绝但因为授权缓存没有失效用户依然可以访问。所以权限变更时必须同步清理 Shiro 的授权缓存。可以在角色权限更新后调用// 清除某个用户的授权缓存 RedisCacheManager cacheManager (RedisCacheManager) securityManager.getCacheManager(); cacheManager.getCache(authorizationCache).remove(username);更彻底的办法是在 Realm 的 doGetAuthorizationInfo 中给权限查询加一个版本号或时间戳判断。但绝大多数项目里手动清理缓存就足够了。我一般在用户管理服务的修改角色和修改权限方法里统一清理对应用户的授权缓存避免把这种逻辑散落在各个 Controller 中。5.3 多实例部署时的 Session 互踢问题这是分布式会话里比较隐蔽的一个问题。当项目部署了两台服务器用户登录后 Session 存在 Redis 中。如果同一个账号在两台服务器上各登录一次Redis 里就会有两个 Session互不干扰。但业务上通常希望同账号只能一个地方登录新登录挤掉旧登录。Shiro 默认没有提供这个能力需要自己实现。思路是在 Realm 的认证方法里登录成功后把新的 sessionId 写入 Rediskey 为该用户名同时在 SessionManager 的创建会话时检查该用户名是否已经有活跃 sessionId如果有则删除旧的 Session。这个逻辑属于业务层定制Shiro 没有现成 API需要自己控制。我实测下来的方案是在登录接口校验完成后手动把旧 sessionId 从 RedisSessionDAO 中删掉再创建新 Session。这个方法虽然繁琐但它保证了唯一登录的需求而且对现有代码侵入很小。6. 性能调优的两条经验系统跑顺之后接下来要考虑性能和并发问题。Shiro 加 Redis 的组合瓶颈通常不在 Shiro 本身而在 Redis 的读写频率和网络开销。经验一是把授权缓存过期时间设置得合理一点。我之前的项目把授权缓存过期时间设为 10 分钟结果用户改完角色要等 10 分钟才能生效体验不好。后来改成 2 分钟配合主动刷新机制效果好了很多。权限数据是低频变化的数据不需要频繁过期但也不要设太长否则权限收回滞后严重。经验二是 Session 过期时间不要太短。有些同学把 Session 超时时间设为 30 分钟用户操作到一半就掉线重新登录体验很差。管理后台通常建议 2 小时移动端可以更长一些配合刷新机制。Shiro 的 Session 验证调度器会在后台定期清理过期 Session如果设置的超时时间太短用户高频操作时会频繁触发重新认证。还有一个我特别想强调的优化点关闭 Shiro 的 Session 在 Cookie 中的默认写入策略如果使用前后端分离架构不要完全依赖 Cookie而是让前端在每次请求的 header 中携带 sessionId。这里需要继承 DefaultWebSessionManager 自定义一个获取 sessionId 的方式核心思路就是从请求头中读取。public class HeaderWebSessionManager extends DefaultWebSessionManager { private static final String AUTH_HEADER Authorization; Override protected String getSessionId(ServletRequest request, ServletResponse response) { HttpServletRequest httpRequest (HttpServletRequest) request; String sessionId httpRequest.getHeader(AUTH_HEADER); if (StringUtils.hasText(sessionId)) { return sessionId; } return super.getSessionId(request, response); } }这样就把 Shiro 的 Session 获取逻辑从只认 Cookie扩展成了先从 header 取取不到再从 Cookie 取既兼容了前后端分离也保留了传统场景的可用性。最后分享我在项目里沉淀的三个习惯这套 Shiro 加 Redis 的组合我在多个项目里反复用过最终沉淀下来几个习惯可能对你有参考价值。第一个习惯是最小权限原则落实在代码层面。每一个接口尽量用权限注解去控制不要只依赖角色判断。角色是粗粒度的权限是细粒度的。比如管理员能导出订单这种需求如果只判断角色后面来个运营主管也要能导出就得到处改代码如果一开始就定义order:export权限再把这个权限分配给相关角色改动成本会低很多。第二个习惯是授权数据查询要一次性查出角色和权限。Shiro 在授权时会调用doGetAuthorizationInfo如果这个方法内部先查角色再查权限每次都是两次数据库交互。把这些查询合并成一条 SQL 或者一次服务调用授权缓存未命中时的开销能减少一半。权限缓存生效后这个开销只在第一次请求时产生。第三个习惯是遇到登录态相关的问题第一时间去 Redis 里看 Session 是否真实存在。到了这一步80% 的问题都能定位要么是 Redis key 过期了要么是 sessionId 对不上要么是序列化失败了。用redis-cli keys shiro:session:*看一眼再决定怎么排查。SpringBoot、Shiro、Redis 这个组合并不复杂真正复杂的是你在工程里对每一个细节的把控。希望这篇分享能帮你少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站