使用 Spring Cloud Gateway 保护服务

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

到目前为止,在本系列中,我们已经介绍了使用 Spring Cloud Gateway 进行 入门隐藏服务。然而,在我们开始隐藏服务时,我们并没有对其进行安全保护。在本文中,我们将纠正这一点。

为了保护我们的服务,我们将使用 OAuth 2.0 以及 Javascript Object Signing & Encryption (JOSE) 和 JSON Web Tokens 标准支持的 Token Relay 模式。这将为我们的用户提供一种识别自己、授权应用程序查看其配置文件以及访问网关后安全资源的方式。

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

在运行演示之前,我想提请您注意其中的主要组件以及它们的创建方式。下图显示了整体系统设计。它由一个由三个服务组成的网络组成:一个单一登录服务器 (Single Sign-On Server),一个 API 网关服务器 (API Gateway Server) 和一个资源服务器 (Resource Server)。

Diagram illustrating the overarching architecture of the demo

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

创建用户

为了对我们的用户进行身份验证,我们需要两样东西:用户账户记录和一个兼容 OAuth2 的身份验证提供者(或服务器)。市面上有许多商业 OAuth2 身份验证提供者,但在我们的演示中,我们将坚持使用开源软件,并使用 Cloud Foundry 的用户账户与身份验证服务器 (User Account & Authentication Server),更常称之为 UAA。

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

对于本次演示,我们使用 Dockerfile 将 UAA 和 Tomcat 构建到一个容器镜像中(您可以在 uaa 文件夹中查看它)。另一个需要提请您注意的是 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://:8080/login/oauth2/code/gateway

将 UAA 与 Spring Cloud Gateway 集成

正如您在 Spring Cloud Security, OAuth2 Token Relay 文档 中所见:“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://: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();
}

第二个是路由的配置。如 Hiding Services 中所述,我们必须将资源服务器公开为一个路由,否则它将隐藏在我们的网络内部。路由声明中的 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 端点,除非您拥有这样的令牌。

@RestController 端点 /resource 期望一个 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 文件,然后将它们和 UAA 打包到容器中。

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

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

$> docker-compose up

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

现在,打开一个‘隐私’或‘无痕’浏览器窗口,然后导航到 https://: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’访问”的复选框。这是资源服务器在允许您访问资源之前将要检查的范围。

单击‘Authorize’后,您将被转发到 [/resource][7] 端点,该端点将显示有关您用户的一些基本详细信息。您将看到类似这样的消息,尽管您的 subjectId 会有所不同。

Resource accessed by: user1 (with subjectId: 43c7681a-6762-451e-8435-d503fd7a0c4d)

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

如果您想查看 user1 的一些额外信息,请导航到 https://: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 Debugger 中。此实用程序可以解析令牌并显示其内容。在输出中,您将找到用户名以及与用户配置文件关联的范围。

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 社区所有即将举行的活动。

查看所有