Skip to content
清晨的一缕阳光
返回

OAuth2 认证基础

OAuth2 认证基础

OAuth2 简介

核心概念

角色

授权码

授权模式

授权码模式(Authorization Code)

隐式模式(Implicit)

密码模式(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

客户端凭证

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 管理、权限控制和审计日志,确保系统安全。


分享这篇文章到:

上一篇文章
Spring Boot 性能优化实战
下一篇文章
Spring Boot 对象存储 OSS 集成