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

Spring Boot 3.0升级后Spring Cloud OAuth2迁移至Spring Security求助

Spring Boot Security 3.0 资源服务器迁移方案(替代废弃Spring Cloud OAuth2组件)

核心问题拆解

  • 依赖冲突:spring-cloud-starter-oauth2:2.2.5.RELEASE与Spring Boot 3.x配套的Spring Security 6.x不兼容,导致JwtAuthenticationToken和旧的OAuth2Authentication类型转换失败。
  • 组件废弃:RemoteTokenServices、AccessTokenConverter、OAuth2Authentication等旧Spring Cloud OAuth2组件已被Spring Security OAuth2 Resource Server模块替代。

迁移步骤

1. 清理旧依赖

移除spring-cloud-starter-oauth2依赖,Spring Boot 3.x的spring-boot-starter-oauth2-resource-server已包含资源服务器所有必需功能:

// 删除此行
// implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE'

2. 替换自定义Token验证逻辑(替代MyRemoteTokenServices)

旧的RemoteTokenServices用于调用授权服务器的check_token端点验证令牌,在Spring Security 6.x中,改用OAuth2IntrospectionAuthenticationProvider实现令牌 introspection 逻辑,同时保留你的缓存、过期检查和自定义信任配置:

package si.osi.rekonoapi.server.services;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import lombok.NonNull;
import org.apache.commons.lang3.SerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException;
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionService;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import si.osi.rekonoapi.server.utils.Utils;

import java.io.IOException;
import java.util.Map;

public class MyOauth2IntrospectionService implements OAuth2IntrospectionService {
    private static final Logger LOG = LoggerFactory.getLogger(MyOauth2IntrospectionService.class);
    private final OAuth2IntrospectionService delegate;
    private final Cache cache;

    public MyOauth2IntrospectionService(CacheManager cacheManager, String trustPath, String trustKey, String introspectionUri, String clientId, String clientSecret) {
        // 初始化自定义RestTemplate(保留原信任配置)
        RestTemplate restTemplate = Utils.prepareRestClient(
                "OAuth2IntrospectionService",
                null, null,
                trustPath, trustKey);
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(@NonNull ClientHttpResponse response) throws IOException {
                if (response.getStatusCode().value() != 400) {
                    super.handleError(response);
                }
            }
        });

        // 使用默认的Introspection服务实现,传入自定义RestTemplate、端点、客户端信息
        this.delegate = new org.springframework.security.oauth2.server.resource.introspection.NimbusOAuth2IntrospectionService(
                introspectionUri, clientId, clientSecret, restTemplate);
        this.cache = cacheManager.getCache("accessTokens");
    }

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) throws OAuth2IntrospectionException {
        // 先检查令牌是否过期
        if (isExpired(token)) {
            cache.evictIfPresent(token);
            throw new OAuth2IntrospectionException("Token is expired.");
        }

        // 从缓存获取验证结果
        OAuth2AuthenticatedPrincipal result = cache.get(token, OAuth2AuthenticatedPrincipal.class);
        if (result == null) {
            LOG.trace("Authentication info not in cache. Calling introspection endpoint...");
            result = delegate.introspect(token);
            // 缓存时转换为可序列化的实现(默认的Nimbus实现不可序列化)
            OAuth2IntrospectionAuthenticatedPrincipal serializablePrincipal =
                    new OAuth2IntrospectionAuthenticatedPrincipal(result.getAttributes(), result.getAuthorities());
            cache.put(token, serializablePrincipal);
        } else {
            LOG.trace("Authentication info present in cache. Reusing...");
        }
        return SerializationUtils.clone((OAuth2IntrospectionAuthenticatedPrincipal) result);
    }

    private boolean isExpired(String accessToken) {
        try {
            String withoutSignature = accessToken.substring(0, accessToken.lastIndexOf('.') + 1);
            Jwts.parserBuilder().build().parseClaimsJwt(withoutSignature);
        } catch (ExpiredJwtException exc) {
            return true;
        }
        return false;
    }
}

3. 重构Security配置类

移除所有废弃组件的Bean,根据你的需求选择JWT本地验证或令牌Introspection模式(二选一,根据授权服务器支持的方式):

方案A:JWT本地验证模式(使用JWKS端点)

适合授权服务器提供JWKS端点,本地验证令牌签名和有效性:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${security.oauth2.resource.jwk.key-set-uri}")
    private String jwkSetUri;

    private final CacheManager cacheManager;
    private final MySecurityResourceProperties properties;

    public SecurityConfig(CacheManager cacheManager, @Qualifier("mySecurityResourceProperties") MySecurityResourceProperties properties) {
        this.cacheManager = cacheManager;
        this.properties = properties;
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    }

    @Bean
    @Order(1)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("some_endpoint").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(jwt -> jwt
                                .jwtAuthenticationConverter(customJwtAuthenticationConverter())
                        )
                )
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
                );

        return http.build();
    }

    // 替代原JwtConverter,将JWT claims设置为认证信息的details
    @Bean
    public JwtAuthenticationConverter customJwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        // 自定义转换逻辑,将JWT的所有claims设置为details
        converter.setPrincipalClaimName("sub");
        converter.setAuthenticationConverter(jwt -> {
            JwtAuthenticationToken token = (JwtAuthenticationToken) converter.getAuthenticationConverter().convert(jwt);
            if (token != null) {
                token.setDetails(jwt.getClaims());
            }
            return token;
        });
        return converter;
    }
}

方案B:令牌Introspection模式(调用授权服务器check_token端点)

适合需要授权服务器实时验证令牌状态的场景:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Value("${security.oauth2.resource.token-info-trust:}")
    private String API_TRUST_PATH;

    @Value("${security.oauth2.resource.token-info-trust-key:}")
    private String API_TRUST_KEY;

    private final CacheManager cacheManager;
    private final MySecurityResourceProperties properties;

    public SecurityConfig(CacheManager cacheManager, @Qualifier("mySecurityResourceProperties") MySecurityResourceProperties properties) {
        this.cacheManager = cacheManager;
        this.properties = properties;
    }

    @Bean
    public OAuth2IntrospectionService introspectionService() {
        return new MyOauth2IntrospectionService(
                cacheManager,
                API_TRUST_PATH,
                API_TRUST_KEY,
                properties.getTokenInfoUri(),
                properties.getClientId(),
                properties.getClientSecret()
        );
    }

    @Bean
    @Order(1)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("some_endpoint").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2
                        .opaqueToken(opaque -> opaque
                                .introspectionService(introspectionService())
                        )
                )
                .exceptionHandling(exceptions -> exceptions
                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
                );

        return http.build();
    }
}

4. 替换业务代码中的类型引用

将所有业务代码中OAuth2Authentication的引用替换为:

  • JWT模式:JwtAuthenticationToken
  • Introspection模式:OAuth2IntrospectionAuthenticationToken

例如,获取认证信息的代码:

// 旧代码
OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();

// 新代码(JWT模式)
JwtAuthenticationToken auth = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> details = auth.getToken().getClaims(); // 对应原auth.getDetails()

最终依赖配置

确保依赖只保留Spring Boot官方组件:

// spring-boot
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.0.0'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.0.0'
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.0'
implementation 'org.springframework.boot:spring-boot-starter-security:3.0.0'
implementation 'org.springframework.boot:spring-boot-starter-cache:3.0.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.0'
testImplementation 'org.springframework.security:spring-security-test:6.3.0'

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 06:13:10