Spring Security 5.8单应用中配置多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
相关产品推荐
相关产品推荐

