英文:
How to manage HTTPS Mutual Authentication including Bearer Token with Spring boot WebClient?
问题
我的帖子目标是直接分享我关于以下主题的答案。我还分享了帮助我的链接 => 我正在开发一个基于Spring WebFlux的后端。一个前端Angular应用程序连接到我的后端。
我的后端还通过HTTPS连接到业务服务器,基于双向身份验证。
我必须在每个向业务服务器的头部请求中设置一个承载令牌。
我通过HTTPS通过OAuth 2.0客户端凭据授权流程从服务器检索此令牌。下面是我如何管理实施的答案。
英文:
The goal of my post is to directly share my answer regarding the following topic. I share also the links that helped me => I'm developping a backend based on Spring webflux. A frontEnd angular application is connected to my backend.
My backend is also connected to a business server through HTTPS based on mutual authentication.
I have to set a bearer token in each header request to my business server.
I retrieve this token from a server through OAuth 2.0 client credentials grant flow through HTTPS. My answer below shows how I managed the implementation.
答案1
得分: 0
以下是翻译好的部分:
感谢以下链接:
- https://stackoverflow.com/questions/62935967/spring-boot-oauth2-clientreactive-mutual-tls-ssl-token-uri
- https://stackoverflow.com/questions/45418523/spring-5-webclient-using-ssl
- https://stackoverflow.com/questions/62935967/spring-boot-oauth2-clientreactive-mutual-tls-ssl-token-uri
- https://stackoverflow.com/questions/17173559/how-to-verify-if-java-sends-the-client-certificate-in-a-mutual-auth-scenario => 有助于在调试模式下检查双向认证
我找到了以下解决方案:
@Configuration
@EnableWebFluxSecurity
public class Oauth2ClientConfig {
// 基于 OAuth 2.0 客户端凭证授权流的 Bearer Token 管理
@Bean
ReactiveClientRegistrationRepository getRegistration() {
ClientRegistration registration = ClientRegistration
.withRegistrationId("credentialProvider")
.tokenUri("https://rhsso:8446/auth/token")
.clientId("client_id").clientSecret("client_secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
// 使用 HTTPS 进行 Bearer Token 管理
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) throws IOException, KeyStoreException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
final FileInputStream keyStoreFile = new FileInputStream("C:\\keystore\\client_keystore.p12"); // 客户端私钥和客户端证书
final FileInputStream trustStoreFile = new FileInputStream("C:\\truststore\\server_truststore.p12");
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
final KeyStore trustStore = KeyStore.getInstance("PKCS12");
final KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String keyStorePassword = "keyStorePassword";
String trustStorePassword = "trustStorePassword";
keyStore.load(keyStoreFile, keyStorePassword.toCharArray());
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
trustStore.load(trustStoreFile, trustStorePassword.toCharArray());
trustManagerFactory.init(trustStore);
// 通过 sslContext 进行 HTTPS 管理
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().keyManager(keyManagerFactory)
.trustManager(trustManagerFactory);
SslContext sslContext = sslContextBuilder.build();
HttpClient httpSslClient = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext));
ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpSslClient);
accessTokenResponseClient.setWebClient(WebClient.builder().clientConnector(httpConnector).build());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().clientCredentials(c -> {
c.accessTokenResponseClient(accessTokenResponseClient);
}).build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean("CustomWebClient") //Qualifier to be used by my dedicated ClientAPI service in order to use the right one among available WebClient beans
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) throws KeyStoreException,
NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauthFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauthFunction.setDefaultClientRegistrationId("credentialProvider"); // 链接到上面管理的 Bearer token 的 ID
final FileInputStream keyStoreFile = new FileInputStream("C:\\keystore\\business_client_keystore.p12"); // 客户端私钥和客户端证书
final FileInputStream trustStoreFile = new FileInputStream("C:\\truststore\\business_server_truststore.p12");
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
final KeyStore trustStore = KeyStore.getInstance("PKCS12");
final KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String keyStorePassword = "business_keyStorePassword";
String trustStorePassword = "business_trustStorePassword";
keyStore.load(keyStoreFile, keyStorePassword.toCharArray());
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
trustStore.load(trustStoreFile, trustStorePassword.toCharArray());
trustManagerFactory.init(trustStore);
// 创建 Netty SslContext、HttpClient
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().keyManager(keyManagerFactory)
.trustManager(trustManagerFactory);
SslContext sslContext = sslContextBuilder.build();
HttpClient httpSslClient = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext));
// 创建 Reactive ClientHttpConnector & WebClient
final ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpSslClient);
return WebClient.builder().clientConnector(httpConnector).filter(oauthFunction).build();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// 禁用前端界面的 CSRF,但通常不建议,只在特殊情况下使用
// 跨域管理以接受安装在本地主机上的前端应用程序,仅允许 POST 方法
http.csrf().disable().cors(corsCustomizer -> {
CorsConfigurationSource configurationSource = request -> {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(List.of("localhost:8080"));
corsConfiguration.setAllowedMethods(List.of("POST"));
return corsConfiguration;
};
corsCustomizer.configurationSource(configurationSource);
}).oauth2Client();
return http.build();
}
}
基于上述配置的 Web 客户端:
@Service
public class ClientAPI {
private WebClient webClient;
private String businessServiceUrl;
@Autowired
public ClientAPI(@Qualifier("CustomWebClient") WebClient webClient) {
this.businessServiceUrl = "https://hostServerName:8081/theService";
this.webClient = webClient;
}
public Mono<Response>
<details>
<summary>英文:</summary>
Thanx to following links :
- <https://stackoverflow.com/questions/62935967/spring-boot-oauth2-clientreactive-mutual-tls-ssl-token-uri>
- <https://stackoverflow.com/questions/45418523/spring-5-webclient-using-ssl>
- <https://stackoverflow.com/questions/62935967/spring-boot-oauth2-clientreactive-mutual-tls-ssl-token-uri>
- <https://stackoverflow.com/questions/17173559/how-to-verify-if-java-sends-the-client-certificate-in-a-mutual-auth-scenario> => useful to check Mutual authentication in debug mode
I found out the following solution :
```java
@Configuration
@EnableWebFluxSecurity
public class Oauth2ClientConfig {
// Bearer Token management based on OAuth 2.0 client credentials grant flow
@Bean
ReactiveClientRegistrationRepository getRegistration() {
ClientRegistration registration = ClientRegistration
.withRegistrationId("credentialProvider")
.tokenUri("https://rhsso:8446/auth/token")
.clientId("client_id").clientSecret("client_secret")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
// Bearer Token management using HTTPS
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService) throws IOException, KeyStoreException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
final FileInputStream keyStoreFile = new FileInputStream("C:\\keystore\\client_keystore.p12"); // client private key and client certificate
final FileInputStream trustStoreFile = new FileInputStream("C:\\truststore\\server_truststore.p12");
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
final KeyStore trustStore = KeyStore.getInstance("PKCS12");
final KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String keyStorePassword = "keyStorePassword";
String trustStorePassword = "trustStorePassword";
keyStore.load(keyStoreFile, keyStorePassword.toCharArray());
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
trustStore.load(trustStoreFile, trustStorePassword.toCharArray());
trustManagerFactory.init(trustStore);
// HTTPS management through sslContext
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().keyManager(keyManagerFactory)
.trustManager(trustManagerFactory);
SslContext sslContext = sslContextBuilder.build();
HttpClient httpSslClient = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext));
ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpSslClient);
accessTokenResponseClient.setWebClient(WebClient.builder().clientConnector(httpConnector).build());
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().clientCredentials(c -> {
c.accessTokenResponseClient(accessTokenResponseClient);
}).build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
@Bean("CustomWebClient") //Qualifier to be used by my dedicated ClientAPI service in order to use the right one among available WebClient beans
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) throws KeyStoreException,
NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauthFunction = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauthFunction.setDefaultClientRegistrationId("credentialProvider"); // Link to Bearer token managed above through Its ID
final FileInputStream keyStoreFile = new FileInputStream("C:\\keystore\\business_client_keystore.p12"); // client private key and client certificate
final FileInputStream trustStoreFile = new FileInputStream("C:\\truststore\\business_server_truststore.p12");
final KeyStore keyStore = KeyStore.getInstance("PKCS12");
final KeyStore trustStore = KeyStore.getInstance("PKCS12");
final KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
String keyStorePassword = "business_keyStorePassword";
String trustStorePassword = "business_trustStorePassword";
keyStore.load(keyStoreFile, keyStorePassword.toCharArray());
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
trustStore.load(trustStoreFile, trustStorePassword.toCharArray());
trustManagerFactory.init(trustStore);
// Create Netty SslContext, HttpClient
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().keyManager(keyManagerFactory)
.trustManager(trustManagerFactory);
SslContext sslContext = sslContextBuilder.build();
HttpClient httpSslClient = HttpClient.create().secure(ssl -> ssl.sslContext(sslContext));
// Create Reactive ClientHttpConnector & WebClient
final ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpSslClient);
return WebClient.builder().clientConnector(httpConnector).filter(oauthFunction).build();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// disable csrf with frontend interface but generally not recommended otherwise only in special cases
// cors management in order to accept FrontEnd application installed localhost and only POST method is allowed
http.csrf().disable().cors(corsCustomizer -> {
CorsConfigurationSource configurationSource = request->{
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(List.of("localhost:8080"));
corsConfiguration.setAllowedMethods(List.of("POST"));
return corsConfiguration;
};
corsCustomizer.configurationSource(configurationSource);
}).oauth2Client();
return http.build();
}
}
My webclient based on my beans above :
@Service
public class ClientAPI {
private WebClient webClient;
private String businessServiceUrl;
@Autowired
public ClientAPI(@Qualifier("CustomWebClient") WebClient webClient) {
this.businessServiceUrl = "https://hostServerName:8081/theService";
this.webClient = webClient;
}
public Mono<Response> sendRequest(Request bodyRequest) {
Mono<Response> result = this.webClient.post().uri(this.businessServiceUrl)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(Mono.just(bodyRequest), Request.class).retrieve().bodyToMono(Response.class);
return result;
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论