Skip to content

TokenRelay 网关过滤器工厂

概述

TokenRelay(令牌转发)是一种 OAuth2 消费者作为客户端的机制,它将传入的令牌转发到传出的资源请求中。消费者可以是纯客户端(如 SSO 应用程序)或资源服务器。

Spring Cloud Gateway 可以使用 TokenRelay GatewayFilter 将 OAuth2 访问令牌转发到它代理的下游服务。

业务场景

在微服务架构中,TokenRelay 主要解决以下业务场景的问题:

  1. 单点登录 (SSO) 场景:用户通过网关登录后,访问不同的微服务时需要携带身份信息
  2. API 聚合场景:网关作为 API 聚合点,需要将用户的访问令牌传递给后端服务
  3. 服务间调用场景:在微服务调用链中,需要保持用户的身份上下文

配置方式

方式一:指定客户端注册 ID

TokenRelay GatewayFilter 接受一个可选参数 clientRegistrationId,用于指定特定的客户端注册配置。

kotlin
@Configuration
class GatewayConfig {

    @Bean
    fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("resource") { routeSpec ->
                routeSpec.path("/resource")
                    .filters { filterSpec ->
                        filterSpec.tokenRelay("myregistrationid") // 指定客户端注册ID
                    }
                    .uri("http://localhost:9000")
            }
            .build()
    }
}
java
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("resource", r -> r.path("/resource")
                    .filters(f -> f.tokenRelay("myregistrationid"))
                    .uri("http://localhost:9000"))
            .build();
}

或者使用 YAML 配置:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: resource
          uri: http://localhost:9000
          predicates:
            - Path=/resource
          filters:
            - TokenRelay=myregistrationid

方式二:使用当前认证用户的令牌

如果使用 oauth2Login() 对用户进行身份验证,Spring Cloud Gateway 也可以转发当前认证用户的 OAuth2 访问令牌。要将此功能添加到网关,可以省略 clientRegistrationId 参数:

kotlin
@Configuration
class GatewayConfig {

    @Bean
    fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("resource") { routeSpec ->
                routeSpec.path("/resource")
                    .filters { filterSpec ->
                        filterSpec.tokenRelay() // 不指定客户端注册ID,使用当前用户令牌
                    }
                    .uri("http://localhost:9000")
            }
            .build()
    }
}
java
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("resource", r -> r.path("/resource")
                    .filters(f -> f.tokenRelay())
                    .uri("http://localhost:9000"))
            .build();
}

或者使用 YAML 配置:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: resource
          uri: http://localhost:9000
          predicates:
            - Path=/resource
          filters:
            - TokenRelay=

这样配置后,网关将会(除了登录用户和获取令牌外)将认证令牌传递给下游服务(在这个例子中是 /resource)。

依赖配置

要在 Spring Cloud Gateway 中启用此功能,需要添加以下依赖:

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
}
xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

工作原理

TokenRelay 过滤器的工作流程如下:

  1. 令牌提取:过滤器从当前认证用户中提取 OAuth2 访问令牌

    • 如果提供了 clientRegistrationId,则使用指定的客户端注册配置获取令牌
    • 如果没有提供 clientRegistrationId,则使用当前认证用户自己的访问令牌(登录时获得)
  2. 令牌转发:提取的访问令牌被放置在下游请求的请求头中

实际业务示例

假设我们有一个电商系统,用户通过网关访问不同的微服务:

kotlin
@Configuration
class ECommerceGatewayConfig {

    @Bean
    fun ecommerceRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            // 用户服务路由 - 使用当前用户令牌
            .route("user-service") { routeSpec ->
                routeSpec.path("/api/users/**")
                    .filters { filterSpec ->
                        filterSpec.tokenRelay() // 转发用户自己的令牌
                    }
                    .uri("http://user-service:8080")
            }
            // 订单服务路由 - 使用特定客户端令牌
            .route("order-service") { routeSpec ->
                routeSpec.path("/api/orders/**")
                    .filters { filterSpec ->
                        filterSpec.tokenRelay("order-client") // 使用订单服务的客户端令牌
                    }
                    .uri("http://order-service:8081")
            }
            // 支付服务路由 - 使用特定客户端令牌
            .route("payment-service") { routeSpec ->
                routeSpec.path("/api/payments/**")
                    .filters { filterSpec ->
                        filterSpec.tokenRelay("payment-client") // 使用支付服务的客户端令牌
                    }
                    .uri("http://payment-service:8082")
            }
            .build()
    }
}

配置文件:

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          # 订单服务客户端注册
          order-client:
            client-id: order-service-client
            client-secret: order-service-secret
            authorization-grant-type: client_credentials
            scope: order:read,order:write
          # 支付服务客户端注册
          payment-client:
            client-id: payment-service-client
            client-secret: payment-service-secret
            authorization-grant-type: client_credentials
            scope: payment:process
        provider:
          order-client:
            token-uri: https://auth-server/oauth2/token
          payment-client:
            token-uri: https://auth-server/oauth2/token

注意事项

IMPORTANT

只有在设置了适当的 spring.security.oauth2.client.* 属性时,才会创建 TokenRelayGatewayFilterFactory bean,这些属性会触发创建 ReactiveClientRegistrationRepository bean。

WARNING

TokenRelayGatewayFilterFactory 使用的 ReactiveOAuth2AuthorizedClientService 的默认实现使用内存数据存储。如果需要更健壮的解决方案,您需要提供自己的 ReactiveOAuth2AuthorizedClientService 实现。

自定义 OAuth2 授权客户端服务

在生产环境中,建议实现自定义的 ReactiveOAuth2AuthorizedClientService 来持久化令牌:

kotlin
@Component
class CustomReactiveOAuth2AuthorizedClientService(
    private val clientRegistrationRepository: ReactiveClientRegistrationRepository
) : ReactiveOAuth2AuthorizedClientService {

    // 自定义实现,例如使用 Redis 或数据库存储令牌
    private val tokenStorage = mutableMapOf<String, OAuth2AuthorizedClient>()

    override fun <T : OAuth2AuthorizedClient> loadAuthorizedClient(
        clientRegistrationId: String,
        principalName: String
    ): Mono<T> {
        val key = "$clientRegistrationId:$principalName"
        return Mono.justOrEmpty(tokenStorage[key] as? T)
    }

    override fun saveAuthorizedClient(
        authorizedClient: OAuth2AuthorizedClient,
        principal: Authentication
    ): Mono<Void> {
        val key = "${authorizedClient.clientRegistration.registrationId}:${principal.name}"
        tokenStorage[key] = authorizedClient
        return Mono.empty()
    }

    override fun removeAuthorizedClient(
        clientRegistrationId: String,
        principalName: String
    ): Mono<Void> {
        val key = "$clientRegistrationId:$principalName"
        tokenStorage.remove(key)
        return Mono.empty()
    }
}

最佳实践

TIP

  1. 令牌刷新:确保实现适当的令牌刷新机制,避免令牌过期导致的服务调用失败
  2. 安全性:在生产环境中使用 HTTPS 传输令牌
  3. 监控:添加适当的日志和监控,跟踪令牌的使用情况
  4. 缓存策略:合理配置令牌缓存,平衡性能和安全性

TokenRelay 过滤器是 Spring Cloud Gateway 中处理 OAuth2 令牌转发的强大工具,它简化了微服务架构中的身份验证和授权流程,确保用户的身份信息能够安全地在服务间传递。