具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌
Posted
技术标签:
【中文标题】具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌【英文标题】:Spring Security OAuth Client for WebClient With Password Grant Type Doesn't Ask For New Token 【发布时间】:2020-10-31 08:21:36 【问题描述】:我有一个 Spring Web 应用程序,它有一个为其 API 端点配置的 oauth2 资源服务器和一个完全不同的 oauth2 客户端,用于它进行的 REST 调用。 oauth2 客户端需要是密码授权类型。用户名和密码是固定的(不是来自 HTTP 请求)。我的问题是 30 分钟后访问令牌和刷新令牌都过期了,所以没有办法进行刷新。我希望 Spring Security 只会要求新的访问令牌,但不会。它使用过期的端点调用 REST 端点并返回 403。 这是我所拥有的:
application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://our.idp.keycloak.host/auth/realms/firstrealm
client:
registration:
my-client-authorization:
client-id: my_client
client-secret: $CLIENT_SECRET
authorization-grant-type: password
scope: openid, profile
provider:
my-client-authorization:
token-uri: https://our.idp.keycloak.host/auth/realms/secondrealm/protocol/openid-connect/token
MyClientConfig.java:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.oauth2.client.OAuth2AuthorizationContext;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Map;
@Configuration
@RequiredArgsConstructor
public class MyClientConfig
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager)
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.baseUrl("https://the.api.host.to.call")
.build();
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository
)
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
DefaultOAuth2AuthorizedClientManager result = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientRepository
);
result.setAuthorizedClientProvider(authorizedClientProvider);
result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
));
return result;
API 调用本身:
private <T> T callApi(Function<UriBuilder, URI> uriFunction, Class<T> resultType)
return this.webClient
.get()
.uri(uriFunction)
.retrieve()
.bodyToMono(resultType)
.block();
我第一次调用它时它就起作用了。但是 30 分钟后,令牌已经死了,我不知道如何获得一个新的。 如果我将其切换为 client_credentials 授权类型,它会起作用,它会在需要时自动获取新令牌。但由于某种原因,我不能对密码授予类型做同样的事情。
编辑: 所以我设法解决了这个问题:https://github.com/spring-projects/spring-security/issues/8831。 当我也为 refreshToken 配置 WebClient 时,它会在刷新令牌过期时崩溃。但是当你之后再次发出请求时,它会获得一个新的令牌。所以我不得不将 API 调用包装在 try catch 中,如果错误是我关心的错误,我会再次调用 API。这不是一个非常优雅的解决方案,但它确实有效。
【问题讨论】:
我的回答能解决你的问题吗? 【参考方案1】:明确提到here,Spring会在访问令牌过期时自动刷新。
我认为您需要您使用过的“反应性”等效类。请试试这个sn-p。
@Configuration
@RequiredArgsConstructor
public class MyClientConfig
@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager)
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("my-client-authorization");
return WebClient.builder()
.filter(oauth2Client)
.baseUrl("https://the.api.host.to.call")
.build();
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
ReactiveOAuth2AuthorizedClientService authorizedClientService
)
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager result = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
authorizedClientService
);
result.setAuthorizedClientProvider(authorizedClientProvider);
result.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, "user",
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, "password"
)));
return result;
【讨论】:
您好,感谢您的回答。我试了一下,得到:方法 MyClientConfig 的参数 0 需要一个找不到的 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository' 类型的 bean。问题是我没有将 WebFlux 与 WebFluxSecurity 用于主要应用程序端点,而是使用普通的 WebSecurity?如果我重新实现它会有帮助吗? @Filip 如果您通过此属性注册了您的客户端:spring.security.oauth2.client.registration
,那么InMemoryReactiveClientRegistrationRepository
bean 将通过ReactiveOAuth2ClientConfigurations
注册。另外我建议不要将 web 和 webflux 混合在一起。
ReactiveOAuth2AuthorizedClientService 设置在哪里?您是否在某个地方有一个示例,其中包含所有设置,包括配置?他们似乎在任何地方都不是一个很好的例子。【参考方案2】:
对Abhinaba Chakraborty 帖子的补充:
没有定义“ReactiveOAuth2AuthorizedClientService”,也没有“clientRegistrationRepository”。根据您的帖子,我有这些工作设置
@Bean
ReactiveClientRegistrationRepository getRegistrationRepository()
ClientRegistration registration = ClientRegistration
.withRegistrationId(REGISTRATION_ID)
.tokenUri(TOKEN_URI)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.authorizationGrantType(new AuthorizationGrantType(GRANT_TYPE))
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
@Bean
WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager)
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId(REGISTRATION_ID);
return WebClient.builder()
.filter(oauth2Client)
.build();
@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository
)
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager clientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository,
new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository)
);
clientManager.setAuthorizedClientProvider(authorizedClientProvider);
clientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono.just(Map.of(
OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, USERNAME,
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, PASSWORD
)));
return clientManager;
所有常量都应该清楚。 REGISTRATION_ID 只是任何值,但在创建/使用存储库时需要相等
【讨论】:
以上是关于具有密码授予类型的 WebClient 的 Spring Security OAuth 客户端不要求新令牌的主要内容,如果未能解决你的问题,请参考以下文章