gpt4 book ai didi

spring-security-oauth2 - OAuth2 与多个网关实例共享主体对象

转载 作者:行者123 更新时间:2023-12-05 02:51:22 26 4
gpt4 key购买 nike

我已经将 Spring Cloud Gateway 与 OAuth2 服务器集成在一起。它适用于单实例网关。这是我的安全配置。

@EnableWebFluxSecurity
public class GatewaySecurityConfiguration {

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange().pathMatchers("/user/v3/api-docs", "/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.csrf().disable();
return http.build();
}

但是,当我将网关扩展到 2 个实例时,一些请求按预期工作,但一些请求返回 401。

    load balancer (kubernetes nodeport service)
/ \
gateway gateway
\ /
(microservice clusters)

当我登录网关的第一个实例时,成功创建主体对象并将 session 分配给 redis。如果下一个请求到达第二个实例,它会返回 401,因为它没有本金。

我该如何解决这个问题?

ps:我正在使用 redis 进行网络 session ,以在网关之间共享 session 信息。

最佳答案

长话短说

可以通过WebSession共享Redis上的session principal信息。但是您不能共享访问 token (JWT),因为它们存储在服务器的内存中。

  • 解决方案 1:您的请求应始终发送到您登录的服务器。(详情如下)
  • 解决方案 2:实现新的 ReactiveOAuth2AuthorizedClientService bean,将 session 存储在 redis 中。 (下面也有详细信息)

长答案

来自 Spring Cloud 文档 ( https://cloud.spring.io/spring-cloud-static/Greenwich.SR5/multi/multi__more_detail.html );

The default implementation of ReactiveOAuth2AuthorizedClientServiceused by TokenRelayGatewayFilterFactory uses an in-memory data store.You will need to provide your own implementationReactiveOAuth2AuthorizedClientService if you need a more robustsolution.

您知道的第一件事:当您成功登录时,访问 token (作为 jwt)由 oauth2 服务器返回,服务器创建 session 并将此 session 映射到 ConcurrentHashMap 上的访问 token (authorizedClients 实例 InMemoryReactiveOAuth2AuthorizedClientService 类)。

当您使用您的 session ID 请求 API 网关访问微服务时,访问 token (jwt) 由网关中的 TokenRelayGatewayFilterFactory 解析,并且此访问 token 设置在授权 header 中,请求被转发到微服务。

那么,让我来解释一下 TokenRelayGatewayFilterFactory 是如何工作的(假设您通过 Redis 使用 WebSession 并且您有 2 个网关实例并且您在 instance-1 登录。)

  • 如果您的请求转到 instance-1,则主体通过 session ID 从 redis 取回,然后在过滤器中调用 authorizedClientRepository.loadAuthorizedClient(..)。此存储库是 AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository 对象的实例。 isPrincipalAuthenticated() 方法返回 true,因此流程继续 authorizedClientService.loadAuthorizedClient()。该服务被定义为 ReactiveOAuth2AuthorizedClientService 接口(interface),它只有一个实现(InMemoryReactiveOAuth2AuthorizedClientService)。这个实现有 ConcurrentHashMap(key: principal object, value: JWT)
  • 如果您的请求转到实例 2,则以上所有流程均有效。但提醒一下,ConcurrentHashMap 没有访问主体的访问 token ,因为访问 token 存储在实例 1 的 ConcurrentHashMap 中。因此,访问 token 为空,然后您的请求在没有 Authorization header 的情况下向下游发送。您将收到 401 Unauthorized。

解决方案一

因此,您的请求应始终发送到您登录的服务器以获取有效的访问 token 。

  • 如果您使用 NGINX 作为负载均衡器,则在上游中使用ip_hash
  • 如果您使用 kubernetes 服务作为负载均衡器,则在 session 关联中使用ClientIP

方案二

InMemoryReactiveOAuth2AuthorizedClientService 只是 ReactiveOAuth2AuthorizedClientService 的实现。因此,创建使用 Redis 的新实现,然后将其作为 bean。

@RequiredArgsConstructor
@Slf4j
@Component
@Primary
public class AccessTokenRedisConfiguration implements ReactiveOAuth2AuthorizedClientService {

private final SessionService sessionService;

@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("loadAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");

// TODO: When changed immutability of OAuth2AuthorizedClient, return directly object without map.
return (Mono<T>) sessionService.getSessionRecord(principalName, "accessToken").cast(String.class)
.map(mapper -> {
return new OAuth2AuthorizedClient(clientRegistration(), principalName, accessToken(mapper));
});
}

@Override
public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
log.info("saveAuthorizedClient for user {}", principal.getName());
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");

return Mono.fromRunnable(() -> {
// TODO: When changed immutability of OAuth2AuthorizedClient , persist OAuthorizedClient instead of access token.
sessionService.addSessionRecord(principal.getName(), "accessToken", authorizedClient.getAccessToken().getTokenValue());
});
}

@Override
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("removeAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return null;
}

private static ClientRegistration clientRegistration() {
return ClientRegistration.withRegistrationId("login-client")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientId("dummy").registrationId("dummy")
.redirectUriTemplate("dummy")
.authorizationUri("dummy").tokenUri("dummy")
.build();
}

private static OAuth2AccessToken accessToken(String value) {
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, value, null, null);
}

}

注意事项:

关于spring-security-oauth2 - OAuth2 与多个网关实例共享主体对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63150759/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com