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

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方法,最终栈溢出。

修复步骤如下:

  1. 删除自定义的AuthenticationManager Bean
    从SecurityConfig中移除以下代码块:

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .build();
    }
    

    Spring Security会根据上下文自动提供合适的AuthenticationManager实例,无需手动注册。

  2. 修正KeycloakLogoutHandler的注解
    原代码中@Component注解后面有多余的注释文字,需修正为标准的@Component注解:

    @Component
    public class KeycloakLogoutHandler implements LogoutHandler {
        // ... 原有逻辑保持不变
    }
    
  3. 检查角色映射(可选)
    确保Keycloak中ADMIN角色的名称与Spring Security中的匹配,若Keycloak角色前缀为ROLE_,则代码中hasRole("ADMIN")会自动匹配ROLE_ADMIN;若Keycloak角色无前缀,可改用hasAuthority("ADMIN"),或者配置角色转换器统一处理。

  4. 清理冗余配置
    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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 13:24:56