You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从Spring Boot2.2升3.2、Java8升21:Spring授权服务器升级遇阻

问题描述

正在将应用从Java 8迁移至Java 21,同时将Spring Boot从2.2版本升级到3.2版本。安全模块升级时遇到问题:应用同时包含授权服务器和资源服务器,此前使用spring-security-oauth2:2.0.10.RELEASE做认证授权,依赖如下:

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.0.10.RELEASE</version>
</dependency>

原授权服务器配置类代码:

import java.util.Arrays;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;


@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
    private static final String GRANT_TYPE_PASSWORD = "password";
    private static final String AUTHORIZATION_CODE = "authorization_code";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String IMPLICIT = "implicit";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1 * 60 * 60;
    private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6 * 60 * 60;
    
    
    @Value("${auth.server.client.id}")
    private String clientId;
    
    @Value("${auth.server.client.secret}")
    private String clientSecret;
    
    @Value("${auth.server.signing.key}")
    private String signingKey;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CustomTokenEnhancer customTokenEnhancer;

    @SuppressWarnings("unused")
    @Autowired
    private DataSource dataSource;
    
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signingKey);
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {

        security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()")
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        clientDetailsServiceConfigurer.inMemory().withClient(clientId)
                .secret(new BCryptPasswordEncoder().encode(clientSecret))
                .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT)
                .scopes(SCOPE_READ, SCOPE_WRITE, TRUST).accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
                .refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS);

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, accessTokenConverter()));

        endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain)
                .authenticationManager(authenticationManager).exceptionTranslator(e -> {
                    if (e instanceof OAuth2Exception oAuth2Exception) {
                        return ResponseEntity.status(oAuth2Exception.getHttpErrorCode())
                                .body(new OAuth2Exception(oAuth2Exception.getMessage()));
                    } else {
                        throw e;
                    }
                });
    }

}

找不到合适的升级方案到新版Spring Authorization Server,尤其是如何整合自定义token增强器和JWT令牌转换器,请问是否有方法实现这些功能?


解决方案

1. 替换依赖

移除旧的spring-security-oauth2依赖,添加适配Spring Boot 3.2的Spring Authorization Server官方依赖:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>1.2.3</version> <!-- 与Spring Boot 3.2版本兼容 -->
</dependency>

Spring Boot会自动管理匹配的Spring Security版本,无需单独指定。

2. 重构授权服务器配置

新版已弃用@EnableAuthorizationServer,改用@Configuration配合核心Bean完成配置,以下是对应原逻辑的重构示例:

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

@Configuration
public class AuthorizationServerConfig {

    @Value("${auth.server.client.id}")
    private String clientId;

    @Value("${auth.server.client.secret}")
    private String clientSecret;

    // 配置客户端信息,对应原ClientDetailsServiceConfigurer逻辑
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId(clientId)
                .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode(clientSecret))
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD) // 密码模式需显式配置
                .redirectUri("http://localhost:8080/login/oauth2/code/client")
                .scope(OidcScopes.OPENID)
                .scope("read")
                .scope("write")
                .scope("trust")
                .tokenSettings(tokenSettings -> tokenSettings
                        .accessTokenTimeToLive(java.time.Duration.ofHours(1))
                        .refreshTokenTimeToLive(java.time.Duration.ofHours(6)))
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    // 配置授权服务器端点路径,保持与原有路径一致
    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .tokenEndpoint("/oauth/token")
                .jwkSetEndpoint("/oauth/jwks")
                .build();
    }

    // 配置JWT密钥源,替换原JwtAccessTokenConverter
    @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 new ImmutableJWKSet<>(jwkSet);
    }

    // 生成RSA密钥对(若已有密钥可替换为自定义加载逻辑)
    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 NimbusJwtDecoder.withJwkSource(jwkSource).build();
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    // 自定义Token增强器,替换原TokenEnhancerChain和CustomTokenEnhancer
    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return context -> {
            // 迁移原CustomTokenEnhancer的逻辑,添加自定义Claims
            if (context.getTokenType().getValue().equals("access_token")) {
                context.getClaims()
                        .claim("custom-field", "custom-value")
                        .claim("username", context.getPrincipal().getName());
                // 原CustomTokenEnhancer的其他逻辑可在此实现
            }
        };
    }

    // 配置AuthenticationManager,支持密码模式
    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        return new ProviderManager(authenticationProvider);
    }
}

3. 关键功能迁移说明

  • 自定义Token增强:原CustomTokenEnhancer的逻辑完全迁移到OAuth2TokenCustomizer<JwtEncodingContext>中,通过JwtEncodingContext可以获取认证主体、令牌类型等信息,自由添加或修改JWT的Claims。
  • JWT转换器替代:原JwtAccessTokenConverter的签名/验签逻辑由JWKSource、JwtEncoder、JwtDecoder组合实现,安全性更高,支持标准的JWK规范。
  • 客户端配置:原ClientDetailsServiceConfigurer的逻辑迁移到RegisteredClientRepository中,通过RegisteredClient构建器配置客户端权限、授权类型、令牌有效期等。

4. 注意事项

  • 密码模式:新版Spring Authorization Server默认不支持密码模式,如需保留必须显式配置AuthenticationManager并确保用户认证逻辑正确。
  • 密钥管理:建议使用RSA密钥对代替简单的字符串签名密钥,若必须使用原有signingKey,可将其转换为HMAC密钥适配JWK规范。
  • 异常处理:原exceptionTranslator的逻辑可通过自定义AuthenticationEntryPoint或AccessDeniedHandler实现,处理认证授权过程中的异常。

内容的提问来源于stack exchange,提问作者Sudan Shrestha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 08:57:04