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

Spring Security集成Keycloak(自签名证书):如何禁用主机名验证?

问题描述

我使用Keycloak 24.0.0(自签名证书),Spring Boot应用通过Spring Security 6.2采用客户端密钥认证+授权码授权类型对接Keycloak做身份验证。核心配置与遇到的问题如下:

ClientRegistration配置

private ClientRegistration keycloakClientRegistration() {
    return ClientRegistration.withRegistrationId("keycloak")
            .clientId(keycloakInitializer.clientId())
            .clientSecret(keycloakInitializer.clientSecret())
            .authorizationUri("%s/realms/%s/protocol/openid-connect/auth".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .tokenUri("%s/realms/%s/protocol/openid-connect/token".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .userInfoUri("%s/realms/%s/protocol/openid-connect/userinfo".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope(Scopes.names())
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .issuerUri("%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .jwkSetUri("%s/realms/%s/protocol/openid-connect/certs".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .clientName(keycloakInitializer.clientName())
            .build();
}

自定义RestTemplate与JwtDecoder配置

我已实现支持接受自签名证书、跳过主机名验证的RestTemplate(通过配置开关控制),并自定义JwtDecoder使用该实例:

@Bean
public JwtDecoder jwtDecoder(RestTemplate restTemplate) {
    return NimbusJwtDecoder.withIssuerLocation("%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
            .restOperations(restTemplate).build();
}

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
    return new RestTemplate(clientHttpRequestFactory);
}

@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(SslBundles sslBundles, @Value("${keycloak.accept-untrusted-certs}") boolean acceptUntrustedCerts) {
    SSLFactory defaultSslFactory = SSLFactory.builder()
            .withUnsafeTrustMaterial()
            .withUnsafeHostnameVerifier()
            .build();

    CloseableHttpClient httpClient;
    if (acceptUntrustedCerts) {
        LOGGER.info("Accepting untrusted certs for keycloak and ignoring hostname verification.");
        httpClient = HttpClients.custom().setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
                        .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create()
                                .setSslContext(defaultSslFactory.getSslContext())
                                .setHostnameVerifier(defaultSslFactory.getHostnameVerifier())
                                .build())
                        .build())
                .build();
    } else {
        try {
            SSLContext sslContext = sslBundles.getBundle("keycloak").createSslContext();
            httpClient = HttpClients.custom().setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
                            .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create()
                                    .setSslContext(sslContext)
                                    .build())
                            .build())
                    .build();
            LOGGER.info("Accepting supplied cert for keycloak and applying hostname verification.");
        } catch (NoSuchSslBundleException e) {
            LOGGER.info("Could not find an SSL Context for keycloak. Using default system SSL settings.");
            httpClient = HttpClients.createDefault();
        }
    }

    return new HttpComponentsClientHttpRequestFactory(httpClient);
}

遇到的问题

  1. 默认OAuth2UserService会自行实例化RestTemplate,不使用我定义的实例,导致认证时证书错误:

[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: I/O error on POST request for "https://keycloak:8443/realms/MyRealm/protocol/openid-connect/token": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

  1. 尝试覆盖OAuth2UserService、AuthorizationCodeTokenResponseClient等组件后,又出现JWK集获取的证书错误:

[invalid_id_token] An error occurred while attempting to decode the Jwt: Couldn't retrieve remote JWK set: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://keycloak:8443/realms/MyRealm/protocol/openid-connect/certs": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

核心需求:无需导入证书到信任库,统一配置OAuth2客户端使其接受自签名证书并忽略主机名验证。


解决方案

不用逐个覆盖OAuth2组件,可通过以下两种方式实现全局统一配置:

方式一:标记自定义RestTemplate为全局优先Bean

Spring Security的OAuth2组件会优先使用容器中已有的RestTemplate实例,只需给自定义RestTemplate添加@Primary注解,确保所有组件自动复用该实例:

@Primary
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
    RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
    // 统一添加OAuth2所需的消息转换器和错误处理器
    restTemplate.setMessageConverters(Arrays.asList(
            new FormHttpMessageConverter(),
            new OAuth2AccessTokenResponseHttpMessageConverter(),
            new MappingJackson2HttpMessageConverter()
    ));
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
    return restTemplate;
}

配置完成后,DefaultOAuth2UserService、DefaultAuthorizationCodeTokenResponseClient等组件会自动使用这个带SSL配置的RestTemplate,无需手动逐个设置。

方式二:统一配置Nimbus与OAuth2组件的SSL上下文

针对JWK集获取等Nimbus发起的请求,直接配置其使用自定义SSL上下文,同时封装公共方法复用同一个ClientHttpRequestFactory:

1. 完善JwtDecoder配置

@Bean
public JwtDecoder jwtDecoder(ClientHttpRequestFactory clientHttpRequestFactory) {
    RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

    NimbusJwtDecoder decoder = NimbusJwtDecoder.withIssuerLocation(
            "%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm())
    ).restOperations(restTemplate).build();

    // 强制JWK集获取使用自定义RestTemplate
    decoder.setJwkSetUri(
            "%s/realms/%s/protocol/openid-connect/certs".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()),
            restTemplate
    );

    return decoder;
}

2. 封装公共方法复用SSL配置

// 公共方法:创建带OAuth2配置的RestTemplate
private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
    RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
    restTemplate.setMessageConverters(Arrays.asList(
            new FormHttpMessageConverter(),
            new OAuth2AccessTokenResponseHttpMessageConverter(),
            new MappingJackson2HttpMessageConverter()
    ));
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
    return restTemplate;
}

// 在SecurityFilterChain中统一使用
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
    RestTemplate oauth2RestTemplate = createOAuth2RestTemplate(clientHttpRequestFactory);

    // 配置OAuth2UserService
    DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
    userService.setRestOperations(oauth2RestTemplate);

    // 配置OidcUserService
    OidcUserService oidcUserService = new OidcUserService();
    oidcUserService.setOauth2UserService(userService);

    // 配置TokenResponseClient
    DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
    tokenResponseClient.setRestOperations(oauth2RestTemplate);

    // 构建SecurityFilterChain
    http.cors(Customizer.withDefaults())
            .csrf(csrf -> csrf
                    .csrfTokenRepository(new CookieCsrfTokenRepository())
                    .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
            )
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated()
                    .anyRequest().permitAll()
            )
            .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .oauth2Client(Customizer.withDefaults())
            .oauth2Login(oauth2 -> oauth2
                    .userInfoEndpoint(userInfo -> userInfo
                            .oidcUserService(oidcUserService)
                            .userService(userService)
                    )
                    .tokenEndpoint(token -> token.accessTokenResponseClient(tokenResponseClient))
            )
            .logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));

    return http.build();
}

关键注意事项

  • 仅在开发环境使用withUnsafeTrustMaterial()和withUnsafeHostnameVerifier(),生产环境必须使用可信证书或导入自定义证书到信任库。
  • 确保keycloak.accept-untrusted-certs配置仅在开发环境开启,避免生产环境的安全风险。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 09:12:02