Spring Authorization Server 迁移至 Spring Security 7.0:变革与展望
引言
2025年,Spring 社区迎来了一个重要的架构变革:Spring Authorization Server 正式迁移至 Spring Security 7.0。这一变更标志着 OAuth2 和 OpenID Connect 支持将完全集成到 Spring Security 的核心模块中,不再作为独立项目存在。
本文将深入探讨此次迁移带来的变化、使用方法,并对 OAuth2 和 OpenID Connect 1.0 协议进行详细对比。
一、迁移背景与架构变革
1.1 为什么要合并?
Spring Authorization Server 最初作为独立项目创建,是为了填补 Spring Security OAuth2 项目停止维护后的空白。然而,随着时间的推移:
- 统一架构:将 OAuth2 功能集成到 Spring Security 核心,可以提供更统一的开发体验
- 维护效率:单一项目更容易维护和更新,减少版本同步问题
- 社区需求:开发者希望在一个依赖中获得完整的安全解决方案
1.2 主要架构变化
之前(Spring Authorization Server 独立项目):
xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.1.x</version>
</dependency>
之后(Spring Security 7.0):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.4+</version>
</dependency>
<!-- OAuth2 支持已内置,无需额外依赖 -->
二、包名与配置迁移
2.1 包名重构
最显著的变化是包名的调整,所有类从 org.springframework.security.oauth2.server.authorization 迁移至 org.springframework.security.oauth2.core 及其子包。
| 旧包名(1.1.x) | 新包名(7.0) | 说明 |
|---|---|---|
org.springframework.security.oauth2.server.authorization |
org.springframework.security.oauth2.core |
核心模型和接口 |
org.springframework.security.oauth2.server.authorization.client |
org.springframework.security.oauth2.client |
客户端相关 |
org.springframework.security.oauth2.server.authorization.resource |
org.springframework.security.oauth2.resource |
资源服务器 |
2.2 配置类变更
旧的配置方式:
java
@Configuration
public class AuthorizationServerConfig {
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:9000")
.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId("client-1")
.clientId("client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/client")
.scope(OidcScopes.OPENID)
.scope("read")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
}
Spring Security 7.0 新配置方式:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2AuthorizationServer(authz -> authz
.authorizationEndpoint(endpoint -> endpoint
.consentUri("/oauth2/consent")
)
.tokenEndpoint(endpoint -> endpoint
.accessTokenGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.accessTokenGrantType(AuthorizationGrantType.REFRESH_TOKEN)
)
.oidc(oidc -> oidc
.providerConfigurationEndpoint(endpoint ->
endpoint.providerConfigurationCustomizer(customizer))
)
);
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient client = RegisteredClient.withId("client-1")
.clientId("client")
.clientSecret("{bcrypt}$2a$10$...")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/client")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.requireProofKey(true)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30))
.refreshTokenTimeToLive(Duration.ofDays(1))
.reuseRefreshTokens(false)
.build())
.build();
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
}
三、核心功能增强
3.1 统一的过滤器链
Spring Security 7.0 将授权服务器的过滤器完全集成到主过滤器链中:
java
@Bean
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
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
// 处理自定义登录页面
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt(Customizer.withDefaults())
);
return http.build();
}
3.2 增强的 Token 存储
新版本默认使用 JDBC 持久化,提供更好的生产环境支持:
java
@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.3 PKCE 支持默认启用
Proof Key for Code Exchange (PKCE) 现在默认启用,增强了公共客户端的安全性:
java
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // 强制要求 PKCE
.build())
四、OAuth2 与 OpenID Connect 1.0 深度对比
4.1 协议层次关系
┌─────────────────────────────────────────────────┐
│ OpenID Connect 1.0 (身份层) │
│ ┌─────────────────────────────────────────────┐ │
│ │ OAuth 2.0 (授权层) │ │
│ │ ┌────────────────────────────────────────┐ │ │
│ │ │ HTTP / HTTPS │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
核心区别:
- OAuth 2.0 是一个授权框架,解决的是"应用 A 能否访问用户在资源服务器 B 上的资源"的问题
- OpenID Connect (OIDC) 是在 OAuth 2.0 之上的身份认证层,解决的是"用户是谁"的问题
4.2 功能对比矩阵
| 特性 | OAuth 2.0 | OpenID Connect 1.0 |
|---|---|---|
| 主要用途 | 授权(Delegated Authorization) | 身份认证(Authentication) |
| 核心问题 | "第三方应用能访问我的资源吗?" | "用户是谁?" |
| 返回内容 | Access Token | ID Token + Access Token |
| 标准化 | Access Token 格式不标准 | ID Token 标准(JWT) |
| 用户信息 | 需要额外 API 调用 | ID Token 包含基础用户信息 |
| 发现机制 | 无 | 标准化发现端点 |
| 会话管理 | 无 | Session Management |
| 退出登录 | 手动实现 | 标准化 RP-Initiated Logout |
4.3 Token 对比
OAuth 2.0 Access Token:
json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
OpenID Connect 响应(包含 ID Token):
json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJzdWIiOiJ1c2VyMTIzIiwiYXVkIjoiY2xpZW50X2lkIiwiZXhwIjoxNzM1NDQxNjAwLCJpYXQiOjE3MzUzOTg0MDAsIm5vbmNlIjoibi0wUjZfcTRGQiIsImF0X2hhc2giOiJGdkx4Y1JvTWJ5WGFpZk52NnRlWjJRIiwiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJKb2huIERvZSJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email",
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}
ID Token 解码后(JWT 格式):
json
{
"iss": "http://localhost:9000", // Issuer
"sub": "user123", // Subject (用户唯一标识)
"aud": "client_id", // Audience
"exp": 1735441600, // Expiration
"iat": 1735398400, // Issued At
"nonce": "n-0R6_q4FB", // Nonce (防重放)
"at_hash": "FvLxcRoMbyXaifNv6teZ2Q", // Access Token hash
"email": "user@example.com",
"email_verified": true,
"name": "John Doe"
}
4.4 使用场景对比
OAuth 2.0 适用场景:
java
// 场景:第三方应用需要访问用户的 Google 相册
GET https://photos.googleapis.com/v1/albums
Authorization: Bearer <access_token>
OpenID Connect 适用场景:
java
// 场景:单点登录(SSO)- 验证用户身份并获取基本信息
// 1. 发现端点获取配置
GET https://auth.example.com/.well-known/openid-configuration
// 响应包含:
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/oauth2/authorize",
"token_endpoint": "https://auth.example.com/oauth2/token",
"userinfo_endpoint": "https://auth.example.com/oauth2/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"end_session_endpoint": "https://auth.example.com/oauth2/logout"
}
// 2. 验证 ID Token 获取用户身份
// ID Token 的 sub 声明是用户的全局唯一标识
五、Spring Security 7.0 完整配置示例
5.1 数据库初始化脚本
sql
-- 客户端注册表
CREATE TABLE oauth2_registered_client (
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
client_secret varchar(200),
client_secret_expires_at TIMESTAMP,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000),
post_logout_redirect_uris varchar(1000),
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),
attributes TEXT,
state varchar(500),
authorization_code_value TEXT,
authorization_code_issued_at TIMESTAMP,
authorization_code_expires_at TIMESTAMP,
authorization_code_metadata TEXT,
access_token_value TEXT,
access_token_issued_at TIMESTAMP,
access_token_expires_at TIMESTAMP,
access_token_metadata TEXT,
access_token_type varchar(100),
refresh_token_value TEXT,
refresh_token_issued_at TIMESTAMP,
refresh_token_expires_at TIMESTAMP,
refresh_token_metadata TEXT,
oidc_id_token_value TEXT,
oidc_id_token_issued_at TIMESTAMP,
oidc_id_token_expires_at TIMESTAMP,
oidc_id_token_metadata TEXT,
user_code_value TEXT,
user_code_issued_at TIMESTAMP,
user_code_expires_at TIMESTAMP,
user_code_metadata TEXT,
device_code_value TEXT,
device_code_issued_at TIMESTAMP,
device_code_expires_at TIMESTAMP,
device_code_metadata TEXT,
PRIMARY KEY (id)
);
-- 授权同意记录
CREATE TABLE oauth2_authorization_consent (
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
5.2 完整配置类
java
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 应用授权服务器默认配置
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc
.providerConfigurationEndpoint(configurationEndpoint ->
configurationEndpoint.providerConfigurationCustomizer(customizer))
.userInfoEndpoint(userInfo -> userInfo
.userInfoAuthenticationMapper(authenticationMapper))
);
// 处理异常
http.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
)
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt(Customizer.withDefaults())
);
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
@Bean
public OAuth2AuthorizationService authorizationService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(
jdbcTemplate,
registeredClientRepository
);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
rowMapper.setLobHandler(new DefaultLobHandler());
service.setAuthorizationRowMapper(rowMapper);
return service;
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
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 new ImmutableJWKSet<>(jwkSet);
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://localhost:9000")
.build();
}
@Bean
public TokenGenerator tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(jwkSource);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator,
accessTokenGenerator,
refreshTokenGenerator
);
}
@Bean
public KeyPair generateRsaKeyPair() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
5.3 自定义用户信息
java
@Component
public class CustomUserAuthenticationMapper
implements Function<OAuth2TokenClaimsContext, OAuth2TokenClaims> {
@Override
public OAuth2TokenClaims apply(OAuth2TokenClaimsContext context) {
OAuth2TokenClaims claims = OAuth2TokenClaims.builder()
.claim("sub", context.getPrincipal().getName())
.claim("email", getUserEmail(context))
.claim("name", getUserName(context))
.claim("groups", getUserGroups(context))
.build();
return claims;
}
private String getUserEmail(OAuth2TokenClaimsContext context) {
// 自定义逻辑获取用户邮箱
Authentication authentication = context.getPrincipal();
if (authentication.getPrincipal() instanceof UserDetails userDetails) {
return userDetails.getUsername() + "@example.com";
}
return "user@example.com";
}
private String getUserName(OAuth2TokenClaimsContext context) {
// 自定义逻辑获取用户名称
return "Custom User Name";
}
private List<String> getUserGroups(OAuth2TokenClaimsContext context) {
// 自定义逻辑获取用户组
return List.of("users", "admin");
}
}
六、迁移指南
6.1 从 1.1.x 迁移到 7.0
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 更新依赖 | 移除 spring-security-oauth2-authorization-server 依赖 |
| 2 | 更新包名 | 替换所有 org.springframework.security.oauth2.server.authorization 导入 |
| 3 | 重构配置 | 使用新的 @Bean 方式配置授权服务器 |
| 4 | 测试验证 | 全面测试授权流程、Token 生成、用户信息获取 |
| 5 | 数据迁移 | 如使用持久化,迁移数据库表结构 |
6.2 常见问题
Q: 我的自定义 Token 生成器如何迁移?
java
// 旧版本
@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
// 自定义实现
}
// 新版本 - 使用 JWTGenerator + 自定义 Claims
@Bean
public JwtGenerator jwtGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator generator = new JwtGenerator(jwkSource);
generator.setJwtCustomizer(customizer); // 自定义 Claims
return generator;
}
Q: 如何继续支持自定义授权类型?
java
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(customConverter)
.accessTokenResponseHandler(customHandler)
);
七、最佳实践
7.1 安全配置
java
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true) // 强制用户同意
.requireProofKey(true) // 启用 PKCE
.jwkSetUrl("https://auth.example.com/.well-known/jwks.json")
.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(30)) // Access Token 30分钟
.refreshTokenTimeToLive(Duration.ofDays(7)) // Refresh Token 7天
.reuseRefreshTokens(false) // 不重用 Refresh Token
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.build())
7.2 OIDC 配置
java
// 启用 OIDC 支持
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc
.providerConfigurationEndpoint(endpoint ->
endpoint.providerConfigurationCustomizer(customizer))
.userInfoEndpoint(userInfo -> userInfo
.userInfoAuthenticationMapper(authenticationMapper))
.clientRegistrationEndpoint(clientRegistration ->
clientRegistration.clientRegistrationRepository(repository))
);
八、总结
Spring Authorization Server 迁移至 Spring Security 7.0 是一次重要的架构升级,带来了:
- 更统一的开发体验 - OAuth2 功能完全集成到 Spring Security
- 更简化的依赖管理 - 无需额外的授权服务器依赖
- 更强的标准化支持 - 对 OIDC 1.0 的支持更加完善
OAuth2 vs OIDC 选择建议:
- 如果只需要授权访问(第三方应用访问 API),使用 OAuth2 即可
- 如果需要身份认证(单点登录、获取用户信息),选择 OpenID Connect
- 生产环境中,OIDC 是推荐选择,因为它提供了更完整的用户身份解决方案
迁移完成后,开发者可以享受到更简洁的配置、更好的性能,以及与 Spring Security 生态系统的无缝集成。
参考资源: