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 刷新机制,可以构建安全可靠的微服务身份认证体系。