![](/img/trans.png)
[英]Spring Boot 2 Spring-Security 5 OAuth2 support for client_credentials grant_type
[英]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 個方法( loadAuthorizedClient
、 removeAuthorizedClient
和saveAuthorizedClient
),將 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.4
和Spring 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.