OAuth2 + JWT 微服务认证方案深度解析
一、问题背景
微服务架构下,认证授权面临多重挑战:
- 无状态认证:服务间共享 session 困难
- Token 安全:如何防止 Token 被伪造或篡改
- 权限控制:细粒度的接口权限管理
- 跨域问题:网关统一认证后如何传递用户信息
OAuth2 + JWT 是目前主流的微服务认证方案,结合网关统一鉴权,可以构建安全可靠的身份认证体系。
二、OAuth2 四种授权模式
2.1 授权码模式
最安全的授权模式,适合有后端的应用。
资源服务器 客户端应用 认证服务器 浏览器 用户 资源服务器 客户端应用 认证服务器 浏览器 用户 访问受保护资源 重定向到授权页面 请求授权 显示授权确认页 用户确认授权 返回授权码 传递授权码 用授权码换 Token 返回 Access Token 用 Token 请求资源 返回资源
2.2 密码凭证模式
适用于受信任的第一方应用。
认证服务器 受信任应用 用户 认证服务器 受信任应用 用户 输入用户名密码 用密码换取 Token 返回 Access Token 登录成功
2.3 客户端凭证模式
适用于服务间调用,不涉及用户。
服务 B 认证服务器 服务 A 服务 B 认证服务器 服务 A 用客户端凭证换取 Token 返回 Access Token 用 Token 调用接口 返回资源
2.4 简化模式
不返回 Refresh Token,适合纯前端应用。
三、JWT 结构解析
3.1 JWT 组成
Header.Payload.Signature
json
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"roles": ["admin", "user"]
}
// Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
3.2 JJWT 解析示例
java
public class JwtUtils {
private final SecretKey secretKey;
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException e) {
return false;
}
}
public boolean isTokenExpired(String token) {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
}
}
3.3 JWT vs Session
| 维度 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 扩展性 | 无状态,易扩展 | 需要 Redis 共享 |
| 安全性 | 签名防篡改 | Cookie HttpOnly |
| 失效机制 | 无法主动失效 | 可立即失效 |
| 体积 | 较小 | 较小 |
| 适用场景 | 跨域、微服务 | 单体、有状态 |
四、Spring Security OAuth2
4.1 Resource Server 配置
java
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
4.2 JWT 配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(
"http://auth-server/oauth2/token"
);
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthorityConverter grantedAuthorityConverter =
new JwtGrantedAuthorityConverter();
grantedAuthorityConverter.setAuthoritiesClaimName("roles");
grantedAuthorityConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
grantedAuthorityConverter
);
return jwtAuthenticationConverter;
}
}
4.3 安全过滤器链
JWT 验证
Security Filter Chain
WebSecurity
SecurityContext
Authentication
Authorization
JwtDecoder
JwtParser
Claims
五、Token 校验流程
5.1 网关统一鉴权
业务服务 认证服务器 API 网关 客户端 业务服务 认证服务器 API 网关 客户端 alt [本地校验] [远程校验] 请求 /api/user {Token} 提取 Token JWT 签名验证 过期检查 Claims 提取 校验 Token 有效性 返回用户信息 注入用户信息到 Header 转发请求 {X-User-Id, X-User-Roles} 权限校验 返回资源 返回响应
5.2 全局过滤器实现
java
@Component
public class JwtAuthenticationFilter implements GlobalFilter {
private final JwtUtils jwtUtils;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 跳过白名单路径
if (isWhiteListed(path)) {
return chain.filter(exchange);
}
String token = extractToken(exchange);
if (StringUtils.isEmpty(token)) {
return unauthorized(exchange);
}
try {
Claims claims = jwtUtils.parseToken(token);
// 验证过期
if (jwtUtils.isTokenExpired(token)) {
return unauthorized(exchange);
}
// 注入用户信息到 Header
ServerHttpRequest modifiedRequest = exchange.getRequest()
.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Roles",
String.join(",", claims.get("roles", List.class)))
.build();
return chain.filter(
exchange.mutate().request(modifiedRequest).build()
);
} catch (Exception e) {
return unauthorized(exchange);
}
}
private String extractToken(ServerWebExchange exchange) {
String bearerToken = exchange.getRequest()
.getHeaders().getFirst("Authorization");
if (StringUtils.hasText(bearerToken)
&& bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(
HttpStatus.UNAUTHORIZED
);
return exchange.getResponse().setComplete();
}
}
六、微服务间认证
6.1 Feign 传递 Token
java
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 从当前请求上下文获取 Token
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (attributes != null) {
String token = attributes.getRequest()
.getHeader("Authorization");
if (StringUtils.hasText(token)) {
template.header("Authorization", token);
}
}
}
};
}
}
6.2 服务间调用安全
java
@Configuration
public class ServiceAuthConfig {
@Bean
public Feign.Builder serviceAuthFeignBuilder() {
return Feign.builder()
.requestInterceptor(new ServiceTokenInterceptor())
.errorDecoder(new ServiceAuthErrorDecoder());
}
}
public class ServiceTokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 服务间调用使用客户端凭证模式
String token = obtainServiceToken();
template.header("Authorization", "Bearer " + token);
}
}
七、Token 刷新机制
7.1 Refresh Token 流程
认证服务器 API 网关 客户端 认证服务器 API 网关 客户端 alt [Token 即将过期] 请求 /api/resource {Access Token} Token 过期检查 返回 401 + refresh_required POST /auth/refresh {Refresh Token} 用 Refresh Token 换取新 Token 返回新 Access Token + 新 Refresh Token 返回新 Token
7.2 Token 刷新实现
java
@Component
public class TokenRefreshService {
private final JwtUtils jwtUtils;
private final RedisTemplate<String, String> redisTemplate;
public AuthToken refreshToken(String refreshToken) {
// 验证 Refresh Token
Claims claims = jwtUtils.parseToken(refreshToken);
// 检查 Token 类型
String tokenType = claims.get("token_type", String.class);
if (!"refresh".equals(tokenType)) {
throw new InvalidTokenException("Invalid token type");
}
// 生成新的 Access Token
String newAccessToken = jwtUtils.generateAccessToken(
claims.getSubject(),
claims.get("roles", List.class)
);
// 生成新的 Refresh Token
String newRefreshToken = jwtUtils.generateRefreshToken(
claims.getSubject()
);
// 旧 Refresh Token 加入黑名单
redisTemplate.opsForValue().set(
"blacklist:refresh:" + refreshToken,
"1",
Duration.ofDays(7)
);
return new AuthToken(newAccessToken, newRefreshToken);
}
}
7.3 Token 黑名单机制
java
@Component
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
public void blacklistToken(String token, long expirationSeconds) {
redisTemplate.opsForValue().set(
"blacklist:token:" + token,
"1",
Duration.ofSeconds(expirationSeconds)
);
}
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(
redisTemplate.hasKey("blacklist:token:" + token)
);
}
}
八、实战演示
8.1 完整的认证服务
java
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserService userService;
@PostMapping("/login")
public AuthToken login(@RequestBody LoginRequest request) {
// 验证用户
User user = userService.authenticate(
request.getUsername(),
request.getPassword()
);
// 生成 Token
String accessToken = jwtUtils.generateAccessToken(
user.getId(),
user.getRoles()
);
String refreshToken = jwtUtils.generateRefreshToken(
user.getId()
);
return new AuthToken(accessToken, refreshToken);
}
@PostMapping("/refresh")
public AuthToken refresh(@RequestBody RefreshRequest request) {
// 验证 Refresh Token
return tokenRefreshService.refreshToken(
request.getRefreshToken()
);
}
@PostMapping("/logout")
public void logout(HttpServletRequest request) {
String token = extractToken(request);
long expiration = jwtUtils.getExpiration(token);
// 加入黑名单
tokenBlacklistService.blacklistToken(token, expiration);
}
}
8.2 网关路由配置
yaml
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
8.3 网关全局安全配置
java
@Configuration
public class GatewaySecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.authorizeExchange()
.pathMatchers("/auth/**").permitAll()
.pathMatchers("/actuator/**").permitAll()
.pathMatchers("/api/public/**").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
}
九、避坑指南
坑 1:JWT 存储安全
问题:Token 被 XSS 攻击窃取
解决:
javascript
// 使用 HttpOnly Cookie 存储 Refresh Token
// Access Token 存储在内存中
const tokenStore = {
accessToken: null,
setAccessToken(token) {
this.accessToken = token;
}
};
坑 2:Token 泄露风险
问题:日志中打印 Token
解决:
java
// 过滤器中清除敏感信息
exchange.getLog().addAttribute("Token", "***");
坑 3:跨域配置冲突
问题:CORS 配置与 Security 冲突
解决:
java
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
坑 4:分布式 Session 问题
问题:多实例部署时用户状态不一致
解决:使用 Token 而非 Session,或使用分布式 Session 存储
坑 5:Token 续期策略
问题:频繁刷新 Token 影响体验
解决:采用"滑动窗口"续期
java
// Access Token 剩余有效期 < 30 分钟时刷新
if (claims.getExpiration().getTime() - System.currentTimeMillis()
< 30 * 60 * 1000) {
// 自动刷新
}
十、JWK Set URI 配置
10.1 JWK Set 概念
JWK Set 是一组 JSON Web Key,用于存储公钥信息,允许客户端验证 JWT 签名而无需共享密钥。
json
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "key-id-1",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
10.2 Spring Security 配置
yaml
spring:
security:
oauth2:
resourceserver:
jwt:
# 方式1: Issuer URI (自动获取 JWK Set)
issuer-uri: http://auth-server/oauth2
# 方式2: 显式指定 JWK Set URI
jwk-set-uri: http://auth-server/.well-known/jwks.json
10.3 自定义 JWK 配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public JwtDecoder jwtDecoder() {
// 从 JWK Set URI 获取公钥
return JwtDecoders.fromJwkSetUri(
"http://auth-server/.well-known/jwks.json"
);
}
@Bean
public NimbusJwtDecoder jwtDecoder(JwkKeyAccessor jwkKeyAccessor) {
return NimbusJwtDecoder.withJwkSetUri(
"http://auth-server/.well-known/jwks.json"
).jwkSetUri(jwkKeyAccessor).build();
}
}
10.4 JWK 缓存机制
java
@Component
public class JwkCacheManager {
private final Map<String, JwkCacheEntry> cache = new ConcurrentHashMap<>();
public JWK getJwk(String kid) {
JwkCacheEntry entry = cache.get(kid);
if (entry == null || entry.isExpired()) {
entry = fetchJwkFromServer(kid);
cache.put(kid, entry);
}
return entry.getJwk();
}
private JwkCacheEntry fetchJwkFromServer(String kid) {
// 从 JWK Set 获取指定 kid 的密钥
return new JwkCacheEntry(jwk, Duration.ofHours(1));
}
}
十一、PKCE 授权码模式
11.1 PKCE 流程
PKCE(Proof Key for Code Exchange)是 OAuth 2.0 的安全扩展,防止授权码被拦截。
User Auth Server Client App User Auth Server Client App 生成 code_verifier (随机字符串) 计算 code_challenge = BASE64URL(SHA256(code_verifier)) 授权请求 + code_challenge + method=S256 显示登录页面 用户登录 返回授权码 用 code + code_verifier 换 Token 验证 code_challenge 返回 Access Token
11.2 代码实现
java
@Service
public class PkceAuthService {
private static final SecureRandom secureRandom = new SecureRandom();
/**
* 生成 code_verifier
*/
public String generateCodeVerifier() {
byte[] buffer = new byte[32];
secureRandom.nextBytes(buffer);
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(buffer);
}
/**
* 生成 code_challenge
*/
public String generateCodeChallenge(String codeVerifier) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* 发起 PKCE 授权请求
*/
public void authorize() {
String codeVerifier = generateCodeVerifier();
String codeChallenge = generateCodeChallenge(codeVerifier);
// 保存 code_verifier 用于后续验证
session.setAttribute("code_verifier", codeVerifier);
// 构建授权 URL
String authUrl = UriComponentsBuilder
.fromHttpUrl("http://auth-server/oauth/authorize")
.queryParam("response_type", "code")
.queryParam("client_id", "client-id")
.queryParam("redirect_uri", "http://localhost:8080/callback")
.queryParam("scope", "read write")
.queryParam("code_challenge", codeChallenge)
.queryParam("code_challenge_method", "S256")
.build()
.toUriString();
// 重定向到授权服务器
return "redirect:" + authUrl;
}
/**
* 用 code 换 Token
*/
public AuthToken exchangeToken(String code) {
String codeVerifier = session.getAttribute("code_verifier");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code", code);
params.add("redirect_uri", "http://localhost:8080/callback");
params.add("client_id", "client-id");
params.add("code_verifier", codeVerifier);
// 用 code_verifier 换取 Token
return restTemplate.postForObject(
"http://auth-server/oauth/token",
params,
AuthToken.class
);
}
}
11.3 Spring Security OAuth2 PKCE 支持
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.authorizationRequestRepository(
new HttpSessionAuthorizationRequestRepository()
)
)
.redirectionEndpoint(endpoint -> endpoint
.baseUri("/oauth2/callback/*")
)
.tokenEndpoint(token -> token
.accessTokenResponseClient(
OAuth2AccessTokenResponseClient
.withDefaults()
)
)
);
return http.build();
}
}
十二、Token 设备指纹绑定
12.1 指纹采集
java
@Component
public class DeviceFingerprintService {
public String generateFingerprint(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
// User Agent
sb.append(request.getHeader("User-Agent"));
sb.append("|");
// Accept Language
sb.append(request.getHeader("Accept-Language"));
sb.append("|");
// Client IP
sb.append(getClientIp(request));
sb.append("|");
// Screen Resolution (前端传入)
sb.append(request.getHeader("X-Screen-Resolution"));
sb.append("|");
// Timezone
sb.append(request.getHeader("X-Timezone"));
// 计算 Hash
return sha256(sb.toString());
}
private String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
12.2 Token 绑定验证
java
@Component
public class TokenBindingValidator {
@Autowired
private DeviceFingerprintService fingerprintService;
public boolean validateBinding(String token, HttpServletRequest request) {
Claims claims = jwtUtils.parseToken(token);
String tokenFingerprint = claims.get("fingerprint", String.class);
String currentFingerprint = fingerprintService.generateFingerprint(request);
// 允许小幅度的指纹变化(如浏览器更新)
return similar(tokenFingerprint, currentFingerprint, 0.8);
}
private boolean similar(String a, String b, double threshold) {
if (a == null || b == null) return false;
// 使用 Jaro-Winkler 距离
double distance = jaroWinklerSimilarity(a, b);
return distance >= threshold;
}
}
12.3 异常处理
java
@Component
public class TokenBindingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = extractToken(request);
if (token != null) {
if (!tokenBindingValidator.validateBinding(token, request)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("{\"error\": \"device_mismatch\"}");
return false;
}
}
return true;
}
}
十三、总结
OAuth2 + JWT 是微服务认证的最佳实践:
| 维度 | 内容 |
|---|---|
| OAuth2 模式 | 授权码、密码、客户端凭证、简化模式 |
| JWT 结构 | Header、Payload、Signature |
| 签名算法 | HS256 对称、RS256 非对称 |
| JWK Set | 公钥分发、动态密钥轮换 |
| PKCE | 授权码安全扩展、防止拦截攻击 |
| Spring Security | ResourceServer + JWT 集成 |
| 网关鉴权 | GlobalFilter 统一 Token 校验 |
| 服务间认证 | Feign 拦截器传递 Token |
| Token 管理 | 刷新机制 + 黑名单 + 设备指纹 |
| 安全增强 | 设备绑定、异常检测 |
结合网关统一鉴权、服务间 Token 传递、完善的 Token 刷新机制,可以构建安全可靠的微服务身份认证体系。