领先一步
VMware提供培训和认证,以加速您的进步。
了解更多在本系列文章中,我们已经介绍了使用 入门 和 隐藏服务 Spring Cloud Gateway。但是,当我们着手隐藏服务时,并没有保护它们的安全。在这篇文章中,我们将纠正这个问题。
为了保护我们的服务,我们将使用 OAuth 2.0 支持的令牌中继模式以及 Javascript 对象签名和加密 (JOSE) 和 JSON Web 令牌标准。这将为我们的用户提供一种识别自身的方法,授权应用程序查看其个人资料并访问网关后面的受保护资源。
此演示的所有代码都已在线发布 在 GitHub 上 的
secured-gateway
文件夹中。如果您只想运行它而不了解其构建方式,请跳至标题为“运行演示”的部分。
在运行演示之前,我想提请您注意其中的主要组件及其创建方式。下图显示了整个系统设计。它由三个服务的网络组成:单点登录服务器、API 网关服务器和资源服务器。
资源服务器是一个隐藏在 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
正如您在 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
链接,您将看到如下内容。
和以前一样,我们将使用 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 服务器,并要求您使用用户名和密码(在本例中为 user1
和 password
)登录。然后,UAA 将要求您“授权”网关读取 user1 的个人资料。您将看到以下屏幕来执行此操作。
尤其要注意标题为“允许使用‘resource.read’访问”的复选框。资源服务器将在允许您访问资源之前检查此范围。
单击“授权”后,您将被转发到 [/resource][7]
端点,该端点将向您显示有关您的用户的一些基本信息。您将看到类似于这样的消息,尽管您的 subjectId
将有所不同。
资源被访问:user1(subjectId:43c7681a-6762-451e-8435-d503fd7a0c4d)
这是资源服务器,确认您可以访问资源,并显示您使用了user1的身份。
如果您想查看有关user1的更多信息,请访问https://127.0.0.1:8080,这里提供了更完整的用户详细信息屏幕。它看起来像这样
最后,我们在日志中打印出了传递给资源服务器的JWT令牌,以便您可以检查它。在生产环境中我们永远不会这样做,但出于演示目的,我们认为看到它会有所帮助。它看起来像这样
resource | TRACE --- SecuredServiceApplication: ***** JWT Token: eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vbG9jYWxob3N0OjgwODAvdWFhL3Rva2VuX2tleXMiLCJraWQiOiJrZXktaWQtMSIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJm (truncated...)
您可以从日志中完整复制此令牌,并将其粘贴到jwt.io的JWT调试器中。此实用程序可以解析令牌并显示其内容。在输出中,您将找到与用户配置文件关联的用户名和范围。
日志本身也相当有启发性(尽管顺序并非保证)。它们显示了这三台服务器相互交互时发生的大部分情况。要查看一些带有附加说明的编辑后的日志重点内容,请点击此处。
完成后,使用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即可节省门票费用。如果您需要帮助说服您的经理,请尝试这些技巧。