使用 Spring Cloud Gateway 保护服务

工程 | Ben Wilcock | 2019年8月16日 | ...

在本系列文章中,我们已经介绍了使用 入门隐藏服务 Spring Cloud Gateway。但是,当我们着手隐藏服务时,并没有保护它们的安全。在这篇文章中,我们将纠正这个问题。

为了保护我们的服务,我们将使用 OAuth 2.0 支持的令牌中继模式以及 Javascript 对象签名和加密 (JOSE) 和 JSON Web 令牌标准。这将为我们的用户提供一种识别自身的方法,授权应用程序查看其个人资料并访问网关后面的受保护资源。

此演示的所有代码都已在线发布 在 GitHub 上secured-gateway 文件夹中。如果您只想运行它而不了解其构建方式,请跳至标题为“运行演示”的部分。

在运行演示之前,我想提请您注意其中的主要组件及其创建方式。下图显示了整个系统设计。它由三个服务的网络组成:单点登录服务器、API 网关服务器和资源服务器。

Diagram illustrating the overarching architecture of the demo

资源服务器是一个隐藏在 API 网关后面的常规 Spring Boot 应用程序。API 网关使用 Spring Cloud Gateway 构建,并将用户帐户和授权的管理委托给单点登录服务器。为了创建这三个组件,需要考虑许多细小但重要的事项。接下来让我们看看这些是什么。

创建用户

为了对我们的用户进行身份验证,我们需要两样东西:用户帐户记录和一个与 OAuth2 兼容的身份验证提供程序(或服务器)。有很多商业 OAuth2 身份验证提供程序,但在我们的演示中,我们将坚持使用开源软件并使用 Cloud Foundry 的用户帐户和身份验证服务器,通常称为 UAA。

UAA 是一个 Web 存档 (WAR),可以部署到任何支持此格式的服务器上。在本例中,我们选择了开源的 Apache Tomcat 服务器。在 Tomcat 中运行时,UAA 会针对其内部用户帐户数据库提供 OAuth 和 OpenId Connect 身份验证。

对于此演示,我们使用 Dockerfile(您可以在 uaa 文件夹中查看)将 UAA 和 Tomcat 构建到容器镜像中。另一个需要注意的是 uaa.yml 文件。此 YAML 文件将使用稍后我们尝试访问资源服务器时使用的用户名和密码配置我们的 UAA。它还包含要注册的 OAuth2 应用程序以及执行 JWT 令牌加密所需的密钥。

uaa.yml 中,我们告诉 UAA 将 user1 添加到其帐户数据库中,并授予此用户 resource.read 范围。这是资源服务器允许访问所需的范围。

scim:
  groups:
    email: Access your email address
    resource.read: Allow access with 'resource.read'
  users:
    - user1|password|[email protected]|first1|last1|uaa.user,profile,email,resource.read

uaa.yml 中,我们还注册我们的 OAuth2“客户端”应用程序。此注册告诉 UAA 它应该期望一个应用程序将自身标识为 gateway,并且此应用程序将使用 authorization_code 方案。网关将需要访问各种范围,包括 resource.read

oauth:
...
  clients:
    gateway:
      name: gateway
      secret: secret
      authorized-grant-types: authorization_code
      scope: openid,profile,email,resource.read
      authorities: uaa.resource
      redirect-uri: https://127.0.0.1:8080/login/oauth2/code/gateway

将 UAA 与 Spring Cloud Gateway 集成

正如您在 Spring Cloud Security,OAuth2 令牌中继文档 中看到的那样:“Spring Cloud Gateway 可以将 OAuth2 访问令牌转发到它正在代理的服务。除了登录用户和获取令牌之外,过滤器还会提取已验证用户的访问令牌并将其放入下游请求的请求标头中。”

这实际上意味着,当使用 Spring Cloud Security 时,将 Spring Cloud Gateway 服务器与我们选择的安全机制集成所涉及的工作非常少。网关将代表我们协调与单点登录服务器的身份验证,并确保下游应用程序在需要时获得用户访问令牌的副本。

为了配置此功能,首先要注意的是网关的 application.yml 文件中的 OAuth2 配置。

security:
    oauth2:
      client:
        registration:
          gateway:
            provider: uaa
            client-id: gateway
            client-secret: secret
            authorization-grant-type: authorization_code
            redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid,profile,email,resource.read
        provider:
          uaa:
            authorization-uri: https://127.0.0.1:8090/uaa/oauth/authorize
            token-uri: http://uaa:8090/uaa/oauth/token
            user-info-uri: http://uaa:8090/uaa/userinfo
            user-name-attribute: sub
            jwk-set-uri: http://uaa:8090/uaa/token_keys

此配置正在执行两项操作。它指定了我们的 OAuth 客户端注册信息(与我们之前在 UAA 中注册的 gateway 应用程序匹配),并且它详细说明了 OAuth 身份验证提供程序的服务可以在哪里找到(以及其他一些属性,例如 jwk-set-uri,网关将使用它来验证令牌的真实性)。此配置本质上是使我们的网关能够有效地与 UAA 通信。

下一个需要注意的是 GatewayApplication.java 类。在这个类中,有两点需要注意。第一点是包含自动装配的 TokenRelayGatewayFilterFactory,第二点是将此类用作资源服务器路由配置中的过滤器。

@Autowired
private TokenRelayGatewayFilterFactory filterFactory;

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("resource", r -> r.path("/resource")
              .filters(f -> f.filters(filterFactory.apply())
                .removeRequestHeader("Cookie"))
            .uri("http://resource:9000")) 
            .build();
}

第二个是路由的配置。如 隐藏服务 中所述,我们必须公开资源服务器作为路由,否则它将保留在我们网络内部。路由声明中的 filterFactory.apply() 方法确保任何打算用于资源服务器的交换都包含 JWT 访问令牌。removeRequestHeader(“Cookie”) 告诉网关在路由操作期间从请求中删除用户的“Cookie”标头(因为下游服务不需要此标头,它们只需要 JWT access_token)。

下面的 YAML 配置实现了相同的功能,但无需 Java 代码。

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

通过这种方式(使用 Java 或 YAML)配置网关后,任何前往 /resource 路由上资源服务器的用户请求都需要 JWT 格式的安全 access_token

创建资源服务器并保护资源

资源服务器现在只需要两件事。第一件事是 /resource 端点,它期望以 JWT 令牌的形式提供身份验证主体。第二件事是一些配置,以防止访问 /resource 端点,除非您拥有这样的令牌。

/resource@RestController 端点期望将 Jwt 对象作为方法参数。此参数被装饰为 @AuthenticationPrincipal。该方法返回一个包含消息的简单字符串。此消息确认已访问资源,并包含有关用户的某些基本详细信息。

@GetMapping("/resource")
public String resource(@AuthenticationPrincipal Jwt jwt) {
    return String.format("Resource accessed by: %s (with subjectId: %s)" ,
            jwt.getClaims().get("user_name"),
            jwt.getSubject());
}

安全配置由 SecurityConfig 类处理。此类包含一个 bean 方法,该方法配置作为 springSecurityFilterChain 方法签名中的参数传递的 ServerHttpSecurity 对象。

此配置声明请求访问路径 /resource 的用户必须经过身份验证,并且其个人资料中必须具有 OAuth2 范围 resource.read。行 .oauth2ResourceServer().jwt() 告诉应用程序它必须使用 OAuth2 JWT 规范作为安全方案。

public class SecurityConfig {

  @Bean
  SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
    http
            .authorizeExchange()
             .pathMatchers("/resource").hasAuthority("SCOPE_resource.read")
              .anyExchange().authenticated()
              .and()
            .oauth2ResourceServer()
              .jwt();
    return http.build();
  }
}

最后,资源服务器需要知道它可以在哪里找到公共密钥来验证其收到的访问令牌的真实性。UAA 提供了一个端点,资源服务器和网关在运行时都依赖于此端点来执行此检查。该端点在每个应用程序的 application.yml 中进行配置。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://uaa:8090/uaa/token_keys

您不必使用端点来获取这些密钥,您可以将它们直接捆绑到您的应用程序中,但我们在这里选择不这样做。如果您在演示运行时在浏览器中点击 jwk-set-uri 链接,您将看到如下内容。

An example of the token keys endpoint data showing a JSON object containing public keys

运行演示

和以前一样,我们将使用 Docker Compose 来简化操作并模拟真实网络。在检出代码并在后台运行 Docker 后,在 security-gateway 文件夹中,执行 build.sh 脚本将资源服务器和网关应用程序编译成 JAR,然后将这些 JAR 和 UAA 打包到容器中。

$> cd security-gateway
$> ./build.sh

此过程成功完成后,我们可以使用 docker-compose up 启动演示。

$> docker-compose up

执行此操作时,您将在屏幕上看到大量输出,同时所有三个服务器都启动,但几分钟后,它应该会稳定下来。

现在,打开一个“私有”或“隐身”浏览器窗口,并导航到 https://127.0.0.1:8080/resource。网关会立即将您的浏览器转发到 UAA 服务器,并要求您使用用户名和密码(在本例中为 user1password)登录。然后,UAA 将要求您“授权”网关读取 user1 的个人资料。您将看到以下屏幕来执行此操作。

An image showing the UAA autherisation screen used to autherise the application's access to the read dot resource scope

尤其要注意标题为“允许使用‘resource.read’访问”的复选框。资源服务器将在允许您访问资源之前检查此范围。

单击“授权”后,您将被转发到 [/resource][7] 端点,该端点将向您显示有关您的用户的一些基本信息。您将看到类似于这样的消息,尽管您的 subjectId 将有所不同。

资源被访问:user1(subjectId:43c7681a-6762-451e-8435-d503fd7a0c4d)

这是资源服务器,确认您可以访问资源,并显示您使用了user1的身份。

如果您想查看有关user1的更多信息,请访问https://127.0.0.1:8080,这里提供了更完整的用户详细信息屏幕。它看起来像这样

An image showing the full user details screen available once logged in and authorized

最后,我们在日志中打印出了传递给资源服务器的JWT令牌,以便您可以检查它。在生产环境中我们永远不会这样做,但出于演示目的,我们认为看到它会有所帮助。它看起来像这样

resource | TRACE --- SecuredServiceApplication: ***** JWT Token: eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAvdWFhL3Rva2VuX2tleXMiLCJraWQiOiJrZXktaWQtMSIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJm (truncated...)

您可以从日志中完整复制此令牌,并将其粘贴到jwt.io的JWT调试器中。此实用程序可以解析令牌并显示其内容。在输出中,您将找到与用户配置文件关联的用户名和范围。

An image showing the jwt.org screen after parsing the JWT token generated by the UAA and used by our user to access reasources

日志本身也相当有启发性(尽管顺序并非保证)。它们显示了这三台服务器相互交互时发生的大部分情况。要查看一些带有附加说明的编辑后的日志重点内容,请点击此处

完成

完成后,使用Ctrl-C关闭Docker服务。如果由于任何原因此操作失败,您也可以从security-gateway文件夹使用docker-compose down。使用任何一种技术,您都应该看到类似于此的输出

$> docker-compose down 
Stopping gateway  ... done
Stopping service  ... done
Stopping registry ... done

可以使用docker-compose rm -f实现Docker的进一步清理。

最后……

今年为何不实现您的开发者梦想,注册参加SpringOne Platform?它是使用Spring构建可扩展应用程序的顶级会议。加入成千上万的其他开发人员,在德克萨斯州奥斯汀市(10月7日至10日)学习、分享和娱乐。注册时使用折扣代码S1P_Save200即可节省门票费用。如果您需要帮助说服您的经理,请尝试这些技巧

获取Spring通讯

保持与Spring通讯的联系

订阅

领先一步

VMware提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部