首页 > 解决方案 > 如何使用 WebClient“serverWebExchange 不能为空”从 Spring Boot Java 客户端调用 Oauth2 保护端点

问题描述

要求是使用 WebClient 从 java 客户端程序调用 OAuth 保护端点。我正在使用使用反应性对象的密码授予授权类型。请注意,我对反应式编程模式非常陌生。当我进行 webclient 调用时,我收到以下错误 ** serverWebExchange 不能为空 **

请让我知道以下配置和使用是否正确和/或如何初始化 serverWebExchange 对象。

        String data = webClient
                .post().uri(endPoint)
                // This will add the Authorization header with the bearer token.
                .attributes(ServerOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("custom"))
                .body(Mono.just(alert), Alert.class)
                .retrieve()
                .bodyToMono(String.class)
                // Block until we receive a response for a non-reactive client.
                .block();
java.lang.IllegalArgumentException: serverWebExchange cannot be null

    at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$loadAuthorizedClient$9(DefaultReactiveOAuth2AuthorizedClientManager.java:102)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Request to POST http://<hostname>/<protected_endpoint> [DefaultWebClient]
Stack trace:
        at org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager.lambda$loadAuthorizedClient$9(DefaultReactiveOAuth2AuthorizedClientManager.java:102)
        at reactor.core.publisher.MonoErrorSupplied.subscribe(MonoErrorSupplied.java:70)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4210)
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:75)
    .....
        at reactor.core.publisher.Mono.block(Mono.java:1665)
        at com.xxxxx.oauthclient.ApplicationTests.testWebClient(ApplicationTests.java:41)
    ....
        at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
        at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
        at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
        at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
        at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
        at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
    Suppressed: java.lang.Exception: #block terminated with an error
        at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)
        at reactor.core.publisher.Mono.block(Mono.java:1666)
        at com.xxxx.oauthclient.ApplicationTests.testWebClient(ApplicationTests.java:41)
        ....

我使用了以下配置

@Configuration
public class ReactiveOAuthConfig {
    @Value("${oauth.username}") String username;
    @Value("${oauth.password}") String password;

    @Bean
    WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);

        return WebClient.builder()
                .filter(oauth)
                .build();
    }
    @Bean
    ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .password()
                        .build();
        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        // For the `password` grant, the `username` and `password` are supplied via request parameters,
        // so map it to `OAuth2AuthorizationContext.getAttributes()`.
        authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

        return authorizedClientManager;
    }

    private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper() {
        return authorizeRequest -> {
            Map<String, Object> contextAttributes = Collections.emptyMap();
            if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
                contextAttributes = new HashMap<>();

                // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
                contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
                contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
            }
            return  Mono.just(contextAttributes);
        };
    }

应用程序属性看起来像这样

spring.security.oauth2.client.provider.custom.token-uri=https://endpoint_to_get_token
spring.security.oauth2.client.registration.custom.client-id=<clientid>
spring.security.oauth2.client.registration.custom.client-secret=<client-secret>
spring.security.oauth2.client.registration.custom.client-authentication-method=post
spring.security.oauth2.client.registration.custom.authorization-grant-type=password
spring.security.oauth2.client.registration.custom.scope=AppIdClaimsTrust

版本:spring boot 版本:2.2.6.RELEASE <spring-security.version>5.2.2.RELEASE</spring-security.version>

注意:我能够验证从 Web 客户端调用 Oauth 保护端点,如本https://github.com/jgrandja/spring-security-oauth-5-2-migrate/tree/master/client-app中所述

标签: spring-bootoauth-2.0spring-security-oauth2spring-webfluxspring-webclient

解决方案


在构建ReactiveOAuth2AuthorizedClientProvider时,您不应该使用 authorizationCode 。并且刷新令牌不应该用于机器对机器的通信。

最重要的是不要使用DefaultReactiveOAuth2AuthorizedClientManager。而是使用

AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
        new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
            clientRegistrationRepository, authorizedClientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

推荐阅读