Skip to content

OAuth2 集成

OAuth 2.0 是一个授权框架,允许第三方应用程序获得对用户帐户的有限访问权限。Spring Security 提供了全面的 OAuth 2.0 支持,可以轻松实现客户端和资源服务器功能。本文将详细介绍如何在 Spring 应用中集成 OAuth 2.0。

OAuth 2.0 基础

OAuth 2.0 角色

OAuth 2.0 定义了四个关键角色:

  1. 资源所有者(Resource Owner):用户,能够授权访问受保护的资源
  2. 客户端(Client):需要访问用户资源的应用程序
  3. 授权服务器(Authorization Server):验证用户身份并颁发令牌
  4. 资源服务器(Resource Server):托管受保护资源的服务器

OAuth 2.0 授权流程

OAuth 2.0 支持多种授权流程,最常用的包括:

  1. 授权码模式(Authorization Code)

    • 最完整、最安全的流程
    • 适用于服务器端应用
    • 涉及前端和后端的交互
  2. 隐式授权模式(Implicit)

    • 简化的流程,令牌直接返回给客户端
    • 适用于纯前端应用
    • 安全性较低(已被弃用)
  3. 资源所有者密码凭证模式(Resource Owner Password Credentials)

    • 使用用户名和密码直接换取令牌
    • 通常用于第一方应用
    • 要求高度信任
  4. 客户端凭证模式(Client Credentials)

    • 无用户参与,适用于服务间通信
    • 客户端直接使用自己的凭证获取令牌

Spring Security OAuth2 客户端

Spring Security OAuth2 客户端支持将应用程序配置为 OAuth2 或 OpenID Connect 客户端。

添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

配置 OAuth2 登录

1. 基本配置

application.yml 配置文件:

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-google-client-id
            client-secret: your-google-client-secret
            scope: openid,profile,email
          github:
            client-id: your-github-client-id
            client-secret: your-github-client-secret
            scope: user:email,read:user
          facebook:
            client-id: your-facebook-client-id
            client-secret: your-facebook-client-secret
            scope: email,public_profile

2. 安全配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(oauth2UserService())
                )
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );
        
        return http.build();
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
        
        return userRequest -> {
            OAuth2User oauth2User = delegate.loadUser(userRequest);
            
            // 提取注册ID (例如 "google", "github")
            String registrationId = userRequest.getClientRegistration().getRegistrationId();
            
            // 从OAuth2User获取属性
            Map<String, Object> attributes = oauth2User.getAttributes();
            
            // 处理不同提供商的用户信息
            String nameAttributeKey = userRequest.getClientRegistration()
                .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
            
            // 创建自定义的用户主体
            return new DefaultOAuth2User(
                oauth2User.getAuthorities(),
                attributes,
                nameAttributeKey
            );
        };
    }
}

3. 自定义 OAuth2 用户处理

集成到自己的用户系统:

java
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        // 提取OAuth2提供商信息
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oauth2User.getName();
        
        // 提取用户详细信息
        String email = getEmail(oauth2User, provider);
        String name = getName(oauth2User, provider);
        
        // 查找或创建用户
        User user = userRepository.findByEmail(email)
            .orElseGet(() -> createNewUser(email, name, provider, providerId));
        
        // 更新用户信息
        updateUserInfo(user, name, provider, providerId);
        
        // 创建自定义用户主体
        return new CustomOAuth2User(oauth2User, user);
    }
    
    private String getEmail(OAuth2User oauth2User, String provider) {
        // 根据不同提供商提取邮箱
        switch (provider) {
            case "google":
                return oauth2User.getAttribute("email");
            case "github":
                return oauth2User.getAttribute("email");
            case "facebook":
                return oauth2User.getAttribute("email");
            default:
                throw new IllegalArgumentException("Unsupported provider: " + provider);
        }
    }
    
    private String getName(OAuth2User oauth2User, String provider) {
        // 根据不同提供商提取姓名
        switch (provider) {
            case "google":
                return oauth2User.getAttribute("name");
            case "github":
                return oauth2User.getAttribute("name");
            case "facebook":
                return oauth2User.getAttribute("name");
            default:
                throw new IllegalArgumentException("Unsupported provider: " + provider);
        }
    }
    
    private User createNewUser(String email, String name, String provider, String providerId) {
        User user = new User();
        user.setEmail(email);
        user.setName(name);
        user.setProvider(provider);
        user.setProviderId(providerId);
        user.setRole("ROLE_USER");
        user.setEnabled(true);
        user.setCreatedAt(LocalDateTime.now());
        
        return userRepository.save(user);
    }
    
    private void updateUserInfo(User user, String name, String provider, String providerId) {
        user.setName(name);
        user.setProvider(provider);
        user.setProviderId(providerId);
        user.setLastLoginAt(LocalDateTime.now());
        
        userRepository.save(user);
    }
}

// 自定义OAuth2用户类
public class CustomOAuth2User implements OAuth2User {
    
    private final OAuth2User oauth2User;
    private final User user;
    
    public CustomOAuth2User(OAuth2User oauth2User, User user) {
        this.oauth2User = oauth2User;
        this.user = user;
    }
    
    @Override
    public Map<String, Object> getAttributes() {
        return oauth2User.getAttributes();
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }
    
    @Override
    public String getName() {
        return oauth2User.getName();
    }
    
    public String getEmail() {
        return user.getEmail();
    }
    
    public String getUserId() {
        return user.getId().toString();
    }
}

4. 安全配置集成自定义用户服务

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService)
                )
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            );
        
        return http.build();
    }
}

5. 登录页面实现

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    
    <div th:if="${param.error}">
        <p style="color: red;">Authentication failed</p>
    </div>
    
    <div th:if="${param.logout}">
        <p>You have been logged out</p>
    </div>
    
    <h2>Login with OAuth2</h2>
    
    <div>
        <a href="/oauth2/authorization/google">Login with Google</a>
    </div>
    
    <div>
        <a href="/oauth2/authorization/github">Login with GitHub</a>
    </div>
    
    <div>
        <a href="/oauth2/authorization/facebook">Login with Facebook</a>
    </div>
    
    <!-- 可以添加表单登录选项 -->
    <h2>Login with username and password</h2>
    <form th:action="@{/login}" method="post">
        <div>
            <label for="username">Username</label>
            <input type="text" id="username" name="username" required autofocus>
        </div>
        <div>
            <label for="password">Password</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Login</button>
    </form>
</body>
</html>

Spring Security OAuth2 资源服务器

资源服务器保护 API 资源,验证客户端提供的令牌有效性并授权访问。

添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

JWT 令牌验证资源服务器

1. 配置 JWT 令牌验证

application.yml 配置文件:

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 使用授权服务器的 jwk-set-uri 或 issuer-uri
          issuer-uri: https://auth.example.com
          # 或者
          jwk-set-uri: https://auth.example.com/.well-known/jwks.json

2. 资源服务器安全配置

java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/user/**").hasAuthority("SCOPE_read")
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");
        grantedAuthoritiesConverter.setAuthoritiesClaimName("scope");
        
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        
        return jwtAuthenticationConverter;
    }
}

3. 自定义 JWT 令牌解析

java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .jwsAlgorithm(SignatureAlgorithm.RS256)
            .build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(jwt -> {
            List<String> roles = jwt.getClaimAsStringList("roles");
            if (roles == null || roles.isEmpty()) {
                return Collections.emptyList();
            }
            
            return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                .collect(Collectors.toList());
        });
        
        return converter;
    }
}

不透明令牌(Opaque Token)资源服务器

对于某些场景,可能使用非 JWT 的不透明令牌,需要调用授权服务器进行验证。

1. 配置不透明令牌验证

application.yml 配置文件:

yaml
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: https://auth.example.com/oauth2/introspect
          client-id: resource-server
          client-secret: secret

2. 不透明令牌资源服务器配置

java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/user/**").hasAuthority("SCOPE_read")
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspector(opaqueTokenIntrospector())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public OpaqueTokenIntrospector opaqueTokenIntrospector() {
        return new NimbusOpaqueTokenIntrospector(
            "https://auth.example.com/oauth2/introspect",
            "resource-server",
            "secret"
        );
    }
}

Spring Security OAuth2 授权服务器

从 Spring Security 5.x 开始,OAuth2 授权服务器支持已从核心库中移除,官方推荐使用 Spring Authorization Server 项目。

添加 Spring Authorization Server 依赖

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.0.0</version>
</dependency>

基本授权服务器配置

java
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        
        http
            .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());    // 启用 OpenID Connect 1.0
        
        http
            .exceptionHandling(exceptions -> exceptions
                .defaultAuthenticationEntryPointFor(
                    new LoginUrlAuthenticationEntryPoint("/login"),
                    new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                )
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
            
        return http.build();
    }
    
    @Bean
    @Order(2)
    public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/assets/**", "/webjars/**", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
            
        return http.build();
    }
    
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("client")
            .clientSecret("{noop}secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client")
            .redirectUri("http://127.0.0.1:8080/authorized")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(true)
                .build())
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(30))
                .refreshTokenTimeToLive(Duration.ofDays(30))
                .build())
            .build();
            
        return new InMemoryRegisteredClientRepository(client);
    }
    
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())
            .build();
            
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }
    
    private static KeyPair generateRsaKey() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
    
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
    
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }
}

扩展授权服务器配置

1. 自定义用户管理

java
@Configuration
public class UserConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
            
        UserDetails admin = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("password")
            .roles("USER", "ADMIN")
            .build();
            
        return new InMemoryUserDetailsManager(user, admin);
    }
}

2. 使用 JDBC 存储客户端和令牌

java
@Configuration
public class JdbcConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema.sql")
            .build();
    }
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }
    
    @Bean
    public OAuth2AuthorizationService authorizationService(
            JdbcTemplate jdbcTemplate, 
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }
    
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(
            JdbcTemplate jdbcTemplate, 
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }
}

3. 自定义令牌配置

java
@Configuration
public class TokenConfig {

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            JwtClaimsSet.Builder claims = context.getClaims();
            
            if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
                Authentication principal = context.getPrincipal();
                Set<String> authorities = principal.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toSet());
                    
                claims.claim("roles", authorities);
                
                // 添加自定义声明
                if (principal instanceof OAuth2AuthenticationToken) {
                    OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) principal;
                    OAuth2User user = token.getPrincipal();
                    claims.claim("user_id", user.getName());
                } else if (principal instanceof UsernamePasswordAuthenticationToken) {
                    UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) principal;
                    UserDetails user = (UserDetails) token.getPrincipal();
                    claims.claim("user_id", user.getUsername());
                }
            }
        };
    }
    
    @Bean
    public TokenSettings tokenSettings() {
        return TokenSettings.builder()
            .accessTokenTimeToLive(Duration.ofHours(1))
            .refreshTokenTimeToLive(Duration.ofDays(30))
            .reuseRefreshTokens(false)
            .build();
    }
}

使用 OAuth2 的最佳实践

1. 安全配置

  • 使用 HTTPS 保护所有端点
  • 对客户端密钥使用强密码
  • 配置适当的令牌过期时间
  • 使用私有客户端进行前端应用(SPA)授权

2. 令牌处理

  • 在前端应用中安全存储令牌(使用 HttpOnly Cookie)
  • 实现令牌刷新机制
  • 验证令牌中的所有相关声明(颁发者、受众、过期时间)

3. 授权范围

  • 遵循最小权限原则
  • 明确定义应用程序的授权范围
  • 要求用户明确同意授权范围

4. 客户端应用

  • 使用 PKCE (Proof Key for Code Exchange) 增强授权码流程
  • 妥善保管客户端凭证
  • 验证重定向 URI 路径
  • 避免在 URL 中传递敏感信息

常见问题解决

1. 跨域资源共享 (CORS)

java
@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("https://client-app.example.com"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return source;
    }
}

在授权服务器和资源服务器配置中应用:

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 其他配置...
        .cors(cors -> cors.configurationSource(corsConfigurationSource()));
    
    return http.build();
}

2. 错误处理

自定义 OAuth2 错误处理:

java
@RestController
@ControllerAdvice
public class OAuth2ExceptionHandler {

    @ExceptionHandler(OAuth2AuthenticationException.class)
    public ResponseEntity<Map<String, String>> handleOAuth2Exception(OAuth2AuthenticationException ex) {
        Map<String, String> response = new HashMap<>();
        response.put("error", ex.getError().getErrorCode());
        response.put("message", ex.getMessage());
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }
    
    @ExceptionHandler(InvalidTokenException.class)
    public ResponseEntity<Map<String, String>> handleInvalidToken(InvalidTokenException ex) {
        Map<String, String> response = new HashMap<>();
        response.put("error", "invalid_token");
        response.put("message", ex.getMessage());
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }
}

3. 令牌撤销

在授权服务器中实现令牌撤销端点:

java
@RestController
@RequestMapping("/oauth2")
public class TokenController {

    @Autowired
    private OAuth2AuthorizationService authorizationService;
    
    @Autowired
    private OAuth2TokenRevocationAuthenticationProvider revocationAuthenticationProvider;
    
    @PostMapping("/revoke")
    public ResponseEntity<Void> revokeToken(@RequestParam("token") String token,
                                           @RequestParam("token_type_hint") String tokenTypeHint,
                                           @RequestHeader("Authorization") String authorization) {
        
        // 解析客户端凭证
        String[] clientCredentials = extractClientCredentials(authorization);
        String clientId = clientCredentials[0];
        String clientSecret = clientCredentials[1];
        
        // 创建令牌撤销认证请求
        OAuth2TokenRevocationAuthenticationToken revocationAuthentication = 
            new OAuth2TokenRevocationAuthenticationToken(
                token, 
                ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
                clientId, 
                tokenTypeHint
            );
        
        // 处理令牌撤销
        try {
            revocationAuthenticationProvider.authenticate(revocationAuthentication);
            return ResponseEntity.ok().build();
        } catch (OAuth2AuthenticationException ex) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        }
    }
    
    private String[] extractClientCredentials(String authorization) {
        // 从Authorization头中提取客户端凭证
        if (authorization != null && authorization.startsWith("Basic ")) {
            String base64Credentials = authorization.substring("Basic ".length());
            String credentials = new String(Base64.getDecoder().decode(base64Credentials));
            return credentials.split(":", 2);
        }
        throw new OAuth2AuthenticationException(new OAuth2Error("invalid_client"));
    }
}

4. 单点登录 (SSO)

使用 OAuth2 和 OIDC 实现单点登录:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(oidcUserService())
                )
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .permitAll()
            )
            .saml2Login(Customizer.withDefaults());  // 可选:同时支持SAML 2.0
        
        return http.build();
    }
    
    @Bean
    public OidcUserService oidcUserService() {
        OidcUserService oidcUserService = new OidcUserService();
        
        // 自定义 OIDC 用户处理
        oidcUserService.setOidcUserMapper(oidcUser -> {
            Map<String, Object> claims = oidcUser.getClaims();
            Set<GrantedAuthority> authorities = new HashSet<>(oidcUser.getAuthorities());
            
            // 添加额外的权限
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
        });
        
        return oidcUserService;
    }
}

总结

Spring Security OAuth2 提供了全面的支持,使开发人员能够轻松实现 OAuth2 客户端、资源服务器和授权服务器功能。通过正确配置和遵循安全最佳实践,可以构建安全、可扩展的身份验证和授权系统,保护应用程序和 API 资源的安全。

无论是构建需要外部身份提供商登录的 Web 应用程序,还是实现自己的 OAuth2 授权服务器,Spring Security 都提供了所需的工具和抽象,简化开发过程,同时确保高安全标准。