Spring Authorization Server
简介
项目背景
Spring Security OAuth2:
- 已停止维护(2019 年)
- 存在安全漏洞
- 不支持 OAuth2.1
Spring Authorization Server:
- Spring 官方新项目
- 2020 年启动
- 支持 OAuth2.1 和 OIDC
- 基于 Spring Security 5.7+
核心特性
OAuth2.1 支持:
- 授权码模式(+ PKCE)
- 客户端模式
- 刷新令牌
- 令牌撤销
OIDC 支持:
- ID Token
- UserInfo 端点
- JWKS 端点
- 发现端点
扩展性:
- 自定义授权类型
- 自定义令牌格式
- 自定义认证流程
快速开始
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,它更安全、更灵活、更符合标准。
在生产环境中,需要做好安全配置、令牌管理和审计日志,确保授权服务器的安全可靠。