Spring Boot Sping Boot 向ServerOAuth2AuthorizedClientExchangeFilterFunction中的WebClient请求添加附加属性

hof1towb  于 2023-11-17  发布在  Spring
关注(0)|答案(5)|浏览(153)

我正在尝试实现client_credentials grant以在我的spring Boot 资源服务器中获取token。我使用Auth 0作为授权服务器。他们似乎需要在请求主体中添加一个额外的参数,称为audience。
我已经尝试通过postman完成请求,并且成功了。我现在正在尝试在Spring中重现它。

  1. curl -X POST \
  2. https://XXX.auth0.com/oauth/token \
  3. -H 'Content-Type: application/x-www-form-urlencoded' \
  4. -d 'grant_type=client_credentials&audience=https%3A%2F%2Fxxxxx.auth0.com%2Fapi%2Fv2%2F&client_id=SOME_CLIENT_ID&client_secret=SOME_CLIENT_SECRET'

字符串
我面临的问题是,我没有办法将缺少的受众参数添加到令牌请求中。
我在我的application.yml中定义了一个配置

  1. client:
  2. provider:
  3. auth0:
  4. issuer-uri: https://XXXX.auth0.com//
  5. registration:
  6. auth0-client:
  7. provider: auth0
  8. client-id: Client
  9. client-secret: Secret
  10. authorization_grant_type: client_credentials
  11. auth0:
  12. client-id: Client
  13. client-secret: Secret


我有这样配置的Web客户端过滤器。

  1. @Bean
  2. WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
  3. ServerOAuth2AuthorizedClientRepository authorizedClients) {
  4. ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
  5. clientRegistrations, authorizedClients);
  6. oauth2.setDefaultClientRegistrationId("auth0");
  7. return WebClient.builder()
  8. .filter(oauth2)
  9. .build();
  10. }


我注入示例,并试图做一个请求,以获得用户通过电子邮件

  1. return this.webClient.get()
  2. .uri(this.usersUrl + "/api/v2/users-by-email?email={email}", email)
  3. .attributes(auth0ClientCredentials())
  4. .retrieve()
  5. .bodyToMono(User.class);


据我所知,过滤器拦截了这个userByEmail请求,在执行它之前,它试图执行/oauth/token请求以获取JWT承载令牌,它可以附加到第一个令牌并执行它。
有没有一种方法来添加一个参数到过滤器?它一直非常困难,通过它,并找出确切的参数被附加,因为它的React和我在这方面相当新。甚至一些指针,在哪里看会有帮助。

ttisahbt

ttisahbt1#

我遇到了同样的问题,访问令牌响应和请求不符合oAuth2标准。下面是我的代码(它在Kotlin中,但对于java开发人员来说也应该是可以理解的),用于spring Boot 版本2.3.6.RELEASE。Gradle依赖项:

  1. implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}"))
  2. implementation("org.springframework.boot:spring-boot-starter-webflux")
  3. implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

字符串
在添加它们之后,你必须首先创建你的自定义令牌请求/响应客户端,它将实现ReactiveOAuth2AccessTokenResponseClient接口:

  1. class CustomTokenResponseClient : ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
  2. private val webClient = WebClient.builder().build()
  3. override fun getTokenResponse(
  4. authorizationGrantRequest: OAuth2ClientCredentialsGrantRequest
  5. ): Mono<OAuth2AccessTokenResponse> =
  6. webClient.post()
  7. .uri(authorizationGrantRequest.clientRegistration.providerDetails.tokenUri)
  8. .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
  9. .bodyValue(CustomTokenRequest(
  10. clientId = authorizationGrantRequest.clientRegistration.clientId,
  11. clientSecret = authorizationGrantRequest.clientRegistration.clientSecret
  12. ))
  13. .exchange()
  14. .flatMap { it.bodyToMono<NotStandardTokenResponse>() }
  15. .map { it.toOAuth2AccessTokenResponse() }
  16. private fun NotStandardTokenResponse.toOAuth2AccessTokenResponse() = OAuth2AccessTokenResponse
  17. .withToken(this.accessToken)
  18. .refreshToken(this.refreshToken)
  19. .expiresIn(convertExpirationDateToDuration(this.data.expires).toSeconds())
  20. .tokenType(OAuth2AccessToken.TokenType.BEARER)
  21. .build()
  22. }


正如您在上面看到的,在这个类中,您可以根据您的特定需求调整令牌请求/响应处理。
注意事项:getTokenResponse方法中的authorizationGrantRequest参数。Spring在这里传递来自应用程序属性的数据,因此在定义它们时遵循标准,例如,它们可能看起来像这样:

  1. spring:
  2. security:
  3. oauth2:
  4. client:
  5. registration:
  6. name-for-oauth-integration:
  7. authorization-grant-type: client_credentials
  8. client-id: id
  9. client-secret: secret
  10. provider:
  11. name-for-oauth-integration:
  12. token-uri: https://oauth.com/token


最后一步是在oAuth2配置中使用CustomTokenResponseClient,它可能看起来像这样:

  1. @Configuration
  2. class CustomOAuth2Configuration {
  3. @Bean
  4. fun customOAuth2WebWebClient(clientRegistrations: ReactiveClientRegistrationRepository): WebClient {
  5. val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(
  6. clientRegistrations.findByRegistrationId("name-for-oauth-integration").block()
  7. )
  8. val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)
  9. val authorizedClientManager =
  10. AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
  11. val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
  12. authorizedClientProvider.setAccessTokenResponseClient(CustomTokenResponseClient())
  13. authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
  14. val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
  15. oauthFilter.setDefaultClientRegistrationId("name-for-oauth-integration")
  16. return WebClient.builder()
  17. .filter(oauthFilter)
  18. .build()
  19. }
  20. }

展开查看全部
b4qexyjb

b4qexyjb2#

现在,这是可能的,但不是优雅的。
请注意,您可以为ServerOAuth2AuthorizedClientExchangeFilterFunction提供自定义的ReactiveOAuth2AccessTokenResponseClient
您可以通过复制WebClientReactiveClientCredentialsTokenResponseClient的内容来创建自己的实现,从而添加所需的任何其他参数。
也就是说,如果有一个setter来使其更方便,那就更好了。

igetnqfo

igetnqfo3#

下面是我在进一步调查后发现的。我的问题中描述的代码永远不会调用client_credentials并适合我的用例。我认为(不是100%确定)如果我试图在微服务架构中的多个服务中传播用户提交的令牌,它将在未来非常有用。像这样的动作链浮现在脑海中:
用户调用服务A ->服务A调用服务B ->服务B响应->服务A响应用户请求。
并使用相同的令牌开始与通过整个过程。

我的用例解决方案:

我所做的是在很大程度上基于原始类创建一个新的Filter类,并在执行请求之前执行一个步骤,检查是否存储了可用于Auth 0 Management API的JWT令牌。如果没有,我构建client_credentials grant请求并获取一个,然后把这个token作为一个bearer附加到初始请求上,然后执行那个请求。我还在-内存缓存机制,以便如果令牌有效,以后的任何其他请求都将使用它。

过滤器

  1. public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {
  2. private ReactiveClientRegistrationRepository clientRegistrationRepository;
  3. /**
  4. * Required by auth0 when requesting a client credentials token
  5. */
  6. private String audience;
  7. private String clientRegistrationId;
  8. private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;
  9. public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
  10. String clientRegistrationId,
  11. String audience) {
  12. this.clientRegistrationRepository = clientRegistrationRepository;
  13. this.audience = audience;
  14. this.clientRegistrationId = clientRegistrationId;
  15. this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
  16. }
  17. public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
  18. this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
  19. }
  20. @Override
  21. public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
  22. return auth0ClientCredentialsToken(next)
  23. .map(token -> bearer(request, token.getTokenValue()))
  24. .flatMap(next::exchange)
  25. .switchIfEmpty(next.exchange(request));
  26. }
  27. private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
  28. return Mono.defer(this::loadClientRegistration)
  29. .map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
  30. .flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
  31. .switchIfEmpty(refreshAuth0Token(request, next)));
  32. }
  33. private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
  34. ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
  35. String tokenUri = clientRegistration
  36. .getProviderDetails().getTokenUri();
  37. ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
  38. .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
  39. .body(clientCredentialsTokenBody(clientCredentialsRequest))
  40. .build();
  41. return next.exchange(clientCredentialsTokenRequest)
  42. .flatMap(response -> response.body(oauth2AccessTokenResponse()))
  43. .map(OAuth2AccessTokenResponse::getAccessToken)
  44. .doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
  45. }
  46. private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
  47. ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
  48. return BodyInserters
  49. .fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
  50. .with("client_id", clientRegistration.getClientId())
  51. .with("client_secret", clientRegistration.getClientSecret())
  52. .with("audience", clientCredentialsRequest.getAudience());
  53. }
  54. private Mono<ClientRegistration> loadClientRegistration() {
  55. return Mono.just(clientRegistrationId)
  56. .flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
  57. }
  58. private ClientRequest bearer(ClientRequest request, String token) {
  59. return ClientRequest.from(request)
  60. .headers(headers -> headers.setBearerAuth(token))
  61. .build();
  62. }
  63. static class ClientCredentialsRequest {
  64. private final ClientRegistration clientRegistration;
  65. private final String audience;
  66. public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
  67. this.clientRegistration = clientRegistration;
  68. this.audience = audience;
  69. }
  70. public ClientRegistration getClientRegistration() {
  71. return clientRegistration;
  72. }
  73. public String getAudience() {
  74. return audience;
  75. }
  76. }
  77. }

字符串

代币存储

  1. public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {
  2. private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
  3. private Clock clock = Clock.systemUTC();
  4. private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);
  5. public Auth0InMemoryAccessTokenStore() {
  6. }
  7. @Override
  8. public Mono<OAuth2AccessToken> retrieveToken() {
  9. return Mono.justOrEmpty(token.get())
  10. .filter(Objects::nonNull)
  11. .filter(token -> token.getExpiresAt() != null)
  12. .filter(token -> {
  13. Instant now = this.clock.instant();
  14. Instant expiresAt = token.getExpiresAt();
  15. if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
  16. return true;
  17. }
  18. return false;
  19. });
  20. }
  21. @Override
  22. public Mono<Void> storeToken(OAuth2AccessToken token) {
  23. this.token.set(token);
  24. return Mono.empty();
  25. }
  26. }

令牌存储接口

  1. public interface ReactiveInMemoryAccessTokenStore {
  2. Mono<OAuth2AccessToken> retrieveToken();
  3. Mono<Void> storeToken(OAuth2AccessToken token);
  4. }


最后定义并使用这些bean。

  1. @Bean
  2. public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
  3. @Value("${auth0.client-registration-id}") String clientRegistrationId,
  4. @Value("${auth0.audience}") String audience) {
  5. return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
  6. }
  7. @Bean(name = "auth0-webclient")
  8. WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
  9. return WebClient.builder()
  10. .filter(filter)
  11. .build();
  12. }


此时令牌存储有一个小问题,因为client_credentials令牌请求将在同时到来的并行请求上执行多个,但我可以在可预见的未来接受这一点。

展开查看全部
fhity93d

fhity93d4#

您的application.yml缺少一个变量:client-authentication-method:post
应该是这样的:

  1. spring:
  2. security:
  3. oauth2:
  4. client:
  5. provider:
  6. auth0-client:
  7. token-uri: https://XXXX.auth0.com//
  8. registration:
  9. auth0-client:
  10. client-id: Client
  11. client-secret: Secret
  12. authorization_grant_type: client_credentials
  13. client-authentication-method: post

字符串
如果没有它,我总是得到“invalid_client”的响应。
在Spring罩2.7.2中进行测试

展开查看全部
k2arahey

k2arahey5#

还有另一种方法可以解决这个问题。如果对auth0.com API的client_credentials令牌请求在请求主体中不包括audience字段,那么auth0.com将默认为https://jwtresourceapi。因此,为了能够使用WebClient,上面@DArkO提供的(令人惊叹的)解决方案,您可以在https://jwtresourceapi的受众中使用auth0.com创建所有API。
我不知道你还需要什么,但我所做的只是在auth0.com中定义一个API,我可以有多个应用程序。只要应用程序在API的机器对机器应用程序中被授权,它就可以工作。
请记住,auth0.com只是一个开发者的Playground,我不会用它来创建生产就绪的应用程序。
PS>我还必须在配置文件中包含authorization-grant-type: client_credentials,尽管配置bean示例说我不需要它。也许我稍后会弄清楚,但现在这是有效的。

相关问题