当前位置: 首页 > news >正文

Spring Boot整合Shiro:从Session到Token的无缝迁移实战

1. 为什么需要从Session迁移到Token

在传统的Spring Boot项目中整合Shiro时,Session是最常用的认证方式。但随着业务发展,特别是需要支持单点登录或微服务架构时,Session的局限性就暴露出来了。我去年接手的一个电商项目就遇到了这个问题:当系统需要接入多个子系统时,Session共享成了大麻烦。

Session机制最大的问题是有状态性。服务器需要维护每个用户的Session信息,这在分布式环境下会导致:

  • 需要Session共享方案(如Redis)
  • 跨域访问困难
  • 移动端支持不友好
  • 服务器内存压力大

相比之下,Token认证(如JWT)具有明显优势:

  1. 无状态:服务器不需要存储会话信息
  2. 跨域友好:通过HTTP Header传输
  3. 移动端适配:天然适合APP开发
  4. 微服务友好:各服务可独立验证Token

但改造的最大挑战在于:如何在不破坏原有Shiro权限体系的前提下,将认证方式从Session切换为Token。这正是本文要解决的核心问题。

2. 改造前的准备工作

2.1 理解Shiro的核心流程

在动手改造前,必须清楚Shiro的工作机制。我画过无数次的流程图,总结下来就三个关键点:

  1. Subject:当前用户主体
  2. SecurityManager:安全管理的核心
  3. Realm:权限数据来源

当我们调用subject.login()时,Shiro会:

  1. 委托SecurityManager处理
  2. SecurityManager调用配置的Realm验证凭证
  3. 验证成功后创建Session(默认行为)

2.2 确定改造范围

根据我的项目经验,改造主要涉及四个部分:

  1. 登录接口:生成Token替代创建Session
  2. 拦截器:验证Token替代Session检查
  3. Shiro配置:替换默认的Session管理器
  4. Token管理:生成、存储、刷新机制

建议先备份原有代码,我吃过没备份的亏,改出问题时回退特别麻烦。

3. 核心改造步骤详解

3.1 登录接口改造

原来的Session登录方式是这样的:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(dto.getUsername(), dto.getPassword()); subject.login(token); // 这里会创建Session return Result.success(); }

改造后的Token版本:

@PostMapping("/login") public Result login(HttpServletResponse response, @RequestBody LoginDTO dto) { // 1. 原始Shiro认证 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken shiroToken = new UsernamePasswordToken(dto.getUsername(), dto.getPassword()); subject.login(shiroToken); // 2. 生成业务Token(示例使用JWT) String jwt = JwtUtil.generateToken(dto.getUsername()); // 3. 存储Token(根据业务选择存储方式) tokenService.saveToken(userId, jwt); // 4. 返回Token给客户端 response.setHeader("Authorization", jwt); return Result.success(jwt); }

这里有几个关键点:

  1. 仍然使用Shiro完成原始认证
  2. 认证通过后才生成业务Token
  3. Token需要存储(Redis或数据库)
  4. 返回方式可以是Header、Body或Cookie

3.2 自定义Token拦截器

这是整个改造最核心的部分。我们需要继承BasicHttpAuthenticationFilter

public class TokenFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { // 1. 获取Token HttpServletRequest httpRequest = (HttpServletRequest) request; String token = httpRequest.getHeader("Authorization"); // 2. Token不存在直接拒绝 if(StringUtils.isBlank(token)) { return false; } try { // 3. 验证Token有效性 Claims claims = JwtUtil.parseToken(token); String username = claims.getSubject(); // 4. 构造ShiroToken(关键步骤!) UsernamePasswordToken shiroToken = new UsernamePasswordToken(username, null); // 5. 交给Shiro认证(此时不会创建Session) SecurityUtils.getSubject().login(shiroToken); return true; } catch (Exception e) { return false; } } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpResponse.getWriter().write("Invalid token"); return false; } }

这个拦截器做了几件重要的事:

  1. 从Header提取Token
  2. 验证Token有效性
  3. 构造Shiro认识的Token对象
  4. 触发Shiro认证流程但不创建Session

3.3 Shiro配置调整

在ShiroConfig中需要做三处关键修改:

@Configuration public class ShiroConfig { @Bean public SessionManager sessionManager() { // 禁用Session DefaultWebSessionManager manager = new DefaultWebSessionManager(); manager.setSessionValidationSchedulerEnabled(false); manager.setSessionIdUrlRewritingEnabled(false); return manager; } @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); // 添加自定义过滤器 Map<String, Filter> filters = new HashMap<>(); filters.put("token", new TokenFilter()); factory.setFilters(filters); // 配置过滤规则 Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login", "anon"); filterMap.put("/**", "token"); // 所有请求走Token验证 factory.setFilterChainDefinitionMap(filterMap); return factory; } }

特别注意:

  1. 禁用了Session管理器
  2. 注册了我们的TokenFilter
  3. 配置了拦截规则

4. 关键问题与解决方案

4.1 如何保持权限体系不变

这是很多开发者的疑问:改用Token后,Shiro的@RequiresRoles等注解还能用吗?答案是肯定的。因为在我们的拦截器中,仍然调用了subject.login(),Shiro的权限体系完全不受影响。

实测代码:

@RequiresRoles("admin") @GetMapping("/admin") public Result adminPage() { // 只有admin角色能访问 return Result.success(); }

4.2 Token管理策略

根据项目需求,Token管理可以有不同的实现方式:

  1. 简单JWT

    • 优点:无需存储
    • 缺点:无法主动失效
  2. Redis存储

    // 存储示例 redisTemplate.opsForValue().set("token:"+username, token, 2, TimeUnit.HOURS); // 验证时检查 String storedToken = redisTemplate.opsForValue().get("token:"+username); if(!token.equals(storedToken)) { throw new AuthenticationException(); }
  3. 数据库存储

    • 适合需要记录详细登录信息的场景
    • 可以方便实现多端登录管理

4.3 跨域问题处理

如果前端是独立部署,需要在拦截器中处理CORS:

@Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Origin", "*"); httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS"); httpResponse.setHeader("Access-Control-Allow-Headers", "Authorization"); return true; }

5. 完整代码示例

5.1 JWT工具类

public class JwtUtil { private static final String SECRET = "your-secret-key"; private static final long EXPIRE = 3600L; // 1小时 public static String generateToken(String username) { Date now = new Date(); Date expire = new Date(now.getTime() + EXPIRE * 1000); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expire) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

5.2 增强版Token拦截器

public class TokenFilter extends BasicHttpAuthenticationFilter { @Autowired private UserService userService; // 如何注入看下文 @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { try { return executeLogin(request, response); } catch (Exception e) { responseError(response, "Token验证失败"); } } return false; } @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader("Authorization"); return token != null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = (HttpServletRequest) request; String token = httpRequest.getHeader("Authorization"); // 验证Token Claims claims = JwtUtil.parseToken(token); String username = claims.getSubject(); // 获取用户详情(可根据需要) User user = userService.findByUsername(username); // 构造ShiroToken UsernamePasswordToken shiroToken = new UsernamePasswordToken( username, null, user.getRoles(), // 传入角色信息 user.getPermissions() // 传入权限信息 ); SecurityUtils.getSubject().login(shiroToken); return true; } private void responseError(ServletResponse response, String message) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpResponse.setContentType("application/json;charset=utf-8"); httpResponse.getWriter().write(JSON.toJSONString(Result.error(message))); } }

5.3 解决Filter注入问题

自定义Filter默认无法使用@Autowired,需要通过以下方式解决:

@Bean public FilterRegistrationBean<TokenFilter> filterRegistrationBean( @Autowired UserService userService) { FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>(); TokenFilter filter = new TokenFilter(); filter.setUserService(userService); // 手动注入 registration.setFilter(filter); registration.setEnabled(false); // 不由Servlet容器管理 return registration; }

然后在ShiroConfig中:

@Bean public TokenFilter tokenFilter(FilterRegistrationBean<TokenFilter> registration) { return registration.getFilter(); }

6. 测试与验证

6.1 测试登录接口

使用Postman测试:

  1. POST /login
    { "username": "admin", "password": "123456" }
  2. 检查返回的Token

6.2 测试权限控制

  1. 不带Token访问受限接口 - 应返回401
  2. 带有效Token访问:
    • 有权限的接口 - 返回200
    • 无权限的接口 - 返回403

6.3 性能测试建议

使用JMeter模拟:

  1. 100并发持续请求
  2. 对比改造前后的内存占用
  3. 重点关注Token解析耗时

7. 进阶优化方向

7.1 Token刷新机制

实现无感刷新:

@PostMapping("/refresh") public Result refreshToken(@RequestHeader("Authorization") String oldToken) { Claims claims = JwtUtil.parseToken(oldToken); if(claims.getExpiration().before(new Date())) { throw new BusinessException("Token已过期"); } String newToken = JwtUtil.generateToken(claims.getSubject()); return Result.success(newToken); }

7.2 多端登录管理

在Token中增加客户端标识:

public static String generateToken(String username, String clientType) { return Jwts.builder() .setSubject(username) .claim("client", clientType) // 添加客户端标识 .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); }

7.3 黑名单机制

对于需要主动注销的场景:

// 注销时 redisTemplate.opsForValue().set("blacklist:"+token, "1", EXPIRE, TimeUnit.SECONDS); // 拦截器中检查 if(redisTemplate.hasKey("blacklist:"+token)) { throw new AuthenticationException("Token已注销"); }

8. 项目中的实际坑点

  1. 时间同步问题:JWT校验依赖服务器时间,集群环境下务必保证时间同步
  2. Token泄露处理:建议结合IP识别、UA指纹等增强安全性
  3. 性能瓶颈:Redis存储Token时,要注意合理设置过期时间
  4. 移动端适配:Android的OkHttp默认不会携带Authorization头,需要特殊处理

我在实际项目中遇到一个典型问题:当Token过期时间设置过长时,会导致安全性下降;设置过短又影响用户体验。最终采用的方案是:

  • 访问Token有效期2小时
  • 刷新Token有效期7天
  • 每次请求检查剩余有效期小于30分钟时自动刷新

这种方案既保证了安全性,又避免了频繁登录。

http://www.cnnetsun.cn/news/1913963.html

相关文章:

  • 如何3分钟搞定Figma中文界面:设计师必备的终极翻译插件指南
  • 3大核心场景实战:用d2s-editor打造你的暗黑2终极角色
  • 给Pixel4注入新灵魂:手把手教你定制Android 12内核,开启隐藏功能与性能调优
  • 工业DPM扫码:PVC/ABS 部件二维码识读难点与京元C75DP 技术实现
  • 天赐范式第11天牛马时间:抱走特斯拉,如何利用 6.57Hz 地球共振为 DNA-AI 芯片无线无限充电?
  • 爱毕业(aibiye)让数学建模论文的复现与智能排版更高效、更精准
  • 机器学习工程师的秘密武器:Meta 如何让AI变身“实战专家“
  • 避坑指南:当Buildroot工具链找不到version.h时该怎么办?5种排查方案
  • AI编程革命:Codex高效脚本实践指南
  • Credo同意收购DustPhotonics,加快进军硅光子领域,推动下一代光互连业务拓展
  • Claude Code Routines:如何让AI编程助手实现全自动工作流?
  • 手机摄像头图像质量优化指南:自动曝光/对焦的底层逻辑与调试秘籍
  • 多模态大模型如何读懂CT+病理+电子病历?:三甲医院AI联合实验室实测92.6%诊断一致性
  • 从原理到实战:用Qt和C++手搓一个带容错的二维码生成器
  • 线程本地缓存?CPU缓存!
  • 企微获客自动化落地——从手动内耗到API集成的技术实现
  • BilibiliDown终极指南:如何轻松批量下载B站视频并建立个人视频库
  • GOM引擎传奇架设:从M2报错到登录器黑屏的实战排雷指南
  • PMP证书在国内究竟认不认?看这三点就明白了!
  • 意义行为原生论:行为即痕迹——主客观关系的重构(正)
  • 5步掌握AssetStudio:Unity游戏资源提取完整实战手册
  • SimpleFOC源码学习07(v2.3.2) - 增量式编码器Encoder.cpp与Encoder.h,从一对 A、B 信号,到速度、方向、绝对位置的完整解法
  • PyTorch 2.1 编译优化:TorchScript到AOT
  • Python 并发编程:asyncio vs threading vs multiprocessing
  • TDesign Vue Next 表格虚拟滚动深度解析:如何实现万级数据秒级渲染?
  • CVPR 2026 | 提速100倍!首个端到端Real-to-Sim物体级感知与重建框架
  • 海南省乡镇界SHP数据实战:从ArcGIS加载到WGS84坐标解析
  • 2025届必备的五大AI辅助写作神器解析与推荐
  • 瑞萨RZN2L固件加密指南:利用OTP和UID实现安全升级
  • 避开宝塔强制绑定:我为什么选择降级到7.4.5而非最新版,以及背后的版本安全考量