使用 Spring Cloud Gateway 保护服务

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

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

为了保护我们的服务,我们将使用 OAuth 2.0 以及 Javascript Object Signing & Encryption (JOSE) 和 JSON Web Tokens 标准支持的 Token Relay(令牌中继)模式。这将为我们的用户提供一种识别身份的方式,授权应用程序查看他们的个人资料并访问网关背后的受保护资源。

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

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

Diagram illustrating the overarching architecture of the demo

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

创建用户

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

UAA 是一个 Web Archive (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: http://localhost: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: http://localhost: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,然后将它们和 UAA 打包到容器中。

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

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

$> docker-compose up

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

现在,打开一个“私密”或“隐身”浏览器窗口,然后导航到 http://localhost: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

特别注意名为“Allow access with ‘resource.read’”的复选框。资源服务器在允许您访问资源之前会检查的就是这个范围。

点击“授权”后,您将被转发到 [/resource][7] 端点,该端点将显示您的用户的一些基本详细信息。您会看到类似以下内容的消息,尽管您的 subjectId 会有所不同

资源由以下用户访问:user1 (with subjectId: 43c7681a-6762-451e-8435-d503fd7a0c4d)

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

如果您想查看有关 user1 的更多信息,请导航到 http://localhost: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 社区的所有即将到来的活动。

查看全部