Spring Boot在ServerOAuth2AuthorizedClientExchangeFilterFunction中向WebClient请求添加其他属性

时间:2018-11-28 17:02:28

标签: spring-boot spring-security-oauth2 auth0

我正在尝试实现client_credentials授予以在我的Spring Boot资源服务器中获取令牌。 我正在使用 Auth0 作为授权服务器。他们似乎需要在请求正文中添加一个额外的参数,称为“受众”。

我尝试通过邮递员提出请求,并且该请求有效。我现在正在尝试在Spring内重现它。这是工作中的邮递员请求

curl -X POST \
  https://XXX.auth0.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -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中定义了一个配置

client:
    provider:
      auth0:
        issuer-uri: https://XXXX.auth0.com//
    registration:
      auth0-client:
        provider: auth0
        client-id: Client
        client-secret: Secret
        authorization_grant_type: client_credentials
      auth0:
        client-id: Client
        client-secret: Secret

我已将Web客户端过滤器配置如下。

@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
                    ServerOAuth2AuthorizedClientRepository authorizedClients) {
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
            clientRegistrations, authorizedClients);
    oauth2.setDefaultClientRegistrationId("auth0");
    return WebClient.builder()
            .filter(oauth2)
            .build();
}

我正在注入实例并尝试发出通过电子邮件获取用户的请求

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

按照我的理解,过滤器会拦截此userByEmail请求,并在执行之前尝试执行/ oauth / token请求以获取JWT Bearer令牌,该令牌可以附加到第一个令牌并执行。

是否可以将参数添加到过滤器?由于它是被动式的,因此很难遍历它并弄清楚参数到底要附加在哪里,这是很新的。甚至一些指向哪里的指针也会很有帮助。

3 个答案:

答案 0 :(得分:1)

现在,这是可能的,但还不够优雅。

请注意,您可以为ReactiveOAuth2AccessTokenResponseClient提供自定义ServerOAuth2AuthorizedClientExchangeFilterFunction

您可以创建自己的实现-通过复制WebClientReactiveClientCredentialsTokenResponseClient的内容来添加所需的其他参数。

也就是说,最好有一个二传手使它更方便。您可以在Spring Security的待办事项列表中关注the corresponding issue

答案 1 :(得分:1)

这是我进一步调查后发现的。我的问题中描述的代码永远不会调用client_credentials并适合我的用例。我认为(对此不是100%肯定),如果我试图在微服务体系结构中的多个服务之间传播用户提交的令牌,它将在将来很有用。想到这样的一系列动作:

用户呼叫服务A->服务A呼叫服务B->服务B响应->服务A响应用户请求。

并使用相同的令牌开始整个过程​​。

我的用例解决方案:

我所做的是在很大程度上基于原始内容创建了一个新的Filter类,并在执行请求之前执行了一个步骤,在此我检查是否存储了可用于Auth0管理API的JWT令牌。如果我不这样做,则建立client_credentials授予请求并获得一个,然后将此令牌作为承载附加到初始请求并执行该请求。我还添加了一个小的令牌内存缓存机制,以便如果令牌有效,以后的任何其他请求都将使用它。这是我的代码。

过滤器

public class Auth0ClientCredentialsGrantFilterFunction implements ExchangeFilterFunction {

    private ReactiveClientRegistrationRepository clientRegistrationRepository;

    /**
     * Required by auth0 when requesting a client credentials token
     */
    private String audience;

    private String clientRegistrationId;

    private Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore;

    public Auth0ClientCredentialsGrantFilterFunction(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                     String clientRegistrationId,
                                                     String audience) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.audience = audience;
        this.clientRegistrationId = clientRegistrationId;
        this.auth0InMemoryAccessTokenStore = new Auth0InMemoryAccessTokenStore();
    }

    public void setAuth0InMemoryAccessTokenStore(Auth0InMemoryAccessTokenStore auth0InMemoryAccessTokenStore) {
        this.auth0InMemoryAccessTokenStore = auth0InMemoryAccessTokenStore;
    }

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        return auth0ClientCredentialsToken(next)
                .map(token -> bearer(request, token.getTokenValue()))
                .flatMap(next::exchange)
                .switchIfEmpty(next.exchange(request));
    }

    private Mono<OAuth2AccessToken> auth0ClientCredentialsToken(ExchangeFunction next) {
        return Mono.defer(this::loadClientRegistration)
                .map(clientRegistration -> new ClientCredentialsRequest(clientRegistration, audience))
                .flatMap(request -> this.auth0InMemoryAccessTokenStore.retrieveToken()
                        .switchIfEmpty(refreshAuth0Token(request, next)));
    }

    private Mono<OAuth2AccessToken> refreshAuth0Token(ClientCredentialsRequest clientCredentialsRequest, ExchangeFunction next) {
        ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
        String tokenUri = clientRegistration
                .getProviderDetails().getTokenUri();
        ClientRequest clientCredentialsTokenRequest = ClientRequest.create(HttpMethod.POST, URI.create(tokenUri))
                .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .body(clientCredentialsTokenBody(clientCredentialsRequest))
                .build();
        return next.exchange(clientCredentialsTokenRequest)
                .flatMap(response -> response.body(oauth2AccessTokenResponse()))
                .map(OAuth2AccessTokenResponse::getAccessToken)
                .doOnNext(token -> this.auth0InMemoryAccessTokenStore.storeToken(token));
    }

    private static BodyInserters.FormInserter<String> clientCredentialsTokenBody(ClientCredentialsRequest clientCredentialsRequest) {
        ClientRegistration clientRegistration = clientCredentialsRequest.getClientRegistration();
        return BodyInserters
                .fromFormData("grant_type", AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
                .with("client_id", clientRegistration.getClientId())
                .with("client_secret", clientRegistration.getClientSecret())
                .with("audience", clientCredentialsRequest.getAudience());
    }

    private Mono<ClientRegistration> loadClientRegistration() {
        return Mono.just(clientRegistrationId)
                .flatMap(r -> clientRegistrationRepository.findByRegistrationId(r));
    }

    private ClientRequest bearer(ClientRequest request, String token) {
        return ClientRequest.from(request)
                .headers(headers -> headers.setBearerAuth(token))
                .build();
    }


    static class ClientCredentialsRequest {
        private final ClientRegistration clientRegistration;
        private final String audience;

        public ClientCredentialsRequest(ClientRegistration clientRegistration, String audience) {
            this.clientRegistration = clientRegistration;
            this.audience = audience;
        }

        public ClientRegistration getClientRegistration() {
            return clientRegistration;
        }

        public String getAudience() {
            return audience;
        }
    }

}

令牌存储

public class Auth0InMemoryAccessTokenStore implements ReactiveInMemoryAccessTokenStore {

    private AtomicReference<OAuth2AccessToken> token = new AtomicReference<>();
    private Clock clock = Clock.systemUTC();
    private Duration accessTokenExpiresSkew = Duration.ofMinutes(1);

    public Auth0InMemoryAccessTokenStore() {
    }

    @Override
    public Mono<OAuth2AccessToken> retrieveToken() {
        return Mono.justOrEmpty(token.get())
                .filter(Objects::nonNull)
                .filter(token -> token.getExpiresAt() != null)
                .filter(token -> {
                    Instant now = this.clock.instant();
                    Instant expiresAt = token.getExpiresAt();
                    if (now.isBefore(expiresAt.minus(this.accessTokenExpiresSkew))) {
                        return true;
                    }
                    return false;
                });
    }

    @Override
    public Mono<Void> storeToken(OAuth2AccessToken token) {
        this.token.set(token);
        return Mono.empty();
    }
}

令牌存储界面

public interface ReactiveInMemoryAccessTokenStore {
    Mono<OAuth2AccessToken> retrieveToken();

    Mono<Void> storeToken(OAuth2AccessToken token);
}

最后定义bean并使用它。

    @Bean
    public Auth0ClientCredentialsGrantFilterFunction auth0FilterFunction(ReactiveClientRegistrationRepository clientRegistrations,
                                                                         @Value("${auth0.client-registration-id}") String clientRegistrationId,
                                                                         @Value("${auth0.audience}") String audience) {
        return new Auth0ClientCredentialsGrantFilterFunction(clientRegistrations, clientRegistrationId, audience);
    }

    @Bean(name = "auth0-webclient")
    WebClient webClient(Auth0ClientCredentialsGrantFilterFunction filter) {
        return WebClient.builder()
                .filter(filter)
                .build();
    }

此时令牌存储区存在一个小问题,因为client_credentials令牌请求将在同时出现的多个并行请求上执行,但在可预见的将来我可以忍受。

答案 2 :(得分:0)

我遇到了同样的问题,访问令牌响应和请求不遵循 oAuth2 标准。这是我的 Spring Boot 版本 2.3.6.RELEASE 的代码(它在 kotlin 中,但对于 Java 开发者来说也应该是可以理解的)。 Gradle 依赖项:

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

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

class CustomTokenResponseClient : ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {

    private val webClient = WebClient.builder().build()

    override fun getTokenResponse(
            authorizationGrantRequest: OAuth2ClientCredentialsGrantRequest
    ): Mono<OAuth2AccessTokenResponse> =
            webClient.post()
                    .uri(authorizationGrantRequest.clientRegistration.providerDetails.tokenUri)
                    .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                    .bodyValue(CustomTokenRequest(
                            clientId = authorizationGrantRequest.clientRegistration.clientId,
                            clientSecret = authorizationGrantRequest.clientRegistration.clientSecret
                    ))
                    .exchange()
                    .flatMap { it.bodyToMono<NotStandardTokenResponse>() }
                    .map { it.toOAuth2AccessTokenResponse() }


    private fun NotStandardTokenResponse.toOAuth2AccessTokenResponse() = OAuth2AccessTokenResponse
            .withToken(this.accessToken)
            .refreshToken(this.refreshToken)
            .expiresIn(convertExpirationDateToDuration(this.data.expires).toSeconds())
            .tokenType(OAuth2AccessToken.TokenType.BEARER)
            .build()

}

如上所示,在本课程中,您可以根据自己的特定需求调整令牌请求/响应处理。

注意:authorizationGrantRequest 方法中的 getTokenResponse 参数。 Spring 在这里传递来自您的应用程序属性的数据,因此在定义它们时请遵循标准,例如它们可能如下所示:

spring:
  security:
    oauth2:
      client:
        registration:
          name-for-oauth-integration:
            authorization-grant-type: client_credentials
            client-id: id
            client-secret: secret
        provider:
          name-for-oauth-integration:
            token-uri: https://oauth.com/token

最后一步是在 oAuth2 配置中使用您的 CustomTokenResponseClient,它可能如下所示:

@Configuration
class CustomOAuth2Configuration {

    @Bean
    fun customOAuth2WebWebClient(clientRegistrations: ReactiveClientRegistrationRepository): WebClient {
        val clientRegistryRepo = InMemoryReactiveClientRegistrationRepository(
                clientRegistrations.findByRegistrationId("name-for-oauth-integration").block()
        )
        val clientService = InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo)

        val authorizedClientManager =
                AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService)
        val authorizedClientProvider = ClientCredentialsReactiveOAuth2AuthorizedClientProvider()
        authorizedClientProvider.setAccessTokenResponseClient(CustomTokenResponseClient())
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)

        val oauthFilter = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager)
        oauthFilter.setDefaultClientRegistrationId("name-for-oauth-integration")

        return WebClient.builder()
                .filter(oauthFilter)
                .build()
    }

}