Spring Authorization Server 迁移至 Spring Security 7.0:变革与展望

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 是一次重要的架构升级,带来了:

  1. 更统一的开发体验 - OAuth2 功能完全集成到 Spring Security
  2. 更简化的依赖管理 - 无需额外的授权服务器依赖
  3. 更强的标准化支持 - 对 OIDC 1.0 的支持更加完善

OAuth2 vs OIDC 选择建议:

  • 如果只需要授权访问(第三方应用访问 API),使用 OAuth2 即可
  • 如果需要身份认证(单点登录、获取用户信息),选择 OpenID Connect
  • 生产环境中,OIDC 是推荐选择,因为它提供了更完整的用户身份解决方案

迁移完成后,开发者可以享受到更简洁的配置、更好的性能,以及与 Spring Security 生态系统的无缝集成。


参考资源:

相关推荐
码农学院2 小时前
Mysql 是如何解决幻读问题的?
数据库·mysql
AscendKing2 小时前
java poi word首行插入文字
java·c#·word
鸽鸽程序猿2 小时前
【JavaEE】【SpringCloud】概述
java·spring cloud·java-ee
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue高校实验室教学管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机学姐2 小时前
基于SpringBoot的共享单车管理系统【2026最新】
java·spring boot·后端·spring·java-ee·intellij-idea·mybatis
Coder_Boy_2 小时前
Spring AI 源码核心分析
java·人工智能·spring
net3m332 小时前
websocket下发mp3帧数据时一个包被分包为几个子包而导致mp3解码失败而播放卡顿有杂音或断播的解决方法
开发语言·数据库·python
、BeYourself2 小时前
Spring AI ChatClient -Prompt 模板
java·后端·spring·springai
TG:@yunlaoda360 云老大2 小时前
华为云国际站代理商WeLink的资源与工具支持具体有哪些?
服务器·数据库·华为云