从会话到令牌:Spring Boot中登录认证的技术演进与实战选型

发布时间:2026/7/15 7:30:57

从会话到令牌:Spring Boot中登录认证的技术演进与实战选型 1. 从会话到令牌认证技术的演进之路记得我刚入行做Java开发那会儿处理用户登录最常用的还是传统的Cookie-Session方案。每次用户登录后服务端都会创建一个Session对象把用户信息存进去然后通过Cookie把Session ID传给浏览器。这种方案在单体应用时代确实简单好用但随着业务发展问题也逐渐暴露出来。最让我头疼的是集群环境下的Session共享问题。有一次公司业务量激增需要部署多台服务器做负载均衡。结果发现用户登录后请求被分配到不同服务器时Session就失效了。当时我们尝试了各种方案比如Session复制、Session粘滞最后用了Redis集中存储Session才解决问题。这让我开始思考有没有更优雅的解决方案2015年左右我第一次接触到JWTJSON Web Token。当时一个做Node.js的朋友向我炫耀他们如何用几行代码就实现了分布式认证让我这个Java程序员既羡慕又怀疑。直到后来在Spring Boot项目中实际应用JWT才发现这确实是认证技术的一次重要演进。2. 传统Cookie-Session的痛点分析2.1 状态保持的代价Cookie-Session机制本质上是一种有状态的认证方式。服务端需要维护每个用户的会话状态这在用户量不大时没问题但当用户量达到百万级时服务端的内存压力就会非常大。我曾经维护过一个日活百万的系统Session存储就占用了近20G内存。// 传统的Session使用方式 GetMapping(/profile) public String profile(HttpSession session) { User user (User) session.getAttribute(currentUser); return Hello, user.getName(); }2.2 集群环境下的挑战在微服务架构中服务实例通常是水平扩展的。使用Session会遇到著名的Session黏着问题。虽然可以通过Session复制或集中存储解决但都增加了系统复杂度。我们曾经因为Session同步不及时导致用户频繁掉线被客户投诉了好几次。2.3 移动端适配困难随着移动互联网兴起我们的API不仅要服务Web端还要服务iOS和Android客户端。但移动端对Cookie的支持不完善很多客户端需要手动处理Cookie非常容易出错。有一次因为Cookie域名设置问题导致APP登录状态无法保持紧急加班到凌晨才修复。3. JWT令牌技术的崛起3.1 什么是JWTJWT是一种开放标准RFC 7519它定义了一种紧凑且自包含的方式用于在各方之间安全地传输信息。一个JWT令牌看起来是这样的eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c它由三部分组成用点号分隔Header描述令牌类型和签名算法Payload包含用户信息和其他元数据Signature用于验证令牌完整性的签名3.2 JWT的核心优势无状态性服务端不需要存储会话信息减轻了服务器压力。我在一个高并发项目中改用JWT后服务器内存使用直接下降了40%。跨域支持完美适配移动端和前后端分离架构。现在我们的API可以同时服务Web、iOS、Android和小程序维护成本大大降低。安全性通过数字签名防止篡改。虽然JWT内容可以被解码查看但如果没有密钥就无法伪造。不过要注意敏感信息不应该放在JWT中因为Payload只是Base64编码并非加密。4. Spring Boot中的JWT实战4.1 引入依赖首先添加JJWT依赖到pom.xmldependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency4.2 JWT工具类我习惯封装一个JWT工具类包含生成和解析令牌的方法public class JwtUtils { private static final String SECRET_KEY your-256-bit-secret; private static final long EXPIRATION_TIME 86400000; // 24小时 public static String generateToken(MapString, Object claims) { return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }4.3 登录接口实现在登录接口中生成并返回JWT令牌PostMapping(/login) public Result login(RequestBody LoginRequest request) { User user userService.authenticate(request); MapString, Object claims new HashMap(); claims.put(userId, user.getId()); claims.put(username, user.getUsername()); claims.put(roles, user.getRoles()); String token JwtUtils.generateToken(claims); return Result.success(token); }5. 认证拦截的实现选择在Spring Boot中我们通常有两种方式实现请求拦截认证过滤器和拦截器。5.1 过滤器(Filter)方案过滤器是Java EE标准可以拦截所有请求。我一般会创建一个登录校验过滤器WebFilter(/*) public class JwtFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String uri request.getRequestURI(); if (uri.contains(/login)) { chain.doFilter(request, response); return; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { sendError(response, 缺少有效令牌); return; } try { Claims claims JwtUtils.parseToken(token.substring(7)); request.setAttribute(userClaims, claims); chain.doFilter(request, response); } catch (Exception e) { sendError(response, 令牌无效或已过期); } } private void sendError(HttpServletResponse response, String message) throws IOException { response.setContentType(application/json); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write({\error\:\ message \}); } }记得在启动类加上ServletComponentScan注解启用过滤器。5.2 拦截器(Interceptor)方案拦截器是Spring MVC提供的配置更灵活public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; if (handlerMethod.hasMethodAnnotation(AllowAnonymous.class)) { return true; } String token request.getHeader(Authorization); // 校验逻辑与过滤器类似 // ... return true; } }配置拦截器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login); } }5.3 过滤器 vs 拦截器如何选型根据我的经验选择主要考虑以下几点作用范围过滤器可以拦截所有请求包括静态资源拦截器只能拦截Controller请求执行顺序过滤器在拦截器之前执行依赖注入拦截器可以使用Spring的依赖注入过滤器默认不行需要通过FilterRegistrationBean配置灵活性拦截器可以精确控制拦截哪些URL排除哪些URL我现在的做法是简单的认证校验用过滤器复杂的权限控制用拦截器。对于纯API项目直接用拦截器更合适。6. 实战中的经验与坑点6.1 令牌刷新机制JWT一旦签发在过期前无法撤销。为了解决这个问题我通常采用短期令牌刷新令牌的方案PostMapping(/refresh) public Result refreshToken(RequestHeader(Authorization) String refreshToken) { Claims claims JwtUtils.parseToken(refreshToken); if (!refresh.equals(claims.get(type))) { throw new InvalidTokenException(非法的刷新令牌); } String newToken JwtUtils.generateToken(claims); return Result.success(newToken); }6.2 安全性最佳实践一定要使用HTTPS传输令牌令牌过期时间不宜过长我一般设置访问令牌2小时刷新令牌7天敏感操作需要二次认证考虑加入IP绑定或设备指纹等防篡改措施6.3 性能优化JWT的一个缺点是令牌体积较大每次请求都要携带。对于内网高并发场景可以考虑缩短Header中的Authorization字段// 生成简化的token ID String tokenId UUID.randomUUID().toString(); redisTemplate.opsForValue().set(tokenId, userId, 2, TimeUnit.HOURS); return tokenId;7. 技术选型建议经过多个项目的实践我总结出以下选型建议传统Web应用如果是有状态的Web应用比如需要服务端渲染Cookie-Session仍然是不错的选择前后端分离/移动端JWT是更好的选择特别是需要支持多端登录的场景高安全要求系统可以考虑OAuth2.0或结合JWT的增强方案内部微服务可以考虑更轻量的方案比如基于HMAC的签名认证最近我在一个新项目中尝试了Spring Security JWT的组合发现它提供了更完善的认证授权解决方案特别是对于复杂的权限系统。不过学习曲线比较陡对于简单项目可能有点重。

相关新闻