我正在使用自签名证书运行 Keycloak 24.0.0。
我的 springboot 应用程序使用客户端秘密身份验证方法和授权代码授予类型(通过 spring security 6.2)对 Keycloak 进行身份验证:
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) // Check if this is correct
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.scope(Scopes.names())
.userNameAttributeName(IdTokenClaimNames.SUB) // Check if this is correct
.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();
}
我用一个自定义的 JwtDecoder 覆盖 JwtDecoder,该自定义 JwtDecoder 接受一个 RestTemplate,该模板可以选择(基于配置)接受自签名证书并跳过主机名验证:
@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);
}
但是,看起来默认的 OAuth2UserService 实例化了自己的 RestTemplate,所以没有使用我的。
我尝试通过提供带有我自己的 RestTemplate 的 OAuth2UserService 来覆盖此问题:
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(RestTemplate restTemplate) {
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRestOperations(restTemplate);
return defaultOAuth2UserService;
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OidcUserService oidcUserService = new OidcUserService();
oidcUserService.setOauth2UserService(userService);
return oidcUserService;
}
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, RestTemplate restTemplate) throws Exception {
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService = oauth2UserService(restTemplate);
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);
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Client(Customizer.withDefaults());
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService(userService))
.userService(userService)
))
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build();
}
但是每当我针对 keycloak 进行身份验证时,都会收到以下错误:
[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
如果我覆盖其他一些端点服务以使用自定义 RestTemplate,如下所示:
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService = oauth2UserService(clientHttpRequestFactory);
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);
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Client(Customizer.withDefaults());
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService(userService))
.userService(userService)
).tokenEndpoint(token -> token
.accessTokenResponseClient(authorizationCodeTokenResponseClient(clientHttpRequestFactory))
))
.logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build();
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRestOperations(restTemplate);
return defaultOAuth2UserService;
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OidcUserService oidcUserService = new OidcUserService();
oidcUserService.setOauth2UserService(userService);
return oidcUserService;
}
private DefaultAuthorizationCodeTokenResponseClient authorizationCodeTokenResponseClient(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setMessageConverters(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(restTemplate);
return tokenResponseClient;
}
然后我得到一个不同的错误:
[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 客户端以接受自签名证书并忽略主机名验证而不将证书导入到信任存储区?
有各种移动部分处理 OAuth 流程的不同部分。 他们每个人都在使用自己的
RestOperations
。
要找出哪些部分出现故障,您可以为
org.springframework.security
启用 TRACE 日志记录。
在这种情况下,您需要重写端点处理程序,创建通用 JwtDecoder,并重写一堆
Default
-s 为每个设置适当的 RestTemplate
。
首先,创建一个
ClientHttpRequestFactory
,有条件地接受自签名证书并忽略主机名验证:
@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);
}
然后在您需要覆盖的每个区域中使用它们:
@Bean
public JwtDecoder jwtDecoder(RestTemplate restTemplate) {
return NimbusJwtDecoder.withIssuerLocation("%s/realms/%s".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()))
.restOperations(restTemplate).build();
}
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http, ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService = oauth2UserService(clientHttpRequestFactory);
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);
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
http.oauth2Client(Customizer.withDefaults());
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService(userService))
.userService(userService)
.userAuthoritiesMapper(userAuthoritiesMapperForKeycloak())
)
.tokenEndpoint(token -> token
.accessTokenResponseClient(authorizationCodeTokenResponseClient(clientHttpRequestFactory))
)
).logout(logout -> logout.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/"));
return http.build();
}
// Handles the calls to /certs
@Bean
public JwtDecoderFactory<ClientRegistration> customJwtDecoderFactory(RestTemplate restTemplate) {
return new CustomJwtDecoderFactory(restTemplate, "%s/realms/%s/protocol/openid-connect/certs".formatted(keycloakInitializer.serverUrl(), keycloakInitializer.realm()));
}
static class CustomJwtDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
private RestTemplate restTemplate;
private String jwtSetUri;
public CustomJwtDecoderFactory(RestTemplate restTemplate, String jwtSetUri) {
this.restTemplate = restTemplate;
this.jwtSetUri = jwtSetUri;
}
public JwtDecoder createDecoder(ClientRegistration reg) {
return NimbusJwtDecoder.withJwkSetUri(jwtSetUri)
.restOperations(restTemplate).build();
}
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();
defaultOAuth2UserService.setRestOperations(restTemplate);
return defaultOAuth2UserService;
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OidcUserService oidcUserService = new OidcUserService();
oidcUserService.setOauth2UserService(userService);
return oidcUserService;
}
// Handles the calls to /token
private DefaultAuthorizationCodeTokenResponseClient authorizationCodeTokenResponseClient(ClientHttpRequestFactory clientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
restTemplate.setMessageConverters(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
tokenResponseClient.setRestOperations(restTemplate);
return tokenResponseClient;
}
考虑到启用此用例所需的代码量和覆盖量,将证书添加到信任存储区似乎要简单得多。假设您的组件架构使这一切变得简单。