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

Spring Security 5.8单应用中配置多OAuth2AuthorizedClientManager问题

多OAuth2AuthorizedClientManager配置问题及解决方案

问题背景

我们正在升级至Spring Security 5.8(为后续升级到6.x做准备),需要在单个Spring Boot应用中配置多个OAuth2AuthorizedClientManager,但未找到明确的官方配置指南。应用架构:

  • 服务发现:Spring Cloud Netflix Eureka Server
  • 内嵌组件:Spring Boot Admin(SBA)
  • OAuth2授权服务器:Keycloak

核心需求

  • SBA通过服务账户(client_credentials模式)调用后端/actuator等接口
  • SBA UI需用户通过authorization_code模式登录后才可访问

当前问题

现有配置已实现服务账户调用接口,但给/admin/**路径配置ADMIN角色权限后,访问该路径仅显示空白页,无法跳转到Keycloak登录页。推测原因是未为登录流程指定对应的OAuth2AuthorizedClientManager,需同时使用两类管理器:

  • DefaultOAuth2AuthorizedClientManager:处理会话式用户登录(authorization_code模式)
  • AuthorizedClientServiceOAuth2AuthorizedClientManager:处理后台线程的client_credentials授权

尝试配置SecurityFilterChain时,DefaultOAuth2AuthorizedClientManager未暴露底层AuthorizedClientService,导致配置冲突。

解决方案

1. 调整OAuth2管理器配置

共享OAuth2AuthorizedClientService,为不同场景绑定专属管理器:

@Configuration
public class OAuth2ManagerConfig {

    // 共享的授权客户端存储服务
    @Bean
    public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    // 后台服务账户专用管理器(client_credentials模式)
    @Bean
    public AuthorizedClientServiceOAuth2AuthorizedClientManager serviceAccountClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService) {
        AuthorizedClientServiceOAuth2AuthorizedClientManager manager = 
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
        // 绑定服务账户客户端ID
        manager.setContextAttributesMapper((context) -> {
            Map<String, Object> attributes = new HashMap<>();
            attributes.put(OAuth2AuthorizationContext.REGISTERED_CLIENT_ID_ATTRIBUTE_NAME, "sba");
            return attributes;
        });
        return manager;
    }

    // 用户登录专用管理器(authorization_code模式)
    @Bean
    public DefaultOAuth2AuthorizedClientManager userLoginClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository,
            OAuth2AuthorizedClientService authorizedClientService) {
        DefaultOAuth2AuthorizedClientManager manager = 
            new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
        // 让登录管理器也能访问共享的授权客户端存储
        manager.setAuthorizedClientService(authorizedClientService);
        manager.setContextAttributesMapper(new DefaultOAuth2AuthorizedClientManager.DefaultContextAttributesMapper());
        return manager;
    }
}

2. 调整SecurityFilterChain配置

为oauth2Login和oauth2Client明确绑定用户登录专用管理器:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private final String apiToken;
    private final ClientRegistrationRepository clientRegistrationRepository;
    private final AuthorizedClientServiceOAuth2AuthorizedClientManager serviceAccountClientManager;
    private final DefaultOAuth2AuthorizedClientManager userLoginClientManager;

    @Autowired
    public WebSecurityConfig(@Value("${SBA_API_TOKEN}") String apiToken,
                             ClientRegistrationRepository clientRegistrationRepository,
                             AuthorizedClientServiceOAuth2AuthorizedClientManager serviceAccountClientManager,
                             DefaultOAuth2AuthorizedClientManager userLoginClientManager) {
        this.apiToken = apiToken;
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.serviceAccountClientManager = serviceAccountClientManager;
        this.userLoginClientManager = userLoginClientManager;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .oauth2Client(oauth2 -> oauth2
                        .authorizedClientManager(userLoginClientManager) // 绑定用户登录管理器
                )
                .oauth2Login(oauth2 -> oauth2
                        .authorizedClientManager(userLoginClientManager) // 指定登录流程使用的管理器
                        .userInfoEndpoint(endpoint -> endpoint.userAuthoritiesMapper(new MyAuthoritiesMapper()))
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/*/*.css", "/admin/img/**", "/admin/third-party/**").permitAll()
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().permitAll()
                )
                .logout(logout -> logout
                        .clearAuthentication(true)
                        .invalidateHttpSession(true)
                        .logoutRequestMatcher(new AntPathRequestMatcher("/admin/logout"))
                        .logoutSuccessHandler(oidcLogoutSuccessHandler())
                );
        return http.build();
    }

    private LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
                new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return oidcLogoutSuccessHandler;
    }

    @Bean
    public HttpHeadersProvider keycloakBearerAuthHeaderProvider() {
        return provider -> {
            HttpHeaders headers = new HttpHeaders();
            headers.add("X-Api-Token", apiToken);
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("sba")
                    .principal("spring-boot-admin")
                    .build();
            OAuth2AuthorizedClient authorizedClient = serviceAccountClientManager.authorize(authorizeRequest);
            assert authorizedClient != null;
            headers.add("X-Authorization-Token", "keycloak-bearer " + authorizedClient.getAccessToken().getTokenValue());
            return headers;
        };
    }
}

3. YAML配置保持不变

原YAML配置无需修改,确保两个客户端注册(external用于用户登录,sba用于服务账户)参数正确即可。

关键说明

  • 共享OAuth2AuthorizedClientService:避免两个管理器的数据隔离问题,统一授权客户端存储
  • 明确绑定管理器:确保登录流程使用DefaultOAuth2AuthorizedClientManager,后台服务调用使用AuthorizedClientServiceOAuth2AuthorizedClientManager
  • 场景分离:两类管理器各司其职,互不干扰登录流程与后台服务调用逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 14:44:58