首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >为什么Spring Boot WebClient OAuth2 (client_credentials)会为每个请求请求一个新的令牌?

为什么Spring Boot WebClient OAuth2 (client_credentials)会为每个请求请求一个新的令牌?
EN

Stack Overflow用户
提问于 2019-11-13 07:43:18
回答 4查看 5.3K关注 0票数 3

我正在尝试创建一个Spring Boot REST应用程序,它必须对另一个受OAuth2保护的应用程序进行远程REST调用。

第一个应用程序使用反应式WebClient调用第二个OAuth2 REST应用程序。我已经用grant_type "client_credentials“配置了WebClient。

application.yml

代码语言:javascript
运行
复制
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
代码语言:javascript
运行
复制
@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();
    }

}
代码语言:javascript
运行
复制
@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第二次运行时,应用程序应该在第一次请求令牌的情况下请求资源,因为最后一个令牌尚未过期。不幸的是,在启用调试日志时,我注意到了相反的情况:每次作业请求资源时,它都会请求一个新的令牌。如果配置或代码中缺少某些内容,请告诉我。

代码语言:javascript
运行
复制
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中仅有的依赖项

代码语言:javascript
运行
复制
<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>
EN

回答 4

Stack Overflow用户

发布于 2019-11-13 20:04:03

我找到了解决我问题的办法。Spring Security version5.1.x的当前WebClient实现不会在令牌过期后请求新的令牌,可能Spring的开发人员每次都决定请求令牌。Spring的开发人员也决定只在新版本5.2.0.M2或(M1)中修复这个错误,而不将修复移植到5.1.x

pom.xml

代码语言:javascript
运行
复制
<?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>
票数 3
EN

Stack Overflow用户

发布于 2021-04-11 22:10:57

UnAuthenticatedServerOAuth2AuthorizedClientRepository继续前进,自从@angus asked为现在已经过时的Spring提供了另一种方法,我想分享我的实现。这分别使用了Spring Boot 2.4.4Spring Security 5.4.5

UnAuthenticatedServerOAuth2AuthorizedClientRepositoryrecommended alternativeAuthorizedClientServiceReactiveOAuth2AuthorizedClientManager。此外,beans的recommended way to provide a WebClient是通过注入WebClient.Builder来实现的。因此,可以这样配置您的WebClient.Builder

代码语言:javascript
运行
复制
@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的授权请求,您可能会对this感兴趣。

测试

下面是一个相应的src/test/resources/application.yml和一个使用MockServer模拟资源服务器和授权服务器的集成测试,以证明对于多个资源请求,令牌URI只被调用一次。

代码语言:javascript
运行
复制
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
代码语言:javascript
运行
复制
@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实现:

代码语言:javascript
运行
复制
@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);
    }
}
票数 1
EN

Stack Overflow用户

发布于 2020-02-01 21:27:47

没错,version 5.2.x.RELEASE没有解决这个问题。

版本5.2.0.M3修复了这个问题和另一个关于“客户端-身份验证-方法=POST”的问题,该问题不起作用。因此,您可以使用它来代替5.2.0.M2

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/58828324

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档