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

标签 spring-security-oauth2 spring-oauth2 spring-cloud-gateway spring-cloud-security

我已将 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 进行 Web session ,以共享网关之间的 session 信息。

最佳答案

TL;DR

您可以通过WebSession共享Redis上的 session 主体信息。但您无法共享访问 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 ReactiveOAuth2AuthorizedClientService used by TokenRelayGatewayFilterFactory uses an in-memory data store. You will need to provide your own implementation ReactiveOAuth2AuthorizedClientService if you need a more robust solution.

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

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

那么,让我解释一下 TokenRelayGatewayFilterFactory 的工作原理(假设您通过 Redis 使用 WebSession,并且您有 2 个网关实例,并且您在实例 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 存储在instance-1 的ConcurrentHashMap 中。因此,访问 token 为空,然后您的请求下游没有授权 header 。您将收到 401 Unauthorized。

解决方案1

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

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

解决方案2

InMemoryReactiveOAuth2AuthorizedClientService只是ReactiveOAuth2AuthorizedClientService的实现。因此,创建使用 Redis 的新实现,然后将其作为primary 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/

相关文章:

spring - 如何结合 Feign 和 OAuth 2.0?

x509certificate - 谁有在Spring Cloud Gateway/Spring WebFlux中实现x509相互认证的简单例子?

spring-boot-actuator - spring cloud gateway,你能排除路径吗(做一个全局的!=)

Spring OAuth2 + JWT在访问 token 中包含其他信息JUST

java - 处理错误: InvalidRequestException,缺少授权类型

spring - 如何 Autowiring 此 TokenStore

java - 授权 header 的 Spring Security OAuth2 CORS 问题

docker-compose - 使用 docker-compose 运行时,Spring Cloud Gateway 给出 500 错误 finishConnect(..) failed : Connection refused: localhost/127. 0.0.1:8083

java - 使用 Spring Boot 的 OAuth2 SSO,无需授权屏幕

spring-security-oauth2 - 如何在 Spring OAuth2 服务器身份验证重定向中保留原始片段标识符