簡體   English   中英

為什么 Spring Boot WebClient OAuth2 (client_credentials) 要求為每個請求提供一個新令牌?

[英]Why Spring Boot WebClient OAuth2 (client_credentials) asks for a new token for each request?

我正在嘗試創建一個 Spring 啟動 REST 應用程序,該應用程序必須對另一個受 OAuth2 保護的應用程序進行遠程 REST 調用。

第一個應用程序使用 Reactive WebClient 調用第二個 OAuth2 REST 應用程序。 我已經用grant_type“client_credentials”配置了WebClient。

應用程序.yml

spring:
  security:
    oauth2:
      client:
        provider:
          client-registration-id:
            token-uri: http://localhost:8080/oauth/token
        registration:
          client-registration-id:
            authorization-grant-type: client_credentials
            client-id: public
            client-secret: test
            client-authentication-method: post
            scope: myscope
@Configuration
public class WebClientConfig {

    @Bean
    WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId("client-registration-id");
        return WebClient.builder().filter(oauth).build();
    }

}
@Component
public class WebClientChronJob {

    Logger logger = LoggerFactory.getLogger(WebClientChronJob.class);

    @Autowired
    private WebClient webClient;

    @Scheduled(fixedRate = 5000)
    public void logResourceServiceResponse() {

        webClient.get()
                .uri("http://localhost:8080/test")
                .retrieve()
                .bodyToMono(String.class)
                .map(string -> "RESPONSE: " + string)
                .subscribe(logger::info);
    }

}

根據此鏈接Baeldung Spring Webclient Oauth2上的文章,WebClientChronJob 第二次運行時,應用程序應該請求資源而不首先請求令牌,因為最后一個令牌尚未過期。 不幸的是,啟用調試日志后,我注意到了相反的情況:每次作業請求資源時,它都在請求新令牌。 如果配置或代碼中缺少某些內容,請告訴我。

Netty started on port(s): 8082
Started MyApp in 2.242 seconds (JVM running for 2.717)
HTTP POST http://localhost:8080/oauth/token
Writing form fields [grant_type, scope, client_id, client_secret] (content masked)
Response 200 OK
Decoded [{access_token=nrLr7bHpV0aqr5cQNhv0NjJYvVv3bv, token_type=Bearer, expires_in=86400, scope=rw:profile  (truncated)...]
Cancel signal (to close connection)
HTTP GET http://localhost:8080/test
Response 200 OK
Decoded "{"status":{"description":"ok","success":true},"result":[]}"
ESPONSE: {"status":{"description":"ok","success":true},"result":[]}
HTTP POST http://localhost:8080/oauth/token
Writing form fields [grant_type, scope, client_id, client_secret] (content masked)
Response 200 OK
Decoded [{access_token=CsOxziw6W6J7IoqA8EiF4clhiwVJ8m, token_type=Bearer, expires_in=86400, scope=rw:profile  (truncated)...]
Cancel signal (to close connection)
HTTP GET http://localhost:8080/test
Response 200 OK
Decoded "{"status":{"description":"ok","success":true},"result":[]}"
ESPONSE: {"status":{"description":"ok","success":true},"result":[]}

下面是我在 pom.xml 中的唯一依賴項

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

我找到了解決我的問題的方法。 當前 Spring 安全版本 5.1.x 的 WebClient 實現不會在令牌過期后請求新令牌,並且可能 Spring 的開發人員決定每次都詢問令牌。 Spring 的開發人員還決定僅在新版本 5.2.0.M2 或 (M1) 中修復此錯誤,而不將修復向后移植到 5.1.x

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>
    <groupId>net.tuxy</groupId>
    <artifactId>oauth2-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>MyApp</name>
    <description>Spring Boot WebClient OAuth2 client_credentials example</description>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </pluginRepository>
    </pluginRepositories>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-core</artifactId>
            <version>5.2.0.M2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>5.2.0.M2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>5.2.0.M2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-core</artifactId>
            <version>5.2.0.M2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
            <version>5.2.0.M2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

沒錯, 5.2.x.RELEASE版本並沒有解決這個問題。

版本5.2.0.M3修復了這個問題和另一個關於“client-authentication-method=POST”的問題,它不起作用。 因此,您可以使用它來代替5.2.0.M2

最近我不得不用WebSessionServerOAuth2AuthorizedClientRepository更改UnAuthenticatedServerOAuth2AuthorizedClientRepository因為我必須在同一個應用程序上驗證 URL

@Override
protected void configure(HttpSecurity http) {
    http
        .requestMatchers()
        .antMatchers("/rest/**")
        .authorizeRequests()
        .anyRequest().authenticated();
}

並且令牌問題再次發生。

可能會發生這種情況,因為令牌存儲在客戶端存儲庫中,並且該錯誤尚未在WebSessionServerOAuth2AuthorizedClientRepository中修復。 無論如何,為了解決這個問題,我剛剛創建了一個自定義客戶端存儲庫,擴展 UnAuthenticatedServerOAuth2AuthorizedClientRepository 並覆蓋其中的 3 個方法( loadAuthorizedClientremoveAuthorizedClientsaveAuthorizedClient ),將 null 傳遞給 Authentication 和 ServerWebExchange 參數。

public class BypassAuthenticatedServerOAuth2AuthorizedClientRepository extends UnAuthenticatedServerOAuth2AuthorizedClientRepository {

    @Override
    public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, Authentication authentication, ServerWebExchange serverWebExchange) {
        return super.loadAuthorizedClient(clientRegistrationId, null, null);
    }

    @Override
    public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication authentication, ServerWebExchange serverWebExchange) {
        return super.saveAuthorizedClient(authorizedClient, null, null);
    }

    @Override
    public Mono<Void> removeAuthorizedClient(String clientRegistrationId, Authentication authentication, ServerWebExchange serverWebExchange) {
        return super.removeAuthorizedClient(clientRegistrationId, null, null);
    }
}
@Configuration
public class WebClientConfig {

    @Bean
    WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                clientRegistrations, new BypassAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId("client-registration-id");
        return WebClient.builder().filter(oauth).build();
    }

}

通過這種方式,我能夠啟用身份驗證,並且令牌僅被請求一次,直到它過期。

Spring 繼續前進,因為@angus 要求提供一種替代方法來替代現已棄用的UnAuthenticatedServerOAuth2AuthorizedClientRepository ,所以我想分享我的實現。 這是分別使用Spring Boot 2.4.4Spring Security 5.4.5

UnAuthenticatedServerOAuth2AuthorizedClientRepository推薦替代方案AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager 此外,為您的 bean 提供WebClient的推薦方法是注入WebClient.Builder 因此,像這樣配置您的WebClient.Builder

@Configuration
public class OAuth2ClientConfiguration {

    @Bean
    public WebClientCustomizer oauth2WebClientCustomizer(
            ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oAuth2AuthorizedClientExchangeFilterFunction =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(reactiveOAuth2AuthorizedClientManager);

        oAuth2AuthorizedClientExchangeFilterFunction.setDefaultClientRegistrationId("api-client");

        return webClientBuilder ->
                webClientBuilder
                        .filter(oAuth2AuthorizedClientExchangeFilterFunction);
    }

    @Bean
    public ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager(
            ReactiveClientRegistrationRepository registrationRepository,
            ReactiveOAuth2AuthorizedClientService authorizedClientService) {
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
                        registrationRepository, authorizedClientService);

        authorizedClientManager.setAuthorizedClientProvider(
                new ClientCredentialsReactiveOAuth2AuthorizedClientProvider());

        return authorizedClientManager;
    }
}

這就是它的全部,真的。 只要授權令牌對資源請求有效,它將僅獲取一次。

稍微偏離主題:如果您想阻止對令牌 URI 的授權請求在您的集成測試中完全發生,您可能對此感興趣。


測試

以下是對應的src/test/resources/application.yml和使用MockServer模擬資源服務器和授權服務器的集成測試,以證明對多個資源請求僅調用一次 Token URI。

spring:
  security:
    oauth2:
      client:
        registration:
          api-client:
            authorization-grant-type: client_credentials
            client-id: test-client
            client-secret: 6b30087f-65e2-4d89-a69e-08cb3c9f34d2
            provider: some-keycloak
        provider:
          some-keycloak:
            token-uri: http://localhost:1234/token/uri
api:
  base-url: http://localhost:1234/api/v1
@SpringBootTest
@ExtendWith(MockServerExtension.class)
@MockServerSettings(ports = 1234)
class TheRestClientImplIT {

    @Autowired
    TheRestClient theRestClient;

    @BeforeEach
    void setUpTest(MockServerClient mockServer) {
        mockServer
                .when(HttpRequest
                        .request("/token/uri"))
                .respond(HttpResponse
                        .response("{\n" +
                                "    \"access_token\": \"c29tZS10b2tlbg==\",\n" +
                                "    \"expires_in\": 300,\n" +
                                "    \"token_type\": \"bearer\",\n" +
                                "    \"not-before-policy\": 0,\n" +
                                "    \"session_state\": \"7502cf31-b210-4754-b919-07e1d8493fa3\"\n" +
                                "}")
                        .withContentType(MediaType.APPLICATION_JSON));
        mockServer
                .when(HttpRequest
                        .request("/api/v1/some-resource")
                        .withHeader("Authorization", "Bearer c29tZS10b2tlbg=="))
                .respond(HttpResponse
                        .response("Hello from resource!"));
    }

    @Test
    void should_access_protected_resource_more_than_once_but_request_a_token_exactly_once(MockServerClient mockServer) {
        int resourceRequestCount = 2; // how often should the resource be requested?

        Stream
                .iterate(1, i -> ++i)
                .limit(resourceRequestCount)
                .forEach(i -> {
                    LoggerFactory
                            .getLogger(TheRestClientImplIT.class)
                            .info("Performing request number: {}", i);

                    StepVerifier
                            .create(theRestClient.getResource())
                            .assertNext(response -> {
                                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
                                assertThat(response.getBody()).isEqualTo("Hello from resource!");
                            })
                            .verifyComplete();
                });

        // verify token request happened exactly once
        mockServer.verify(HttpRequest
                        .request("/token/uri"),
                VerificationTimes.once());

        // verify resource request happened as often as defined
        mockServer.verify(HttpRequest
                        .request("/api/v1/some-resource")
                        .withHeader("Authorization", "Bearer c29tZS10b2tlbg=="),
                VerificationTimes.exactly(resourceRequestCount));
    }
}

作為參考,這是TheRestClient實現:

@Component
public class TheRestClientImpl implements TheRestClient {

    private final WebClient webClient;

    @Autowired
    public TheRestClientImpl(WebClient.Builder webClientBuilder,
                             @Value("${api.base-url}") String apiBaseUrl) {
        this.webClient = webClientBuilder
                .baseUrl(apiBaseUrl)
                .build();
    }

    @Override
    public Mono<ResponseEntity<String>> getResource() {
        return webClient
                .get()
                .uri("/some-resource")
                .retrieve()
                .toEntity(String.class);
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM