spring-security - 如何使用带有 WebClient 的 spring-security-oauth2 自定义 OAuth2 token 请求的 Authorization header ?

标签 spring-security spring-webflux spring-security-oauth2 spring-webclient

我正在尝试通过 WebClient 调用升级到 spring security 5.5.1。
我发现 oauth2 clientId 和 secret 现在是 URL 编码的 AbstractWebClientReactiveOAuth2AccessTokenResponseClient ,但我的 token 提供程序不支持此功能(例如,如果 secret 包含 + 字符,则仅当它作为 + 而不是作为 %2B 发送时才有效)。
我知道这被视为 bug fix from spring-security side ),但我不能让 token 提供者轻易改变它的行为。
所以我试图找到一种方法来解决这个问题。
当您使用 WebClient 配置(这是我的情况)时,关于如何自定义访问 token 请求的 [文档] ( https://docs.spring.io/spring-security/site/docs/current/reference/html5/#customizing-the-access-token-request ) 似乎并不适用。
为了删除 clientid/secret 编码,我不得不从 AbstractWebClientReactiveOAuth2AccessTokenResponseClient 扩展和复制大部分现有代码。定制 WebClientReactiveClientCredentialsTokenResponseClient因为其中大部分都具有私有(private)/默认可见性。
我在 enhancement issue 中追踪了这个在 spring-security 项目中。
是否有更简单的方法来自定义 token 请求的 Authorization header ,以跳过 url 编码?

最佳答案

一些围绕定制的 API 肯定有改进的余地,而且来自社区的这些类型的问题/请求/问题肯定会继续帮助突出这些领域。
关于AbstractWebClientReactiveOAuth2AccessTokenResponseClient特别是,目前无法覆盖内部方法来填充 Authorization 中的基本身份验证凭据。标题。但是,您可以自定义 WebClient用于进行 API 调用。如果它在您的用例中是可以接受的(暂时,在解决行为更改和/或添加自定义选项时)您应该能够拦截 WebClient 中的请求。 .
这是一个将创建 WebClient 的配置能够使用 OAuth2AuthorizedClient :

@Configuration
public class WebClientConfiguration {

    @Bean
    public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        // @formatter:off
        ServerOAuth2AuthorizedClientExchangeFilterFunction exchangeFilterFunction =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        exchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true);

        return WebClient.builder()
                .filter(exchangeFilterFunction)
                .build();
        // @formatter:on
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
        // @formatter:off
        WebClientReactiveClientCredentialsTokenResponseClient accessTokenResponseClient =
                new WebClientReactiveClientCredentialsTokenResponseClient();
        accessTokenResponseClient.setWebClient(createAccessTokenResponseWebClient());

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .clientCredentials(consumer ->
                                consumer.accessTokenResponseClient(accessTokenResponseClient)
                                        .build())
                        .build();

        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        // @formatter:on

        return authorizedClientManager;
    }

    protected WebClient createAccessTokenResponseWebClient() {
        // @formatter:off
        return WebClient.builder()
                .filter((clientRequest, exchangeFunction) -> {
                    HttpHeaders headers = clientRequest.headers();
                    String authorizationHeader = headers.getFirst("Authorization");
                    Assert.notNull(authorizationHeader, "Authorization header cannot be null");
                    Assert.isTrue(authorizationHeader.startsWith("Basic "),
                            "Authorization header should start with Basic");
                    String encodedCredentials = authorizationHeader.substring("Basic ".length());
                    byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
                    String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
                    Assert.isTrue(credentialsString.contains(":"), "Decoded credentials should contain a \":\"");
                    String[] credentials = credentialsString.split(":");
                    String clientId = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8);
                    String clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8);

                    ClientRequest newClientRequest = ClientRequest.from(clientRequest)
                            .headers(httpHeaders -> httpHeaders.setBasicAuth(clientId, clientSecret))
                            .build();
                    return exchangeFunction.exchange(newClientRequest);
                })
                .build();
        // @formatter:on
    }

}
此测试表明凭据已针对内部访问 token 响应 WebClient 进行解码:
@ExtendWith(MockitoExtension.class)
public class WebClientConfigurationTests {

    private WebClientConfiguration webClientConfiguration;

    @Mock
    private ExchangeFunction exchangeFunction;

    @Captor
    private ArgumentCaptor<ClientRequest> clientRequestCaptor;

    @BeforeEach
    public void setUp() {
        webClientConfiguration = new WebClientConfiguration();
    }

    @Test
    public void exchangeWhenBasicAuthThenDecoded() {
        WebClient webClient = webClientConfiguration.createAccessTokenResponseWebClient()
                .mutate()
                .exchangeFunction(exchangeFunction)
                .build();
        when(exchangeFunction.exchange(any(ClientRequest.class)))
                .thenReturn(Mono.just(ClientResponse.create(HttpStatus.OK).build()));

        webClient.post()
                .uri("/oauth/token")
                .headers(httpHeaders -> httpHeaders.setBasicAuth("aladdin", URLEncoder.encode("open sesame", StandardCharsets.UTF_8)))
                .retrieve()
                .bodyToMono(Void.class)
                .block();

        verify(exchangeFunction).exchange(clientRequestCaptor.capture());

        ClientRequest clientRequest = clientRequestCaptor.getValue();
        String authorizationHeader = clientRequest.headers().getFirst("Authorization");
        assertThat(authorizationHeader).isNotNull();
        String encodedCredentials = authorizationHeader.substring("Basic ".length());
        byte[] decodedBytes = Base64.getDecoder().decode(encodedCredentials);
        String credentialsString = new String(decodedBytes, StandardCharsets.UTF_8);
        String[] credentials = credentialsString.split(":");

        assertThat(credentials[0]).isEqualTo("aladdin");
        assertThat(credentials[1]).isEqualTo("open sesame");
    }

}

关于spring-security - 如何使用带有 WebClient 的 spring-security-oauth2 自定义 OAuth2 token 请求的 Authorization header ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68304980/

相关文章:

使用 mongodb 进行 Spring session 复制未按预期工作

spring-boot - 如何在 spring-webflux RouterFunction 端点中使用 OpenApi 注释?

spring - 尝试解析 AuthenticationManager(Spring Security 和 Oauth)时检测到依赖循环

spring-mvc - 构造函数抛出异常;嵌套异常是 java.lang.NoSuchMethodError : org. springframework.util.AntPathMatcher.setCaseSensitive(Z)V

java - Cometd,Spring 安全 : Currently authenticated user not available inside a Listener

java - (...) 中的字段 memberRepo 需要无法找到类型的 bean

spring-webflux - 在 webflux 中读取请求正文

spring-mvc - OAuth2 Spring Security - OAuth2AuthenticationProcessingFilter 不工作

spring-boot - 使用 Spring Security 将数据库和 SAML 身份验证合并到一个应用程序中

java - Spring webflux 中的异常处理