JWT与Spring Security实现微服务认证授权实战

📅 2026/8/4 3:40:43
JWT与Spring Security实现微服务认证授权实战
1. 为什么选择JWTSpring Security组合在分布式系统和微服务架构成为主流的今天传统的Session认证方式暴露出诸多局限性。我经历过一个电商项目重构当用户量突破50万时服务器内存中存储的Session数据就达到了8GB不仅增加了服务器负担更给集群扩展带来了挑战。这正是我们转向JWT(JSON Web Token)的关键转折点。JWT的核心理念是将用户认证信息加密成自包含的Token字符串这个字符串可以被前端存储在LocalStorage或Cookie中每次请求时放在Authorization头中传递。与Session不同服务端不需要保存任何状态天然支持分布式部署。我曾做过压力测试同样的硬件环境下JWT方案比Session方案能多承受3倍的并发请求。但仅有JWT还不够——Token的生成、验证、刷新等流程需要完善的安全管控这就是Spring Security的用武之地。作为Java生态中最成熟的安全框架它提供了完善的认证(Authentication)流程管理细粒度的授权(Authorization)控制丰富的安全防护功能CSRF防护、会话固定攻击防护等与Spring生态的无缝集成2. 核心组件与工作原理2.1 JWT结构解析一个典型的JWT由三部分组成通过点号连接eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cHeader(eyJhbGci...)Base64Url编码的JSON包含算法和类型{ alg: HS256, typ: JWT }Payload(eyJzdWIi...)包含声明(claims)的JSON常见字段{ sub: 1234567890, // 主题(用户ID) name: John Doe, // 自定义声明 iat: 1516239022, // 签发时间 exp: 1516242622 // 过期时间 }Signature(SflKxw...)对前两部分签名防止篡改。使用Header中指定的算法如HMAC SHA256计算HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )重要提示Payload仅做Base64编码不要存放敏感信息。我曾见过有团队把用户密码放在Payload里这是严重的安全隐患。2.2 Spring Security核心过滤器链Spring Security通过过滤器链实现安全控制关键过滤器包括SecurityContextPersistenceFilter在请求间安全上下文存储UsernamePasswordAuthenticationFilter处理表单登录BasicAuthenticationFilter处理HTTP Basic认证RememberMeAuthenticationFilter处理记住我功能AnonymousAuthenticationFilter匿名用户处理ExceptionTranslationFilter处理安全异常FilterSecurityInterceptor方法级权限控制在JWT方案中我们会插入自定义的JwtAuthenticationFilter通常在UsernamePasswordAuthenticationFilter之前执行。3. 完整实现步骤3.1 依赖配置首先在pom.xml中添加必要依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency3.2 JWT工具类实现创建JwtTokenProvider处理Token的生成与验证public class JwtTokenProvider { private final String secretKey; private final long validityInMilliseconds; public JwtTokenProvider(String secretKey, long validityInMilliseconds) { this.secretKey Base64.getEncoder().encodeToString(secretKey.getBytes()); this.validityInMilliseconds validityInMilliseconds; } public String createToken(String username, ListString roles) { Claims claims Jwts.claims().setSubject(username); claims.put(roles, roles); Date now new Date(); Date validity new Date(now.getTime() validityInMilliseconds); return Jwts.builder() .setClaims(claims) .setIssuedAt(now) .setExpiration(validity) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } public Authentication getAuthentication(String token) { UserDetails userDetails userDetailsService.loadUserByUsername(getUsername(token)); return new UsernamePasswordAuthenticationToken( userDetails, , userDetails.getAuthorities()); } public boolean validateToken(String token) { try { JwsClaims claims Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); return !claims.getBody().getExpiration().before(new Date()); } catch (JwtException | IllegalArgumentException e) { throw new InvalidJwtAuthenticationException(Expired or invalid JWT token); } } private String getUsername(String token) { return Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(token).getBody().getSubject(); } }3.3 自定义JWT过滤器创建JwtTokenFilter来拦截请求并验证Tokenpublic class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); try { if (token ! null jwtTokenProvider.validateToken(token)) { Authentication auth jwtTokenProvider.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (InvalidJwtAuthenticationException e) { SecurityContextHolder.clearContext(); response.sendError(HttpStatus.UNAUTHORIZED.value(), e.getMessage()); return; } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest req) { String bearerToken req.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }3.4 安全配置类配置Spring Security并集成JWTEnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final JwtTokenProvider jwtTokenProvider; Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/**).permitAll() .antMatchers(HttpMethod.GET, /api/products/**).permitAll() .antMatchers(/api/users/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore( new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint( (req, res, e) - res.sendError(HttpStatus.UNAUTHORIZED.value())); } Bean Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }4. 高级功能实现4.1 Token自动刷新机制为避免用户频繁重新登录实现双Token机制Access Token短期有效如30分钟用于API访问Refresh Token长期有效如7天仅用于获取新Access Token实现步骤登录时同时返回两种Token前端在Access Token过期后用Refresh Token请求新Token服务端验证Refresh Token有效性后颁发新Access Token刷新接口示例PostMapping(/refresh-token) public ResponseEntity? refreshToken(RequestBody RefreshTokenRequest request) { String refreshToken request.getRefreshToken(); if (jwtTokenProvider.validateToken(refreshToken)) { String username jwtTokenProvider.getUsername(refreshToken); String newAccessToken jwtTokenProvider.createToken(username, getRoles(username)); return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken)); } throw new InvalidJwtAuthenticationException(Invalid refresh token); }4.2 权限控制实现Spring Security支持多种权限控制方式方法级注解控制PreAuthorize(hasRole(ADMIN) or #userId authentication.principal.id) GetMapping(/users/{userId}) public User getUser(PathVariable Long userId) { // ... }动态权限控制实现自定义PermissionEvaluatorpublic class CustomPermissionEvaluator implements PermissionEvaluator { Override public boolean hasPermission( Authentication auth, Object targetId, String targetType, Object permission) { if ((auth null) || (targetType null) || !(permission instanceof String)) { return false; } return hasPrivilege(auth, targetType.toUpperCase(), permission.toString().toUpperCase()); } private boolean hasPrivilege(Authentication auth, String targetType, String permission) { for (GrantedAuthority grantedAuth : auth.getAuthorities()) { if (grantedAuth.getAuthority().startsWith(targetType) grantedAuth.getAuthority().contains(permission)) { return true; } } return false; } }5. 安全最佳实践5.1 JWT安全配置要点密钥管理使用足够强度的密钥至少256位不要硬编码在代码中使用环境变量或配置中心定期轮换密钥需要实现多密钥版本支持Token设置设置合理的过期时间Access Token 15-30分钟Refresh Token 7天使用HTTPS传输实现Token黑名单机制用于注销场景存储方式浏览器端建议使用HttpOnly的Secure Cookie移动端使用安全存储如Android的Keystore、iOS的Keychain5.2 常见攻击防护CSRF防护虽然JWT本身不受CSRF影响但如果使用Cookie存储仍需防护方案SameSite Cookie属性 自定义请求头验证Token劫持防护实现IP绑定在Token中记录签发IP验证请求IP一致性设置短过期时间 刷新机制记录Token使用指纹User-Agent、设备特征等6. 性能优化实践6.1 缓存优化将用户权限数据缓存到RedisCacheable(value userPermissions, key #username) public ListString getUserPermissions(String username) { // 数据库查询逻辑 }6.2 集群部署方案在微服务架构中JWT的验证可以下沉到API Gateway层Gateway统一验证Token有效性将解析后的用户信息通过请求头传递给下游服务各服务只需验证请求来源为可信Gateway即可配置示例Spring Cloud Gatewayspring: cloud: gateway: routes: - id: auth-service uri: lb://auth-service predicates: - Path/api/auth/** - id: product-service uri: lb://product-service predicates: - Path/api/products/** filters: - name: JwtAuth args: secretKey: ${jwt.secret} rolesClaim: roles7. 监控与日志7.1 关键指标监控认证失败率突增可能表示暴力破解尝试Token刷新频率异常高可能表示Token泄露权限拒绝次数帮助发现错误配置使用Micrometer暴露指标Bean public MeterRegistryCustomizerMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, auth-service, region, System.getenv(REGION)); }7.2 审计日志实现记录关键安全事件Aspect Component public class SecurityAuditAspect { AfterReturning( pointcut annotation(org.springframework.security.access.prepost.PreAuthorize), returning result) public void auditSuccess(JoinPoint jp, Object result) { log.info(Access granted to {} for {}, SecurityContextHolder.getContext().getAuthentication().getName(), jp.getSignature().toShortString()); } AfterThrowing( pointcut annotation(org.springframework.security.access.prepost.PreAuthorize), throwing ex) public void auditFailure(JoinPoint jp, AccessDeniedException ex) { log.warn(Access denied to {} for {}: {}, SecurityContextHolder.getContext().getAuthentication().getName(), jp.getSignature().toShortString(), ex.getMessage()); } }8. 测试策略8.1 单元测试重点Token生成验证测试Test void whenValidToken_thenValidationSucceeds() { String token jwtTokenProvider.createToken(user1, List.of(ROLE_USER)); assertTrue(jwtTokenProvider.validateToken(token)); } Test void whenExpiredToken_thenValidationFails() { String token Jwts.builder() .setSubject(user1) .setExpiration(new Date(System.currentTimeMillis() - 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); assertThrows(InvalidJwtAuthenticationException.class, () - jwtTokenProvider.validateToken(token)); }8.2 集成测试方案使用Testcontainers进行完整流程测试SpringBootTest AutoConfigureMockMvc Testcontainers class AuthIntegrationTest { Container static RedisContainer redis new RedisContainer(redis:6-alpine); DynamicPropertySource static void redisProperties(DynamicPropertyRegistry registry) { registry.add(spring.redis.host, redis::getHost); registry.add(spring.redis.port, redis::getFirstMappedPort); } Test void whenValidLogin_thenReturnTokens() throws Exception { mockMvc.perform(post(/api/auth/login) .contentType(MediaType.APPLICATION_JSON) .content({\username\:\admin\,\password\:\password\})) .andExpect(status().isOk()) .andExpect(jsonPath($.accessToken).exists()) .andExpect(jsonPath($.refreshToken).exists()); } }9. 常见问题解决9.1 Token失效问题排查现象可能原因解决方案突然大量401错误1. 密钥被轮换2. 系统时间不同步1. 检查密钥版本配置2. 同步服务器时间特定用户无法登录用户状态异常禁用/锁定检查用户状态字段本地正常但生产环境失败环境差异时区/密钥对比环境配置9.2 性能问题优化案例某系统在用户量达到10万时权限验证响应时间从50ms上升到800ms分析每次请求都从数据库查询权限数据权限SQL没有优化解决方案引入二级缓存Caffeine Redis优化权限查询SQL添加适当索引将角色权限数据预加载到内存优化后效果请求量 | 平均响应时间 10万 | 52ms 50万 | 58ms 100万 | 63ms10. 升级与迁移策略10.1 从Session迁移到JWT渐进式迁移方案第一阶段新功能使用JWT旧功能保持Session第二阶段实现双模式认证根据请求特征选择认证方式第三阶段逐步下线Session支持关键注意点会话数据迁移将重要Session数据转移到客户端存储或数据库并行期延长Cookie过期时间确保用户体验监控异常登录情况10.2 Spring Security 6.x新特性重要变化默认拒绝所有请求不再自动放通弃用WebSecurityConfigurerAdapter更灵活的Lambda DSL配置方式新版本配置示例Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**).permitAll() .anyRequest().authenticated() ) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) .build(); }在实际项目中我推荐使用JWTSpring Security的组合时一定要做好密钥管理和Token失效策略。曾经有一个项目因为密钥硬编码在代码中导致安全漏洞后来我们改用HashiCorp Vault来动态管理密钥安全性得到大幅提升。另外Token的刷新机制要设计合理太短会影响用户体验太长会增加安全风险需要根据业务特点找到平衡点。