OAuth2 认证基础
OAuth2 简介
核心概念
角色:
- 资源所有者(Resource Owner):用户
- 客户端(Client):第三方应用
- 资源服务器(Resource Server):API 服务
- 授权服务器(Authorization Server):颁发 Token
授权码:
- Authorization Code:授权码
- Access Token:访问令牌
- Refresh Token:刷新令牌
- ID Token:身份令牌(OIDC)
授权模式
授权码模式(Authorization Code):
- 最安全的模式
- 适用于 Web 应用
- 支持 Refresh Token
隐式模式(Implicit):
- 简单但不安全
- 适用于纯前端应用
- 不支持 Refresh Token
密码模式(Resource Owner Password Credentials):
- 用户直接提供密码
- 适用于可信应用
- 逐渐被淘汰
客户端模式(Client Credentials):
- 客户端自身认证
- 适用于服务间调用
- 无用户参与
授权码模式
授权流程
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ Client │ │ Auth Server │ │Resource Svr │
└────┬────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
│ 1. 重定向到授权页面 │ │
│──────────────────────────>│ │
│ │ │
│ 2. 用户登录并授权 │ │
│<──────────────────────────│ │
│ │ │
│ 3. 返回授权码 │ │
│<──────────────────────────│ │
│ │ │
│ 4. 用授权码换取 Token │ │
│──────────────────────────>│ │
│ │ │
│ 5. 返回 Access Token │ │
│<──────────────────────────│ │
│ │ │
│ 6. 携带 Token 访问 API │ │
│──────────────────────────────────────────────────────────>│
│ │ │
│ 7. 验证 Token 并返回数据 │ │
│<──────────────────────────────────────────────────────────│
│ │ │
实现示例
授权请求:
GET /oauth/authorize?
response_type=code&
client_id=client123&
redirect_uri=https://example.com/callback&
scope=read write&
state=xyz123
回调处理:
@Controller
public class OAuth2CallbackController {
@GetMapping("/callback")
public String callback(
@RequestParam String code,
@RequestParam String state
) {
// 验证 state
if (!validateState(state)) {
throw new SecurityException("Invalid state");
}
// 用授权码换取 Token
OAuth2TokenResponse tokenResponse = exchangeCodeForToken(code);
// 保存 Token
saveToken(tokenResponse);
return "redirect:/home";
}
private OAuth2TokenResponse exchangeCodeForToken(String code) {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("code", code);
params.add("client_id", "client123");
params.add("client_secret", "secret456");
params.add("redirect_uri", "https://example.com/callback");
ResponseEntity<OAuth2TokenResponse> response = restTemplate.postForEntity(
"https://auth-server.com/oauth/token",
params,
OAuth2TokenResponse.class
);
return response.getBody();
}
}
Token 响应:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "read write"
}
客户端模式
应用场景
服务间调用:
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
@Value("${payment.service.url}")
private String paymentServiceUrl;
@Value("${oauth.client-id}")
private String clientId;
@Value("${oauth.client-secret}")
private String clientSecret;
public PaymentResult pay(Order order) {
// 获取 Access Token
String accessToken = getAccessToken();
// 调用支付服务
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Order> request = new HttpEntity<>(order, headers);
ResponseEntity<PaymentResult> response = restTemplate.postForEntity(
paymentServiceUrl + "/payments",
request,
PaymentResult.class
);
return response.getBody();
}
private String getAccessToken() {
RestTemplate restTemplate = new RestTemplate();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "client_credentials");
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
ResponseEntity<Map> response = restTemplate.postForEntity(
"https://auth-server.com/oauth/token",
params,
Map.class
);
return (String) response.getBody().get("access_token");
}
}
JWT Token
Token 结构
Header.Payload.Signature
Header:
{
"alg": "RS256",
"typ": "JWT"
}
Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"scope": "read write",
"client_id": "client123"
}
Signature:
RS256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
private_key
)
Token 验证
验证 JWT:
@Component
public class JwtValidator {
@Value("${oauth.public-key}")
private String publicKey;
public boolean validate(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(getPublicKey())
.build()
.parseClaimsJws(token);
// 检查过期时间
Date expiration = claims.getBody().getExpiration();
if (expiration.before(new Date())) {
return false;
}
return true;
} catch (JwtException e) {
return false;
}
}
public Claims getClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getPublicKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getPublicKey() {
return Keys.parsePublicKey(publicKey);
}
}
资源服务器验证:
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri("https://auth-server.com/.well-known/jwks.json")
.build();
decoder.setJwtValidator(JwtValidators.createDefault());
return decoder;
}
}
PKCE 扩展
适用场景
移动端/SPA 应用:
public class PkceUtil {
/**
* 生成 code_verifier
*/
public static String generateCodeVerifier() {
SecureRandom secureRandom = new SecureRandom();
byte[] codeVerifier = new byte[32];
secureRandom.nextBytes(codeVerifier);
return Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier);
}
/**
* 生成 code_challenge
*/
public static String generateCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}
授权请求:
GET /oauth/authorize?
response_type=code&
client_id=client123&
redirect_uri=https://example.com/callback&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Token 请求:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://example.com/callback&
client_id=client123&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
最佳实践
1. 安全配置
HTTPS:
- 所有 OAuth2 通信使用 HTTPS
- 防止中间人攻击
- 保护敏感信息
客户端凭证:
- 保密 Client Secret
- 定期轮换凭证
- 使用环境变量或密钥管理
Token 存储:
// 不推荐:localStorage
localStorage.setItem('access_token', token);
// 推荐:HttpOnly Cookie
Set-Cookie: access_token=<token>; HttpOnly; Secure; SameSite=Strict
2. Token 管理
Token 刷新:
@Service
public class TokenRefreshService {
@Autowired
private RestTemplate restTemplate;
public OAuth2TokenResponse refreshToken(String refreshToken) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "refresh_token");
params.add("refresh_token", refreshToken);
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
ResponseEntity<OAuth2TokenResponse> response = restTemplate.postForEntity(
"https://auth-server.com/oauth/token",
params,
OAuth2TokenResponse.class
);
return response.getBody();
}
}
Token 撤销:
public void revokeToken(String token) {
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", token);
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
restTemplate.postForEntity(
"https://auth-server.com/oauth/revoke",
params,
Void.class
);
}
3. 权限控制
Scope 控制:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/read/**").hasScope("read")
.requestMatchers("/api/write/**").hasScope("write")
.requestMatchers("/api/admin/**").hasScope("admin")
.anyRequest().authenticated()
);
return http.build();
}
}
4. 审计日志
记录认证事件:
@Component
public class OAuth2AuditLogger {
@EventListener
public void onAuthorizationSuccess(OAuth2AuthorizationSuccessEvent event) {
log.info("授权成功:client_id={}, user_id={}, scope={}",
event.getClientId(),
event.getUserId(),
event.getScope()
);
auditLogRepository.save(new AuditLog(
"AUTH_SUCCESS",
event.getClientId(),
event.getUserId(),
LocalDateTime.now()
));
}
@EventListener
public void onAuthorizationFailure(OAuth2AuthorizationFailureEvent event) {
log.warn("授权失败:client_id={}, error={}",
event.getClientId(),
event.getError()
);
auditLogRepository.save(new AuditLog(
"AUTH_FAILURE",
event.getClientId(),
null,
LocalDateTime.now()
));
}
}
总结
OAuth2 是业界标准的授权协议,支持多种授权模式,适用于不同的应用场景。
授权码模式最安全,适用于 Web 应用;客户端模式适用于服务间调用;PKCE 扩展增强了移动端和 SPA 的安全性。
在实际应用中,需要做好 Token 管理、权限控制和审计日志,确保系统安全。