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

Spring Authorization Server

Spring Authorization Server

简介

项目背景

Spring Security OAuth2

Spring Authorization Server

核心特性

OAuth2.1 支持

OIDC 支持

扩展性

快速开始

1. 添加依赖

<dependencies>
    <!-- Spring Authorization Server -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    </dependency>
    
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!-- JWT -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

2. 基础配置

server:
  port: 9000

spring:
  security:
    oauth2:
      authorizationserver:
        issuer: http://localhost:9000

3. 安全配置

@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(
        HttpSecurity http
    ) throws Exception {
        
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        
        http
            .getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());  // 启用 OIDC
        
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/assets/**", "/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());  // 启用登录表单
        
        return http.build();
    }
    
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        
        return http.build();
    }
}

4. 用户详情服务

@Configuration
public class UserConfig {
    
    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.withUsername("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();
        
        return new InMemoryUserDetailsManager(user);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5. 客户端配置

@Configuration
public class ClientConfig {
    
    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("client123")
            .clientSecret("{noop}secret456")  // {noop} 表示不加密
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("https://example.com/callback")
            .redirectUri("https://example.com/authorized")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(true)  // 需要用户授权确认
                .requireProofKey(true)  // 需要 PKCE
                .build())
            .build();
        
        // 保存到数据库
        JdbcRegisteredClientRepository registeredClientRepository = 
            new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(registeredClient);
        
        return registeredClientRepository;
    }
}

核心组件

1. 授权服务器配置

OAuth2AuthorizationServerConfigurer

@Configuration
public class ServerConfig {
    
    @Bean
    public OAuth2AuthorizationServerConfiguration authorizationServerConfig(
        OAuth2AuthorizationService authorizationService,
        OAuth2TokenService tokenService,
        OAuth2ClientAuthenticationService clientAuthenticationService
    ) {
        return http -> {
            http
                .authorizationService(authorizationService)
                .tokenService(tokenService)
                .clientAuthenticationService(clientAuthenticationService)
                .authorizationEndpoint(authorizationEndpoint -> 
                    authorizationEndpoint.consentPage("/oauth2/consent"))
                .tokenEndpoint(tokenEndpoint -> 
                    tokenEndpoint.accessTokenRequestConverter(
                        new OAuth2AccessTokenRequestConverter()
                    )
                );
        };
    }
}

2. JWT 配置

JWT 编码器

@Configuration
public class JwtConfig {
    
    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }
    
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = Jwks.generateRsa();
        
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }
    
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
}

自定义 JWT Claims

@Configuration
public class JwtClaimsConfig {
    
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
        return context -> {
            if (context.getTokenType().getValue().equals(TokenType.ACCESS_TOKEN.getValue())) {
                ClaimsSet claims = context.getClaims();
                
                // 添加自定义 Claims
                claims.claim("user_id", context.getPrincipal().getName());
                claims.claim("roles", getRoles(context.getPrincipal()));
            }
        };
    }
    
    private List<String> getRoles(Principal principal) {
        // 获取用户角色
        return Arrays.asList("USER", "ADMIN");
    }
}

3. 授权端点

自定义授权页面

@Controller
public class AuthorizationConsentController {
    
    private final TemplateEngine templateEngine;
    
    public AuthorizationConsentController(TemplateEngine templateEngine) {
        this.templateEngine = templateEngine;
    }
    
    @GetMapping("/oauth2/consent")
    public String consent(
        @RequestParam("client_id") String clientId,
        @RequestParam("scope") String scope,
        @RequestParam("state") String state,
        Model model
    ) {
        model.addAttribute("clientId", clientId);
        model.addAttribute("scope", scope);
        model.addAttribute("state", state);
        
        return "consent";
    }
    
    @PostMapping("/oauth2/consent")
    public String consentResponse(
        @RequestParam("client_id") String clientId,
        @RequestParam("scope") String scope,
        @RequestParam("state") String state,
        @RequestParam(value = "user_consent", defaultValue = "false") boolean userConsent
    ) {
        if (!userConsent) {
            return "redirect:/oauth2/authorize?error=access_denied";
        }
        
        return "redirect:/oauth2/authorize?client_id=" + clientId + 
               "&scope=" + scope + "&state=" + state;
    }
}

4. 令牌管理

令牌服务

@Configuration
public class TokenServiceConfig {
    
    @Bean
    public OAuth2TokenService tokenService(
        JwtEncoder jwtEncoder,
        OAuth2AuthorizationService authorizationService
    ) {
        return new DelegatingOAuth2TokenService(
            new JwtGenerator(jwtEncoder),
            new RefreshTokenGenerator(),
            new OAuth2AccessTokenGenerator()
        );
    }
    
    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate);
    }
}

令牌撤销

@RestController
public class TokenRevocationController {
    
    @Autowired
    private OAuth2AuthorizationService authorizationService;
    
    @PostMapping("/oauth2/revoke")
    public ResponseEntity<Void> revokeToken(
        @RequestParam String token,
        @RequestParam String clientId
    ) {
        // 查找授权
        OAuth2Authorization authorization = authorizationService.findByToken(token);
        
        if (authorization != null) {
            // 撤销授权
            authorizationService.remove(authorization);
        }
        
        return ResponseEntity.ok().build();
    }
}

OIDC 支持

1. OpenID Connect 配置

@Configuration
public class OidcConfig {
    
    @Bean
    public ProviderSettings providerSettings() {
        return ProviderSettings.builder()
            .issuer("http://localhost:9000")
            .build();
    }
    
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> oidcTokenCustomizer() {
        return context -> {
            if (context.getTokenType().getValue().equals(TokenType.ID_TOKEN.getValue())) {
                ClaimsSet claims = context.getClaims();
                
                // 添加 OIDC Claims
                claims.claim("email", context.getPrincipal().getAttribute("email"));
                claims.claim("name", context.getPrincipal().getAttribute("name"));
            }
        };
    }
}

2. UserInfo 端点

@Configuration
@EnableWebSecurity
public class OidcSecurityConfig {
    
    @Bean
    @Order(2)
    public SecurityFilterChain oidcSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/userinfo")
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        
        return http.build();
    }
}

@RestController
public class UserInfoController {
    
    @GetMapping("/userinfo")
    public ResponseEntity<Map<String, Object>> userinfo(OAuth2Authentication authentication) {
        Map<String, Object> claims = new HashMap<>();
        
        claims.put("sub", authentication.getName());
        claims.put("email", authentication.getPrincipal().getAttribute("email"));
        claims.put("name", authentication.getPrincipal().getAttribute("name"));
        
        return ResponseEntity.ok(claims);
    }
}

3. 发现端点

GET /.well-known/openid-configuration

Response:
{
  "issuer": "http://localhost:9000",
  "authorization_endpoint": "http://localhost:9000/oauth2/authorize",
  "token_endpoint": "http://localhost:9000/oauth2/token",
  "userinfo_endpoint": "http://localhost:9000/userinfo",
  "jwks_uri": "http://localhost:9000/oauth2/jwks",
  "response_types_supported": ["code", "token", "id_token"],
  "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic"],
  ...
}

数据库配置

1. Schema 初始化

-- OAuth2 客户端表
CREATE TABLE oauth2_registered_client (
    id VARCHAR(100) NOT NULL,
    client_id VARCHAR(100) NOT NULL,
    client_id_issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    client_secret VARCHAR(200) DEFAULT NULL,
    client_secret_expires_at TIMESTAMP DEFAULT NULL,
    client_name VARCHAR(200) NOT NULL,
    client_authentication_methods VARCHAR(1000) NOT NULL,
    authorization_grant_types VARCHAR(1000) NOT NULL,
    redirect_uris VARCHAR(1000) DEFAULT NULL,
    scopes VARCHAR(1000) NOT NULL,
    client_settings VARCHAR(2000) NOT NULL,
    token_settings VARCHAR(2000) NOT NULL,
    PRIMARY KEY (id)
);

-- OAuth2 授权表
CREATE TABLE oauth2_authorization (
    id VARCHAR(100) NOT NULL,
    registered_client_id VARCHAR(100) NOT NULL,
    principal_name VARCHAR(200) NOT NULL,
    authorization_grant_type VARCHAR(100) NOT NULL,
    authorized_scopes VARCHAR(1000) DEFAULT NULL,
    attributes BLOB DEFAULT NULL,
    state VARCHAR(500) DEFAULT NULL,
    authorization_code_value BLOB DEFAULT NULL,
    authorization_code_issued_at TIMESTAMP DEFAULT NULL,
    authorization_code_expires_at TIMESTAMP DEFAULT NULL,
    authorization_code_metadata BLOB DEFAULT NULL,
    access_token_value BLOB DEFAULT NULL,
    access_token_issued_at TIMESTAMP DEFAULT NULL,
    access_token_expires_at TIMESTAMP DEFAULT NULL,
    access_token_metadata BLOB DEFAULT NULL,
    access_token_type VARCHAR(100) DEFAULT NULL,
    access_token_scopes VARCHAR(1000) DEFAULT NULL,
    refresh_token_value BLOB DEFAULT NULL,
    refresh_token_issued_at TIMESTAMP DEFAULT NULL,
    refresh_token_expires_at TIMESTAMP DEFAULT NULL,
    refresh_token_metadata BLOB DEFAULT NULL,
    PRIMARY KEY (id)
);

2. 数据源配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/authorization_server
    username: root
    password: root
  jdbc:
    initialize-schema: always

最佳实践

1. 安全配置

密码加密

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // 增加强度
}

// 客户端密码加密
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
    .clientId("client123")
    .clientSecret(passwordEncoder.encode("secret456"))  // 加密存储
    .build();

HTTPS 配置

server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: password
    key-store-type: PKCS12

2. 令牌管理

令牌有效期

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
    return context -> {
        if (context.getTokenType().getValue().equals(TokenType.ACCESS_TOKEN.getValue())) {
            context.getClaims().expiresAt(Instant.now().plus(1, ChronoUnit.HOURS));
        }
        if (context.getTokenType().getValue().equals(TokenType.REFRESH_TOKEN.getValue())) {
            context.getClaims().expiresAt(Instant.now().plus(7, ChronoUnit.DAYS));
        }
    };
}

3. 审计日志

@Component
public class AuthorizationEventLogger {
    
    @EventListener
    public void onAuthorizationSuccess(OAuth2AuthorizationSuccessEvent event) {
        log.info("授权成功:client={}, user={}",
            event.getAuthorization().getRegisteredClientId(),
            event.getAuthorization().getPrincipalName()
        );
    }
    
    @EventListener
    public void onAuthorizationFailure(OAuth2AuthorizationFailureEvent event) {
        log.warn("授权失败:client={}, error={}",
            event.getClientId(),
            event.getError()
        );
    }
}

4. 监控告警

@Component
public class AuthorizationMetrics {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    @EventListener
    public void onAuthorizationSuccess(OAuth2AuthorizationSuccessEvent event) {
        meterRegistry.counter("oauth2.authorization.success",
            "client", event.getAuthorization().getRegisteredClientId()
        ).increment();
    }
    
    @EventListener
    public void onAuthorizationFailure(OAuth2AuthorizationFailureEvent event) {
        meterRegistry.counter("oauth2.authorization.failure",
            "client", event.getClientId(),
            "error", event.getError().getErrorCode()
        ).increment();
    }
}

总结

Spring Authorization Server 是 Spring 官方的 OAuth2 授权服务器实现,提供完整的 OAuth2.1 和 OIDC 支持。

相比旧的 Spring Security OAuth2,它更安全、更灵活、更符合标准。

在生产环境中,需要做好安全配置、令牌管理和审计日志,确保授权服务器的安全可靠。


分享这篇文章到:

上一篇文章
Spring Boot WebSocket 实时通信
下一篇文章
Spring Boot WebFlux 响应式编程