1. 项目概述
在微服务架构中,统一认证授权是一个关键的基础设施组件。本文将详细介绍如何基于Spring Security构建一个资源服务器(Resource Server),实现JWT令牌的解析和权限注入。这个方案可以无缝集成到现有系统中,为微服务提供统一的无状态认证能力。
作为开发者,我们经常遇到这样的场景:多个微服务需要共享同一套认证体系,但又不能每个服务都重复实现认证逻辑。通过本文介绍的方案,你可以在资源服务器中快速集成JWT认证,实现以下核心功能:
- 从Authorization头中提取Bearer Token
- 验证JWT签名并解析标准声明(sub)和自定义声明(roles)
- 将用户身份和权限注入Spring Security上下文
- 支持方法级权限控制(@PreAuthorize)
- 完全无状态,适合微服务架构
这个方案已经在生产环境验证,支持日活百万级用户的系统。下面让我们深入实现细节。
2. 核心依赖配置
2.1 Maven依赖
首先需要在pom.xml中添加必要的依赖:
xml复制<!-- Spring Security核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT库(JWT解析) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Servlet API(Spring Boot已内置,可选) -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
依赖选择说明:
- spring-boot-starter-security:提供Spring Security核心功能
- jjwt-*:JJWT库用于JWT解析和验证,这是目前Java生态中最成熟的JWT库
- jakarta.servlet-api:Servlet规范API,Spring Boot项目通常已内置
注意:生产环境建议锁定所有依赖版本,避免因依赖升级导致兼容性问题。
2.2 版本兼容性
本方案基于以下版本验证:
- Spring Boot 3.x
- Java 17+
- JJWT 0.12.x
如果你使用较旧版本的Spring Boot(2.x),需要注意:
- Servlet包名从javax迁移到了jakarta
- Spring Security配置类有细微差异
- 需要调整JJWT版本到0.11.x
3. JWT过滤器实现
3.1 过滤器核心逻辑
JWT过滤器的核心职责是:
- 从请求头中提取JWT令牌
- 验证令牌有效性
- 解析用户身份和权限
- 构建Authentication对象并注入安全上下文
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtVerifier jwtVerifier;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 获取Authorization头
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 2. 提取Token
String token = header.substring(7);
try {
// 3. 验证并解析JWT
JwtClaims claims = jwtVerifier.verify(token);
// 4. 构建权限集合
Collection<SimpleGrantedAuthority> authorities = claims.roles().stream()
.map(r -> r.startsWith("ROLE_") ? r : "ROLE_" + r)
.map(SimpleGrantedAuthority::new)
.toList();
// 5. 构建Authentication对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
claims.subject(), // 用户标识
null, // 凭证(密码) - JWT无需密码
authorities // 权限列表
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 6. 注入安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception ex) {
// 令牌无效时清空上下文
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
关键点说明:
- 继承OncePerRequestFilter确保每个请求只处理一次
- 令牌格式必须为"Bearer
" - 角色名称自动补全"ROLE_"前缀,兼容Spring Security的hasRole()检查
- 令牌无效时清空上下文,后续流程会返回401
3.2 异常处理策略
生产环境中需要考虑各种异常情况:
- 令牌过期(ExpiredJwtException)
- 签名无效(SignatureException)
- 令牌格式错误(MalformedJwtException)
- 签发方不匹配(InvalidClaimException)
建议的异常处理改进:
java复制try {
// JWT验证逻辑
} catch (ExpiredJwtException ex) {
log.warn("JWT令牌已过期: {}", ex.getMessage());
throw new AuthenticationException("令牌已过期", ex);
} catch (SignatureException ex) {
log.warn("JWT签名无效: {}", ex.getMessage());
throw new AuthenticationException("令牌签名无效", ex);
} catch (Exception ex) {
log.warn("JWT验证失败: {}", ex.getMessage());
throw new AuthenticationException("令牌验证失败", ex);
}
4. JWT验证器实现
4.1 验证器核心类
JwtVerifier负责JWT的签名验证和声明解析:
java复制public class JwtVerifier {
private final SecretKey key;
private final String expectedIssuer;
public JwtVerifier(String hmacSecret, String expectedIssuer) {
this.key = Keys.hmacShaKeyFor(hmacSecret.getBytes(StandardCharsets.UTF_8));
this.expectedIssuer = expectedIssuer;
}
public JwtClaims verify(String token) throws Exception {
Claims claims = Jwts.parser()
.verifyWith(key)
.requireIssuer(expectedIssuer)
.build()
.parseSignedClaims(token)
.getPayload();
String sub = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) claims.get("roles", List.class);
return new JwtClaims(sub, roles == null ? List.of() : roles);
}
}
关键配置参数:
- hmacSecret:HS256算法的对称密钥,长度至少32字符
- expectedIssuer:预期的签发方标识,防止令牌被滥用
4.2 生产环境配置建议
-
密钥管理:
- 不要硬编码在代码中
- 使用配置中心或KMS服务管理
- 定期轮换密钥
-
签发方验证:
- 确保iss声明与预期一致
- 可以配置多个可信签发方
-
声明校验:
- 验证exp(过期时间)
- 验证nbf(不早于时间)
- 验证aud(受众)
示例配置application.yml:
yaml复制jwt:
hmac-secret: ${JWT_HMAC_SECRET:default-32-chars-secret-please-change-me}
issuer: ${JWT_ISSUER:auth-service}
audience: ${JWT_AUDIENCE:resource-server}
5. Spring Security配置
5.1 安全配置类
java复制@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
JwtVerifier jwtVerifier(
@Value("${jwt.hmac-secret}") String hmacSecret,
@Value("${jwt.issuer}") String issuer) {
return new JwtVerifier(hmacSecret, issuer);
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter(JwtVerifier jwtVerifier) {
return new JwtAuthenticationFilter(jwtVerifier);
}
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationFilter jwtFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults())
.build();
}
}
配置说明:
- @EnableMethodSecurity:启用方法级安全控制
- 禁用CSRF:无状态API不需要CSRF保护
- 无状态会话:不创建和使用HttpSession
- 权限规则:
- /public/** 允许匿名访问
- 其他所有请求需要认证
5.2 配置最佳实践
- 路径匹配优化:
java复制.requestMatchers(
HttpMethod.GET, "/public/info", "/public/health").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
- 多种认证方式:
java复制.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults())
.formLogin(login -> login.disable())
- 安全头配置:
java复制.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.sameOrigin())
)
6. 权限控制实战
6.1 控制器示例
java复制@RestController
public class DemoController {
@GetMapping("/me")
public Authentication me(Authentication authentication) {
return authentication;
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminOnly() {
return "Admin area";
}
@PreAuthorize("hasAnyRole('USER', 'VIP')")
@GetMapping("/user")
public String userArea() {
return "User area";
}
@PreAuthorize("#userId == authentication.name")
@GetMapping("/users/{userId}/profile")
public String userProfile(@PathVariable String userId) {
return "Profile of " + userId;
}
}
6.2 权限表达式详解
Spring Security支持丰富的权限表达式:
-
角色检查:
- hasRole('ADMIN'):必须有ADMIN角色
- hasAnyRole('USER','VIP'):有任一角色即可
-
权限检查:
- hasAuthority('READ_PRIVILEGE')
- hasAnyAuthority('READ','WRITE')
-
业务规则:
- #userId == authentication.name:路径变量与当前用户匹配
- @permissionChecker.hasAccess(#projectId):调用自定义权限检查
-
组合表达式:
- @PreAuthorize("hasRole('ADMIN') or hasRole('ROOT')")
- @PreAuthorize("isAuthenticated() and #user.id == principal.id")
7. 生产环境进阶配置
7.1 非对称加密(RS256)
如果认证中心使用RS256算法,需要修改JwtVerifier:
java复制@Bean
JwtVerifier jwtVerifier(@Value("${jwt.public-key}") String publicKeyStr) {
RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(
Base64.getDecoder().decode(publicKeyStr)));
return new JwtVerifier(publicKey, expectedIssuer);
}
// JwtVerifier改造
public JwtClaims verify(String token) {
Claims claims = Jwts.parser()
.verifyWith(publicKey) // 使用公钥验证
.build()
.parseSignedClaims(token)
.getPayload();
// ...解析逻辑不变
}
7.2 JWKS支持
对于动态公钥场景,可以从JWKS端点获取公钥:
java复制@Bean
JwtVerifier jwtVerifier(JwksCache jwksCache) {
return new JwtVerifier(jwksCache, expectedIssuer);
}
// 实现JWKS缓存
public class JwksCache {
private final RestTemplate restTemplate;
private final String jwksUrl;
private volatile Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
public PublicKey getKey(String kid) {
if (!keyCache.containsKey(kid)) {
refreshKeys();
}
return keyCache.get(kid);
}
private synchronized void refreshKeys() {
// 从JWKS端点获取最新公钥
JwksResponse response = restTemplate.getForObject(jwksUrl, JwksResponse.class);
// 解析并更新缓存
// ...
}
}
7.3 自定义用户信息
如果需要携带更多用户信息:
- 创建自定义UserDetails:
java复制public class JwtUser implements UserDetails {
private final String userId;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
// 实现UserDetails方法
// ...
}
- 修改过滤器:
java复制JwtUser user = userService.loadUserBySubject(claims.subject());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
8. 性能优化与监控
8.1 性能考量
-
JWT解析开销:
- HS256验证:约0.1ms
- RS256验证:约1-2ms
- 考虑使用本地缓存已验证的令牌
-
线程安全:
- JwtVerifier应该是无状态的
- 可以配置为单例Bean
-
过滤器链优化:
- 对公开路径添加忽略规则
- 避免不必要的过滤器执行
8.2 监控指标
建议监控以下指标:
- 认证成功率/失败率
- 各种失败原因统计
- JWT验证耗时
- 权限检查耗时
示例使用Micrometer监控:
java复制@Aspect
@Component
@RequiredArgsConstructor
public class SecurityMetricsAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(preAuthorize)")
public Object measureAuthCheck(ProceedingJoinPoint pjp,
PreAuthorize preAuthorize) throws Throwable {
Timer.Sample sample = Timer.start(meterRegistry);
try {
return pjp.proceed();
} finally {
sample.stop(Timer.builder("security.auth.check")
.tag("expression", preAuthorize.value())
.register(meterRegistry));
}
}
}
9. 常见问题排查
9.1 问题排查指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 返回401未授权 | 1. 缺少Authorization头 2. Token过期 3. 签名无效 |
1. 检查请求头格式 2. 检查令牌有效期 3. 验证密钥是否正确 |
| 返回403禁止访问 | 1. 角色不足 2. 权限表达式不匹配 |
1. 检查用户角色 2. 调试权限表达式 |
| 无法注入Authentication | 1. 过滤器未正确配置 2. 安全上下文未传播 |
1. 检查过滤器顺序 2. 确保异步任务正确传播上下文 |
| 角色检查失败 | 1. 角色前缀不匹配 2. 角色名称大小写问题 |
1. 检查ROLE_前缀处理 2. 统一角色命名规范 |
9.2 调试技巧
- 启用调试日志:
properties复制logging.level.org.springframework.security=DEBUG
logging.level.com.example.security=TRACE
- 检查SecurityContext:
java复制SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
// 调试查看auth详情
- 模拟JWT令牌:
java复制String token = Jwts.builder()
.subject("test-user")
.issuer("auth-service")
.claim("roles", List.of("USER"))
.signWith(secretKey)
.compact();
10. 安全最佳实践
10.1 JWT安全建议
-
令牌有效期:
- 访问令牌:15-30分钟
- 刷新令牌:7天
-
敏感数据:
- 不要在JWT中存储敏感信息
- 必要时加密payload
-
令牌撤销:
- 维护令牌黑名单
- 短期令牌减少撤销需求
10.2 防御措施
-
注入攻击防护:
- 严格验证声明值
- 防范JWT头部注入
-
重放攻击防护:
- 使用jti(唯一标识)防止重放
- 短期令牌有效性
-
密钥安全:
- 定期轮换密钥
- 使用HS256时确保密钥强度
10.3 合规要求
-
GDPR:
- 确保JWT不包含个人数据
- 提供用户数据访问接口
-
OAuth2规范:
- 遵循RFC 7519(JWT)
- 实现标准的token introspection
-
审计日志:
- 记录关键认证事件
- 保存令牌签发日志
11. 微服务集成方案
11.1 网关层集成
在API网关统一处理JWT认证:
- 验证令牌有效性
- 转发用户身份到下游服务
- 实现令牌刷新流程
示例Spring Cloud Gateway配置:
java复制public class JwtFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = extractToken(exchange.getRequest());
if (token == null) {
return chain.filter(exchange);
}
return jwtVerifier.verifyReactive(token)
.flatMap(claims -> {
addHeaders(exchange, claims);
return chain.filter(exchange);
})
.onErrorResume(e -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
});
}
}
11.2 服务间通信
服务间调用也需要传递用户身份:
- 使用Feign拦截器:
java复制public class FeignJwtInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getCredentials() instanceof String) {
template.header("Authorization", "Bearer " + auth.getCredentials());
}
}
}
- 异步上下文传播:
java复制@Bean
public Executor asyncExecutor() {
return new DelegatingSecurityContextExecutorService(
Executors.newCachedThreadPool());
}
12. 扩展与定制
12.1 多租户支持
-
租户识别:
- 从JWT声明中提取租户ID
- 存储到SecurityContext
-
数据隔离:
- 自动添加租户查询条件
- 使用Hibernate过滤器
12.2 权限模型扩展
- 基于资源的权限:
java复制@PreAuthorize("@rbac.check(authentication, #projectId, 'READ')")
public Project getProject(String projectId) {
// ...
}
- 动态权限:
java复制public class DynamicPermissionEvaluator
implements PermissionEvaluator {
@Override
public boolean hasPermission(
Authentication auth,
Object target,
Object permission) {
// 自定义权限逻辑
}
}
12.3 响应式支持
WebFlux环境下的实现:
java复制public class JwtWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst("Authorization"))
.filter(h -> h.startsWith("Bearer "))
.map(h -> h.substring(7))
.flatMap(jwtVerifier::verifyReactive)
.flatMap(claims -> {
Authentication auth = createAuthentication(claims);
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put(
ReactiveSecurityContextHolder.withAuthentication(auth)));
})
.switchIfEmpty(chain.filter(exchange));
}
}
13. 测试策略
13.1 单元测试
测试JwtVerifier:
java复制@Test
void verify_validToken_returnsClaims() {
String token = createTestToken();
JwtClaims claims = jwtVerifier.verify(token);
assertEquals("test-user", claims.subject());
assertTrue(claims.roles().contains("USER"));
}
@Test
void verify_invalidToken_throwsException() {
String invalidToken = "invalid.token.here";
assertThrows(Exception.class, () -> jwtVerifier.verify(invalidToken));
}
13.2 集成测试
使用@AutoConfigureMockMvc测试控制器:
java复制@SpringBootTest
@AutoConfigureMockMvc
class DemoControllerTest {
@Test
void adminEndpoint_withoutAuth_returnsUnauthorized() throws Exception {
mockMvc.perform(get("/admin"))
.andExpect(status().isUnauthorized());
}
@Test
void adminEndpoint_withUserRole_returnsForbidden() throws Exception {
String token = createTokenWithRoles("USER");
mockMvc.perform(get("/admin")
.header("Authorization", "Bearer " + token))
.andExpect(status().isForbidden());
}
}
13.3 性能测试
使用JMeter测试:
- 模拟不同并发下的认证性能
- 测量平均响应时间
- 监控系统资源使用情况
14. 部署与运维
14.1 容器化部署
Dockerfile示例:
dockerfile复制FROM eclipse-temurin:17-jre
COPY target/resource-server.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Kubernetes部署:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: resource-server
spec:
replicas: 3
template:
spec:
containers:
- name: server
image: my-registry/resource-server:1.0.0
env:
- name: JWT_HMAC_SECRET
valueFrom:
secretKeyRef:
name: jwt-secrets
key: hmac-secret
14.2 配置管理
-
敏感信息:
- 使用Secret管理密钥
- 通过环境变量注入
-
动态配置:
- 集成配置中心
- 支持热更新
14.3 健康检查
实现健康端点:
java复制@RestController
public class HealthController {
@GetMapping("/actuator/health")
public Health health() {
// 添加自定义健康检查
return Health.up().build();
}
}
15. 升级与迁移
15.1 从Spring Boot 2.x迁移
主要变更点:
- Jakarta EE 9+包名变更
- Spring Security配置调整
- JJWT API变化
迁移步骤:
- 更新包导入(javax → jakarta)
- 调整安全配置类
- 测试认证流程
15.2 从传统会话迁移
迁移策略:
- 并行运行新旧系统
- 逐步切换流量
- 监控认证错误率
15.3 版本升级检查清单
升级时检查:
- 依赖兼容性矩阵
- 废弃API替换
- 安全公告修复
16. 总结与展望
本文详细介绍了基于Spring Security实现JWT认证的完整方案,涵盖了从基础实现到生产级优化的各个方面。通过这个方案,你可以快速为微服务架构构建统一的无状态认证体系。
关键收获:
- 理解了JWT在Spring Security中的处理流程
- 掌握了权限注入和方法级控制的最佳实践
- 学习了生产环境中的各种优化技巧
未来可能的改进方向:
- 与OAuth2授权服务器深度集成
- 支持更细粒度的权限模型
- 实现分布式会话管理
这个方案已经在多个大型生产系统中验证,能够支撑高并发、高可用的认证需求。希望本文能帮助你构建更安全、更灵活的微服务认证体系。