JWT与Spring Security实现微服务认证授权实战
1. 为什么选择JWT+Spring 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>3.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, List<String> 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 { Jws<Claims> 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来拦截请求并验证Token:
public 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并集成JWT:
@EnableWebSecurity 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) { // ... }动态权限控制实现自定义PermissionEvaluator:
public 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的Keychain)
5.2 常见攻击防护
CSRF防护
- 虽然JWT本身不受CSRF影响,但如果使用Cookie存储仍需防护
- 方案:SameSite Cookie属性 + 自定义请求头验证
Token劫持防护
- 实现IP绑定:在Token中记录签发IP,验证请求IP一致性
- 设置短过期时间 + 刷新机制
- 记录Token使用指纹(User-Agent、设备特征等)
6. 性能优化实践
6.1 缓存优化
将用户权限数据缓存到Redis:
@Cacheable(value = "userPermissions", key = "#username") public List<String> getUserPermissions(String username) { // 数据库查询逻辑 }6.2 集群部署方案
在微服务架构中,JWT的验证可以下沉到API Gateway层:
- Gateway统一验证Token有效性
- 将解析后的用户信息通过请求头传递给下游服务
- 各服务只需验证请求来源为可信Gateway即可
配置示例(Spring Cloud Gateway):
spring: 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 MeterRegistryCustomizer<MeterRegistry> 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(); }在实际项目中,我推荐使用JWT+Spring Security的组合时,一定要做好密钥管理和Token失效策略。曾经有一个项目因为密钥硬编码在代码中导致安全漏洞,后来我们改用HashiCorp Vault来动态管理密钥,安全性得到大幅提升。另外,Token的刷新机制要设计合理,太短会影响用户体验,太长会增加安全风险,需要根据业务特点找到平衡点。