从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
相关产品推荐
相关产品推荐

