Spring Boot集成Keycloak认证后出现StackOverflowError问题求助
尝试将Spring Boot应用与Keycloak集成,Keycloak采用无密码登录(通过认证应用获取OTP码)。系统包含一个可公开访问的外部页面,用户点击"customers"时需登录Keycloak,仅ADMIN角色用户可访问customers页面。但ADMIN用户完成认证后,应用抛出StackOverflowError,浏览器显示白标错误页而非预期的customers页面。
错误日志
2023-06-17T19:59:10.737+02:00 ERROR 8213 --- [nio-8081-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Filter execution threw an exception] with root cause java.lang.StackOverflowError: null at org.springframework.util.ReflectionUtils.isEqualsMethod(ReflectionUtils.java:516) ~[spring-core-6.0.8.jar:6.0.8] at org.springframework.aop.support.AopUtils.isEqualsMethod(AopUtils.java:151) ~[spring-aop-6.0.8.jar:6.0.8] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:175) ~[spring-aop-6.0.8.jar:6.0.8] at jdk.proxy2/jdk.proxy2.$Proxy125.authenticate(Unknown Source) ~[na:na] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:201) ~[spring-security-core-6.0.3.jar:6.0.3] ... 2023-06-17T19:59:10.751+02:00 DEBUG 8213 --- [nio-8081-exec-4] o.s.security.web.FilterChainProxy : Secured GET /error?state=XIg20i762t2joGdc86aeXARAwo4JwxKw-m1E1E1pvB4%3D&session_state=0ae9a0ee-6bbb-48e8-a1f4-cca029be6d4e&code=5957c24f-ea25-4197-9b35-9c85c6e594fa.0ae9a0ee-6bbb-48e8-a1f4-cca029be6d4e.318d1d70-732d-47a5-8f8b-dd8bc45c1306
相关代码配置
SecurityConfig代码
@Configuration @EnableWebSecurity public class SecurityConfig { private final KeycloakLogoutHandler keycloakLogoutHandler; public SecurityConfig(KeycloakLogoutHandler keycloakLogoutHandler) { this.keycloakLogoutHandler = keycloakLogoutHandler; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeRequests((authorizeRequests) -> authorizeRequests .requestMatchers("/customers*") .hasRole("ADMIN") .anyRequest() .permitAll()); http .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/customers") .failureUrl("/external")) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/external") .addLogoutHandler(keycloakLogoutHandler)); http.oauth2ResourceServer((oauth2) -> oauth2 .jwt(Customizer.withDefaults()) ); System.out.println("linea 55"); return http.build(); } @Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { return http.getSharedObject(AuthenticationManagerBuilder.class) .build(); } }
KeycloakLogoutHandler代码
@Component automáticamente en el contexto de la aplicación. public class KeycloakLogoutHandler implements LogoutHandler { private static final Logger logger = LoggerFactory.getLogger(KeycloakLogoutHandler.class); private final RestTemplate restTemplate; //instancia de la clase RestTemplate, que es una clase de Spring que proporciona una forma conveniente de interactuar con servicios RESTful. public KeycloakLogoutHandler(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { logoutFromKeycloak((OidcUser) auth.getPrincipal()); } private void logoutFromKeycloak(OidcUser user) { String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout"; UriComponentsBuilder builder = UriComponentsBuilder .fromUriString(endSessionEndpoint) .queryParam("id_token_hint", user.getIdToken().getTokenValue()); ResponseEntity<String> logoutResponse = restTemplate.getForEntity( builder.toUriString(), String.class); if (logoutResponse.getStatusCode().is2xxSuccessful()) { logger.info("Successfully logged out from Keycloak"); } else { logger.error("Could not propagate logout to Keycloak"); } } }
WebController代码
@Controller public class WebController { @Autowired private CustomerDAO customerDAO; @GetMapping(path = "/") public String index() { return "external"; } @GetMapping("/logout") public String logout(HttpServletRequest request) throws Exception { request.logout(); return "redirect:/"; } @GetMapping(path = "/customers") public String customers(Principal principal, Model model) { return "customers"; } }
application.properties配置
# server port server.port=8081 #client registration configuration: spring.security.oauth2.client.registration.keycloak.client-id=appweb spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.keycloak.scope=openid keycloak.auth-server-url=http://localhost:8090/auth keycloak.realm=demo keycloak.resource=appweb #OIDC provider configuration spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8090/realms/demo spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username #configuration needed for validating JWT token against our keycloak server spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8090/realms/demo logging.level.org.springframework.security=DEBUG #si keycloak fuera https keycloak.ssl-required=external
导致StackOverflowError的核心原因是自定义的AuthenticationManager Bean触发了循环依赖,Spring Security在OAuth2登录流程中会自动配置AuthenticationManager,手动注册这个Bean会导致代理对象循环调用authenticate方法,最终栈溢出。
修复步骤如下:
删除自定义的AuthenticationManager Bean
从SecurityConfig中移除以下代码块:@Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { return http.getSharedObject(AuthenticationManagerBuilder.class) .build(); }Spring Security会根据上下文自动提供合适的
AuthenticationManager实例,无需手动注册。修正KeycloakLogoutHandler的注解
原代码中@Component注解后面有多余的注释文字,需修正为标准的@Component注解:@Component public class KeycloakLogoutHandler implements LogoutHandler { // ... 原有逻辑保持不变 }检查角色映射(可选)
确保Keycloak中ADMIN角色的名称与Spring Security中的匹配,若Keycloak角色前缀为ROLE_,则代码中hasRole("ADMIN")会自动匹配ROLE_ADMIN;若Keycloak角色无前缀,可改用hasAuthority("ADMIN"),或者配置角色转换器统一处理。清理冗余配置
application.properties中包含部分旧版Keycloak适配器的配置(如keycloak.auth-server-url、keycloak.realm),这些在Spring Security OAuth2客户端模式下无需保留,可删除,仅保留Spring Security OAuth2相关配置:# server port server.port=8081 #client registration configuration: spring.security.oauth2.client.registration.keycloak.client-id=appweb spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.keycloak.scope=openid #OIDC provider configuration spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8090/realms/demo spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username #configuration needed for validating JWT token against our keycloak server spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8090/realms/demo logging.level.org.springframework.security=DEBUG #si keycloak fuera https #keycloak.ssl-required=external
完成以上修改后,重启应用,ADMIN用户认证成功后将正常跳转到customers页面,不再出现栈溢出错误。
内容的提问来源于stack exchange,提问作者Pau Perez

