龙空技术网

Spring云原生实战指南:11 安全性:身份验证和 SPA

启辰8 112

前言:

此刻姐妹们对“angular获取cookie”都比较注重,咱们都需要知道一些“angular获取cookie”的相关资讯。那么小编同时在网上搜集了一些有关“angular获取cookie””的相关内容,希望姐妹们能喜欢,我们一起来学习一下吧!

本章涵盖

了解 Spring 安全性基础知识使用 Keycloak 管理用户帐户使用 OpenID Connect、JWT 和 Keycloak使用 Spring Security 和 OpenID Connect 对用户进行身份验证测试 Spring 安全性和 OpenID Connect

安全性是 Web 应用程序最关键的方面之一,如果做错了,可能是最具灾难性影响的方面。出于教育目的,我现在才介绍这个话题。在实际场景中,我建议从每个新项目或功能开始就考虑安全性,并且在应用程序停用之前永远不要放弃它。

仅当用户的身份得到证明并且具有所需的权限时,访问控制系统才允许用户访问资源。为此,我们需要遵循三个关键步骤:识别、身份验证和授权。

当用户(人或机器)声明身份时,就会发生标识。在现实世界中,那是我通过说出我的名字来介绍自己的时候。在数字世界中,我会通过提供我的用户名或电子邮件地址来做到这一点。身份验证是通过护照、驾驶执照、密码、证书或令牌等因素验证用户声明的身份。当使用多个因素来验证用户的身份时,我们谈论多因素身份验证授权始终在身份验证后发生,它会检查允许用户在给定上下文中执行的操作。

本章和下一章将介绍如何在云原生应用程序中实现访问控制系统。您将看到如何向 Polar Bookshop 等系统添加身份验证,以及如何使用专用的身份和访问管理解决方案(如 Keycloak)。我将向您展示如何使用Spring Security来保护应用程序,并采用JWT,OAuth2和OpenID Connect等标准。在此过程中,您还将向系统添加 Angular 前端,并了解涉及单页应用程序 (SPA) 时的安全性最佳实践。

注意本章中示例的源代码位于第 11/11 章开始和第 11/11 章结束文件夹中,其中包含项目的初始和最终状态 ()。

11.1 了解 Spring 安全性基础知识

Spring 安全性 () 是保护 Spring 应用程序的事实标准,支持命令式和反应式堆栈。它提供身份验证和授权功能以及针对最常见攻击的保护。

该框架通过依赖筛选器提供其主要功能。让我们考虑向 Spring 引导应用程序添加身份验证的可能要求。用户应该能够通过登录表单使用其用户名和密码进行身份验证。当我们配置 Spring 安全性以启用此类功能时,框架会添加一个过滤器来拦截任何传入的 HTTP 请求。如果用户已通过身份验证,则会发送请求,由给定的 Web 处理程序(如 @RestController 类)处理。如果用户未通过身份验证,则会将用户转发到登录页面并提示输入其用户名和密码。

注意在命令式 Spring 应用程序中,过滤器被实现为 Servlet Filter 类。在反应式应用程序中,使用 WebFilter 类。

大多数 Spring 安全性功能在启用后,都是通过过滤器处理的。该框架建立了一系列过滤器,这些过滤器根据明确定义和合理的顺序执行。例如,处理身份验证的过滤器在检查授权的过滤器之前运行,因为我们无法在知道用户是谁之前验证用户的权限。

让我们从一个基本示例开始,以更好地理解 Spring 安全性的工作原理。我们希望为 Polar 书店系统添加身份验证。由于边缘服务是入口点,因此在那里处理安全性等横切问题是有意义的。用户应该能够通过登录表单使用用户名和密码进行身份验证。

首先,在边缘服务项目(边缘服务)的 build.gradle 文件中添加对 Spring 安全性的新依赖项。请记住在新添加后刷新或重新导入 Gradle 依赖项。

示例 11.1 在边缘服务中添加对 Spring 安全性的依赖关系

dependencies {  ...  implementation 'org.springframework.boot:spring-boot-starter-security' }

在 Spring Security 中定义和配置安全策略的中心位置是 SecurityWebFilterChain bean。该对象告知框架应启用哪些筛选器。您可以通过 ServerHttpSecurity 提供的 DSL 构建 SecurityWebFilterChain bean。

目前,我们希望遵守以下要求:

边缘服务公开的所有终结点都必须要求用户身份验证。身份验证必须通过登录表单页面进行。

要收集与安全性相关的所有配置,请在新的 SecurityConfig 类(com.polarbookshop.edgeservice.config 包)中创建一个 SecurityWebFilterChain bean:

@Bean         ❶SecurityWebFilterChain springSecurityFilterChain(  ServerHttpSecurity http) {}

SecurityWebFilterChain Bean 用于定义和配置应用程序的安全策略。

ServerHttpSecurity对象由Spring自动连接,为配置Spring Security和构建SecurityWebFilterChain bean提供了一个方便的DSL。使用 authorizeExchange(),您可以为任何请求定义访问策略(在反应式 Spring 中称为交换)。在这种情况下,我们希望所有请求都需要身份验证(authenticated()):

@BeanSecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {  return http     .authorizeExchange(exchange ->       exchange.anyExchange().authenticated())    ❶    .build(); }

所有请求都需要身份验证。

Spring Security 提供了几种身份验证策略,包括 HTTP Basic、登录表单、SAML 和 OpenID Connect。对于此示例,我们希望使用登录表单策略,我们可以通过 ServerHttpSecurity 对象公开的 formLogin() 方法启用该策略。我们将使用默认配置(可通过 Spring 安全定制器界面获得),其中包括一个由框架开箱即用提供的登录页面,并在请求未经过身份验证时自动重定向到该页面:

@BeanSecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {  return http    .authorizeExchange(exchange -> exchange.anyExchange().authenticated())    .formLogin(Customizer.withDefaults())      ❶    .build();}

通过登录表单启用用户身份验证

接下来,使用 @EnableWebFluxSecurity 注释 SecurityConfig 类以启用 Spring Security WebFlux 支持。最终的安全配置如以下清单所示。

清单 11.2 要求通过登录表单对所有端点进行身份验证

package com.polarbookshop.edgeservice.config; import org.springframework.context.annotation.Bean;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.reactive.➥EnableWebFluxSecurity;import org.springframework.security.config.web.server.ServerHttpSecurity;import org.springframework.security.web.server.SecurityWebFilterChain; @EnableWebFluxSecuritypublic class SecurityConfig {   @Bean  SecurityWebFilterChain springSecurityFilterChain(    ServerHttpSecurity http  ) {    return http      .authorizeExchange(exchange ->        exchange.anyExchange().authenticated())    ❶      .formLogin(Customizer.withDefaults())        ❷      .build();  }}

所有请求都需要身份验证。

通过登录表单启用用户身份验证

让我们验证它是否正常工作。首先,启动边缘服务所需的 Redis 容器。打开终端窗口,导航到保存 Docker Compose 文件的文件夹 (polar-deployment/docker/docker-compose.yml),然后运行以下命令:

$ docker-compose up -d polar-redis

然后运行边缘服务应用程序(./gradlew bootRun),打开浏览器窗口,然后前往 。您应该被重定向到 Spring 安全性提供的登录页面,您可以在其中进行身份验证。

等一会!如何在不定义用户的情况下进行身份验证?默认情况下,Spring 安全性在内存中定义一个用户帐户,其中包含用户名用户和在应用程序日志中随机生成并打印出来的密码。您应该查找如下所示的日志条目:

Using generated security password: ee60bdf6-fb82-439a-8ed0-8eb9d47bae08

您可以使用 Spring 安全性创建的预定义用户帐户进行身份验证。成功进行身份验证后,您将被重定向到 /books 终结点。由于目录服务已关闭,并且 Edge 服务具有在查询书籍时返回空列表的回退方法(在第 9 章中实现),因此您将看到一个空白页。这是意料之中的。

注意我建议您从现在开始每次测试应用程序时都打开一个新的隐身浏览器窗口。由于您将尝试不同的安全方案,因此隐身模式将防止您遇到与以前会话中的浏览器缓存和 cookie 相关的问题。

此测试的关键点是用户尝试访问边缘服务公开的受保护终结点。应用程序将用户重定向到登录页面,显示登录表单,并要求用户提供用户名和密码。然后,边缘服务根据其内部用户数据库(在内存中自动生成)验证凭据,并在发现凭据有效后启动与浏览器的经过身份验证的会话。由于HTTP是一种无状态协议,因此用户会话通过cookie保持活动状态,该cookie的值由浏览器在每个HTTP请求(会话cookie)中提供。在内部,边缘服务维护会话标识符和用户标识符之间的映射,如图 11.1 所示。

图 11.1 登录步骤完成后,用户会话通过会话 cookie 保持活动状态。

完成应用程序测试后,使用 Ctrl-C 终止该过程。然后导航到保存 Docker Compose 文件的文件夹 (polar-deployment/docker/docker-compose.yml),并运行以下命令以停止 Redis 容器:

$ docker-compose down

将以前的方法应用于云原生系统时存在一些问题。在本章的其余部分,我们将分析这些问题,确定云原生应用程序的可行解决方案,并在我们刚刚实现的内容之上使用它们。

11.2 使用Keycloak管理用户帐户

在上一节中,我们基于登录表单向 Edge 服务添加了用户身份验证。您尝试通过启动时在内存中自动生成的用户帐户登录。这对于第一次尝试Spring Security来说很好,但这不是你想要在生产中做的事情。

作为最低要求,我们需要用户帐户的持久存储和注册新用户的选项。应特别关注使用强大的加密算法存储密码并防止未经授权访问数据库。鉴于此类功能的重要性,将其委托给专用应用程序是有意义的。

Keycloak () 是由红帽社区开发和维护的开源身份和访问管理解决方案。它提供了广泛的功能,包括单点登录 (SSO)、社交登录、用户联合、多重身份验证和集中式用户管理。Keycloak依赖于OAuth2,OpenID Connect和SAML 2.0等标准。现在,我们将使用 Keycloak 来管理 Polar Bookshop 中的用户帐户。稍后我将向您展示如何使用其OpenID Connect和OAuth2功能。

注意Spring 安全性提供了实现用户管理服务的所有必要功能。如果您想了解有关此主题的更多信息,可以参考Laurenśiu Spilcă(Manning,3)的Spring Security in Action的第4章和第2020章。

您可以在本地将 Keycloak 作为独立的 Java 应用程序或容器运行。对于生产,有一些解决方案可以在 Kubernetes 上运行 Keycloak。Keycloak还需要一个关系数据库来实现持久性。它带有一个嵌入式 H2 数据库,但您需要在生产中将其替换为外部数据库。

对于 Polar Bookshop,我们将在本地运行 Keycloak 作为 Docker 容器,依赖于嵌入式 H2 数据库。在生产中,我们将使用 PostgreSQL。这似乎与环境奇偶校验原则相矛盾,但由于它是第三方应用程序,因此测试其与数据源的交互不是您的责任。

本节将指导您逐步完成 Polar 书店用例的 Keycloak 配置。首先,打开极地部署存储库。然后在docker/docker-compose.yml中定义一个新的polar-keycloak容器。

示例 11.3 在 Docker Compose 中定义 Keycloak 容器

version: "3.8"services:  ...   polar-keycloak:                           ❶    image: quay.io/keycloak/keycloak:19.0    container_name: "polar-keycloak"    command: start-dev                      ❷    environment:                            ❸      - KEYCLOAK_ADMIN=user      - KEYCLOAK_ADMIN_PASSWORD=password    ports:      - 8080:8080

描述钥匙斗篷容器的部分

在开发模式下启动Keycloak(使用嵌入式数据库)

将管理员凭据定义为环境变量

注意稍后,我将为您提供一个 JSON 文件,该文件可用于在启动 Keycloak 容器时加载整个配置,因此您无需担心容器的持久性。

您可以通过打开终端窗口,导航到保存 docker-compose.yml 文件的文件夹并运行以下命令来启动 Keycloak 容器:

$ docker-compose up -d polar-keycloak

在开始管理用户帐户之前,我们需要定义一个安全领域。我们接下来会这样做。

11.2.1 定义安全领域

在Keycloak中,应用程序或系统的任何安全方面都是在领域上下文中定义的,领域是我们在其中应用特定安全策略的逻辑域。默认情况下,Keycloak预配置了一个领域,但您可能希望为您构建的每个产品创建一个专用的领域。让我们创建一个新的 PolarBookshop 领域来托管 Polar Bookshop 系统的任何与安全相关的方面。

确保之前启动的 Keycloak 容器仍在运行。然后打开终端窗口,并在 Keycloak 容器内输入一个 bash 控制台:

$ docker exec -it polar-keycloak bash

提示钥匙斗篷可能需要几秒钟才能启动。如果在启动容器后立即尝试访问它,则可能会收到错误,因为它尚未准备好接受连接。如果发生这种情况,请等待几秒钟,然后重试。您可以使用 docker 日志 -f polar-keycloak 检查 Keycloak 日志。打印出消息“在开发模式下运行服务器”后,Keycloak 就可以使用了。

我们将通过其管理 CLI 配置 Keycloak,但您可以使用 中提供的 GUI 实现相同的结果。首先,导航到 Keycloak Admin CLI 脚本所在的文件夹:

$ cd /opt/keycloak/bin

Admin CLI 受我们在 Docker Compose 中为 Keycloak 容器定义的用户名和密码保护。在运行任何其他命令之前,我们需要启动经过身份验证的会话:

$ ./kcadm.sh config credentials \    --server  \     ❶    --realm master \                     ❷    --user user \                        ❸    --password password                  ❹

Keycloak 在容器内的端口 8080 上运行。

在 Keycloak 中配置的默认领域

我们在 Docker Compose 中定义的用户名

我们在 Docker Compose 中定义的密码

提示您应该保持当前终端窗口打开,直到您完成 Keycloak 的配置。如果在任何时候经过身份验证的会话过期,您始终可以通过运行上一个命令来启动新会话。

此时,您可以继续创建一个新的安全域,其中将存储与 Polar Bookshop 关联的所有策略:

$ ./kcadm.sh create realms -s realm=PolarBookshop -s enabled=true
11.2.2 管理用户和角色

我们需要一些用户来测试不同的身份验证方案。正如第2章所预期的那样,Polar Bookshop有两种类型的用户:客户和员工。

客户可以浏览并购买书籍。员工还可以将新图书添加到目录中、修改现有图书以及删除它们。

若要管理与每种类型的用户关联的不同权限,让我们创建两个角色:客户员工。稍后,你将基于这些角色保护应用程序终结点。这是一种称为基于角色的访问控制 (RBAC) 的授权策略。

首先,从您目前使用的 Keycloak Admin CLI 控制台在 Polar 书店领域中创建两个角色:

$ ./kcadm.sh create roles -r PolarBookshop -s name=employee$ ./kcadm.sh create roles -r PolarBookshop -s name=customer

然后创建两个用户。伊莎贝尔·达尔(Isabelle Dahl)既是书店的员工,也是书店的顾客(用户名:伊莎贝尔)。您可以按如下方式为她创建一个帐户:

$ ./kcadm.sh create users -r PolarBookshop \    -s username=isabelle \                    ❶    -s firstName=Isabelle \    -s lastName=Dahl \    -s enabled=true                           ❷ $ ./kcadm.sh add-roles -r PolarBookshop \    --uusername isabelle \                    ❸    --rolename employee \    --rolename customer

新用户的用户名。它将用于登录。

用户应处于活动状态。

伊莎贝尔既是员工又是客户。

然后对书店的顾客Bjorn Vinterberg(用户名:bjorn)做同样的事情:

$ ./kcadm.sh create users -r PolarBookshop \    -s username=bjorn \                       ❶    -s firstName=Bjorn \    -s lastName=Vinterberg \    -s enabled=true                           ❷ $ ./kcadm.sh add-roles -r PolarBookshop \    --uusername bjorn \                       ❸    --rolename customer

新用户的用户名。它将用于登录。

用户应处于活动状态。

比约恩是客户。

在真实场景中,用户会自己选择一个密码,最好启用双因素身份验证。Isabelle 和 Bjorn 是测试用户,因此分配显式密码(密码)是可以的。您可以从Keycloak Admin CLI执行此操作,如下所示:

$ ./kcadm.sh set-password -r PolarBookshop \    --username isabelle --new-password password$ ./kcadm.sh set-password -r PolarBookshop \    --username bjorn --new-password password

这就是用户管理。您可以使用 exit 命令退出 Keycloak 容器内的 bash 控制台,但保持 Keycloak 运行。

接下来,让我们探讨如何改进边缘服务中的身份验证策略。

11.3 使用 OpenID Connect、JWT 和 Keycloak 进行身份验证

目前,用户必须使用用户名和密码通过浏览器登录。由于 Keycloak 现在管理用户帐户,因此我们可以继续更新边缘服务,以使用 Keycloak 本身检查用户凭据,而不是使用其内部存储。但是,如果我们向 Polar 书店系统介绍不同的客户,例如移动应用程序和物联网设备,会发生什么?那么用户应该如何进行身份验证呢?如果书店员工已经在公司的活动目录(AD)中注册并希望通过SAML登录怎么办?我们能否跨不同的应用程序提供单点登录 (SSO) 体验?用户是否能够通过他们的GitHub或Twitter帐户(社交登录)登录

当我们获得新要求时,我们可以考虑在边缘服务中支持所有这些身份验证策略。但是,这不是一种可扩展的方法。更好的解决方案是委派专用标识提供者,以按照任何受支持的策略对用户进行身份验证。然后,边缘服务将使用该服务来验证用户的身份,而无需担心执行实际的身份验证步骤。专用服务可以让用户以各种方式进行身份验证,例如使用系统中注册的凭据、通过社交登录或通过 SAML 依赖公司 AD 中定义的身份。

使用专用服务对用户进行身份验证会导致我们需要解决两个方面才能使系统正常工作。首先,我们需要为 Edge 服务建立一个协议,以便将用户身份验证委托给标识提供者,并为标识提供者提供有关身份验证结果的信息。其次,我们需要定义一种数据格式,标识提供者可以使用该格式在成功通过用户身份验证后安全地通知边缘服务有关用户的身份。本节将使用OpenID Connect和JSON Web Token解决这两个问题。

11.3.1 使用 OpenID Connect 对用户进行身份验证

OpenID Connect (OIDC) 是一种协议,它使应用程序(称为客户端)能够根据受信任方(称为授权服务器)执行的身份验证来验证用户的身份并检索用户配置文件信息。授权服务器通过 ID 令牌通知客户端应用程序身份验证步骤的结果。

OIDC 是 OAuth2 之上的身份层,OAuth2 是一个授权框架,它解决了使用令牌进行授权但不处理身份验证来委派访问权限的问题。如您所知,授权只能在身份验证后进行。这就是为什么我决定首先介绍 OIDC;OAuth<>将在下一章中进一步探讨。这不是涵盖这些主题的典型方式,但我认为在设计访问控制系统时是有意义的,就像我们为 Polar Bookshop 所做的那样。

注意本书将只介绍OAuth2和OIDC的一些基本方面。如果你有兴趣了解更多关于它们的信息,曼宁的目录中有几本关于这个主题的书:贾斯汀·里奇和安东尼奥·桑索的《OAuth 2 in Action》(曼宁,2017 年)和普拉巴斯·西里瓦德纳的 OpenID Connect in Action(曼宁,2022 年)。

在处理用户身份验证时,我们可以确定 OAuth2 框架中 OIDC 协议使用的三个主要参与者:

授权服务器 - 负责对用户进行身份验证和颁发令牌的实体。在极地书店,这将是Keycloak。用户 - 也称为资源所有者,这是使用授权服务器登录以获取对客户端应用程序的身份验证访问权限的人员。在极地书店,要么是顾客,要么是员工。客户端 - 需要对用户进行身份验证的应用程序。这可以是移动应用程序、基于浏览器的应用程序、服务器端应用程序,甚至是智能电视应用程序。在Polar Bookshop,它是边缘服务。

图 11.2 显示了如何将这三个参与者映射到 Polar 书店架构。

图 11.2 如何将 OIDC/OAuth2 角色分配给 Polar 书店架构中的实体以进行用户身份验证

注意OAuth2 框架定义的角色在 OpenID Connect 上下文中使用时也具有不同的名称。OAuth2 授权服务器也称为 OIDC 提供程序。OAuth2 客户端依赖于授权服务器进行身份验证和令牌颁发,也称为信赖方 (RP)。OAuth2 用户也称为最终用户。为了保持一致性,我们将坚持使用 OAuth2 命名,但了解 OIDC 中使用的替代术语会很有帮助。

在 Polar Bookshop 中,边缘服务将启动用户登录流程,但随后它会通过 OIDC 协议(Spring Security 开箱即用)将实际的身份验证步骤委托给 Keycloak。Keycloak提供了多种身份验证策略,包括传统的登录表单,通过GitHub或Twitter等提供商的社交登录以及SAML。它还支持双因素身份验证 (2FA)。在以下部分中,我们将使用登录表单策略作为示例。由于用户将直接与Keycloak交互以登录,因此除了Keycloak之外,他们的凭据永远不会暴露给系统的任何组件,这是采用这种解决方案的好处之一。

当未经身份验证的用户调用 Edge 服务公开的安全终结点时,将发生以下情况:

边缘服务(客户端)将浏览器重定向到 Keycloak(授权服务器)进行身份验证。Keycloak 对用户进行身份验证(例如,通过登录表单请求用户名和密码),然后将浏览器重定向回边缘服务以及授权代码边缘服务调用 Keycloak 以将授权代码与 ID 令牌交换,其中包含有关经过身份验证的用户的信息。边缘服务根据会话 Cookie 初始化与浏览器的经过身份验证的用户会话。在内部,边缘服务维护会话标识符和 ID 令牌(用户标识)之间的映射。

注意OIDC 支持的身份验证流程基于 OAuth2 授权代码流。第二步可能看起来是多余的,但授权代码对于确保只有合法客户端才能与令牌交换它至关重要。

图 11.3 描述了 OIDC 协议支持的身份验证流程的基本部分。即使 Spring Security 支持开箱即用,并且您不需要自己实现任何内容,但对流程有一个概述仍然是有益的。

图11.3 OIDC协议支持的认证流程

采用图 11.3 所示的身份验证流程时,Edge 服务不受特定身份验证策略的影响。我们可以将Keycloak配置为使用Active Directory或通过GitHub执行社交登录,边缘服务不需要任何更改。它只需要支持 OIDC 来验证身份验证是否正确发生,并通过 ID 令牌获取有关用户的信息。什么是 ID 令牌?它是一个 JSON Web 令牌 (JWT),包含有关用户身份验证事件的信息。我们将在下一节中仔细研究 JWT。

注意每当我提到OIDC时,我指的是OpenID Connect Core 1.0规范()。每当我提到OAuth2时,除非另有说明,否则我指的是目前正在标准化()的OAuth 2.1规范,旨在取代RFC 2()中描述的OAuth 6749.6749标准。

11.3.2 与智威汤逊交换用户信息

在分布式系统(包括微服务和云原生应用程序)中,交换有关经过身份验证的用户及其授权的信息的最常用策略是通过令牌。

JSON Web 令牌 (JWT) 是用于表示要在双方之间转移的声明的行业标准。它是一种广泛使用的格式,用于在分布式系统中的不同方之间安全地传播有关经过身份验证的用户及其权限的信息。JWT 本身不使用,但它包含在更大的结构 JSON Web 签名 (JWS) 中,该结构通过对 JWT 对象进行数字签名来确保声明的完整性。

数字签名 JWT (JWS) 是由以 Base64 编码并由点 (.) 字符分隔的三个部分组成的字符串:

<header>.<payload>.<signature>

注意出于调试目的,可以使用 上提供的工具对令牌进行编码和解码。

如您所见,数字签名的 JWT 由三个部分组成:

标头 — JSON 对象(称为 JOSE 标头),包含有关对有效负载执行的加密操作的信息。这些操作遵循 Javascript 对象签名和加密 (JOSE) 框架中的标准。解码的标头如下所示:{ "alg": "HS256", "typ": "JWT" } 用于对令牌进行数字签名的算法 代币类型有效负载 - 包含令牌传达的声明的 JSON 对象(称为声明集)。JWT 规范定义了一些标准声明名称,但您也可以定义自己的声明名称。解码的有效负载如下所示:{ "iss": ";, "sub": "isabelle", "exp": 1626439022 } 发行JWT的实体(发行人) 作为 JWT 主体的实体(最终用户) JWT 何时过期(时间戳)签名 - JWT 的签名,确保声明未被篡改。使用 JWS 结构的先决条件是我们信任发行令牌的实体(颁发者),并且我们有办法检查其有效性。

当 JWT 需要完整性和机密性时,它首先作为 JWS 签名,然后使用 JSON Web 加密 (JWE) 进行加密。在本书中,我们将只使用 JWS。

注意如果您有兴趣了解有关 JWT 及其相关方面的更多信息,可以参考 IETF 标准规范。JSON Web Token (JWT) 记录在 RFC 7519 () 中,JSON Web Signature (JWS) 在 RFC 7515 () 中描述,JSON Web 加密 (JWE) 在 RFC 7516 () 中描述。您可能还对 JSON Web 算法 (JWA) 感兴趣,它定义了 JWT 的可用加密操作,并在 RFC 7518 ( rfc7518) 中详细介绍。

对于 Polar Bookshop,边缘服务可以将身份验证步骤委托给 Keycloak。成功对用户进行身份验证后,Keycloak 将向边缘服务发送一个 JWT,其中包含有关新经过身份验证的用户(ID 令牌)的信息。边缘服务将通过其签名验证 JWT,并对其进行检查以检索有关用户的数据(声明)。最后,它将基于会话 cookie 与用户的浏览器建立经过身份验证的会话,其标识符映射到 JWT。

若要安全地委派身份验证和检索令牌,必须在 Keycloak 中将边缘服务注册为 OAuth2 客户端。让我们看看如何。

11.3.3 在Keycloak中注册应用程序

正如您在前面的部分中了解到的,OAuth2 客户端是一个应用程序,可以请求用户身份验证并最终从授权服务器接收令牌。在 Polar 书店架构中,此角色由边缘服务扮演。使用 OIDC/OAuth2 时,您需要先向授权服务器注册每个 OAuth2 客户端,然后再将其用于对用户进行身份验证。

客户可以是公开的,也可以是保密的。如果应用程序无法保密,我们会将其注册为公共客户端。例如,移动应用程序将被注册为公共客户端。另一方面,机密客户端是可以保密的客户端,它们通常是边缘服务等后端应用程序。无论哪种方式,注册过程都是相似的。主要区别在于机密客户端需要使用授权服务器对自己进行身份验证,例如依赖于共享机密。这是我们无法用于公共客户端的额外保护层,因为它们无法安全地存储共享密钥。

OAuth2 中的客户端困境

可以将客户端角色分配给前端或后端应用程序。主要区别在于解决方案的安全级别。客户端是将从授权服务器接收令牌的实体。客户端必须将它们存储在某个地方,以便在来自同一用户的后续请求中使用。令牌是应该保护的敏感数据,没有比后端应用程序更好的地方了。但这并不总是可能的。

这是我的经验法则。如果前端是 iOS 或 Android 等移动或桌面应用程序,则为 OAuth2 客户端,它将被归类为公共客户端。您可以使用 AppAuth () 等库来添加对 OIDC/OAuth2 的支持,并将令牌尽可能安全地存储在设备上。如果前端是 Web 应用程序(如 Polar Bookshop 中),则后端服务应该是客户端。在这种情况下,它将被归类为机密客户端。

这种区别的原因是,无论您如何尝试在浏览器中隐藏 OIDC/OAuth2 令牌(cookie、本地存储、会话存储),它们始终面临被暴露和滥用的风险。“从安全角度来看,在前端Web应用程序中保护令牌几乎是不可能的。这就是应用程序安全专家Philippe De Ryck所写的,建议工程师依靠后端对前端模式,并让后端应用程序处理令牌。

我建议将浏览器和后端之间的交互基于会话 cookie(就像您对单体架构所做的那样),并让后端应用程序负责控制身份验证流并使用授权服务器颁发的令牌,即使在 SPA 的情况下也是如此。这是安全专家推荐的当前最佳实践。

a P. De Ryck,“单页应用程序中刷新令牌轮换的批判性分析”,Ping Identity 博客,18 年 2021 月 6 日,;>

.

由于边缘服务将是 Polar 书店系统中的 OAuth2 客户端,因此让我们使用 Keycloak 注册它。我们可以再次依赖Keycloak Admin CLI。

确保之前启动的 Keycloak 容器仍在运行。然后打开终端窗口并进入 Keycloak 容器内的 bash 控制台:

$ docker exec -it polar-keycloak bash

接下来,导航到 Keycloak 管理 CLI 脚本所在的文件夹:

$ cd /opt/keycloak/bin

如前所述,管理 CLI 受我们在 Docker Compose 中为 Keycloak 容器定义的用户名和密码保护,因此我们需要在运行任何其他命令之前启动经过身份验证的会话:

$ ./kcadm.sh config credentials --server  \    --realm master --user user --password password

最后,在 PolarBookshop 领域将边缘服务注册为 OAuth2 客户端:

$ ./kcadm.sh create clients -r PolarBookshop \    -s clientId=edge-service \                       ❶    -s enabled=true \                                ❷    -s publicClient=false \                          ❸    -s secret=polar-keycloak-secret \                ❹    -s 'redirectUris=[";,    ➥"*"]'  ❺

OAuth2 客户端标识符

必须启用它。

边缘服务是机密客户端,而不是公共客户端。

由于它是一个机密客户端,它需要一个秘密来使用 Keycloak 进行身份验证。

Keycloak有权在用户登录或注销后重定向请求的应用程序URL

有效的重定向 URL 是 OAuth2 客户端应用程序(边缘服务)公开的终结点,Keycloak 将在其中重定向身份验证请求。由于 Keycloak 可以在重定向请求中包含敏感信息,因此我们希望限制哪些应用程序和端点有权接收此类信息。稍后您将了解到,身份验证请求的重定向 URL 将遵循 Spring Security 提供的默认格式为 oauth2/code/*。为了支持注销操作后的重定向,我们还需要添加 作为有效的重定向 URL。

本节就是这样。在本书随附的源代码存储库中,我包含一个JSON文件,您可以在将来启动Keycloak容器时使用它来加载整个配置(Chapter11/11-end/polar-deployment/docker/keycloak/realm-config.json)。现在,你已熟悉 Keycloak,可以更新容器定义,以确保在启动时始终具有所需的配置。将 JSON 文件复制到您自己的项目中的相同路径上,并更新 docker-compose.yml 文件中的 polar-keycloak 服务,如下所示。

清单 11.4 在 Keycloak 容器中导入领域配置

version: "3.8"services:  ...   polar-keycloak:    image: quay.io/keycloak/keycloak:19.0    container_name: "polar-keycloak"    command: start-dev --import-realm          ❶    volumes:                                   ❷      - ./keycloak:/opt/keycloak/data/import     environment:      - KEYCLOAK_ADMIN=user      - KEYCLOAK_ADMIN_PASSWORD=password    ports:      - 8080:8080

在启动时导入提供的配置

配置卷以将配置文件加载到容器中

为什么选择钥匙斗篷

我决定使用 Keycloak,因为它是一个成熟的开源解决方案,用于自己运行授权服务器。在社区的需求不断增加之后,Spring 启动了一个新的 Spring 授权服务器项目 ()。自版本 0.2.0 以来,它已成为用于设置 OAuth2 授权服务器的生产就绪解决方案。在撰写本文时,该项目为最常见的 OAuth2 功能提供了一个实现,并且目前正在努力扩展对 OIDC 特定功能的支持。您可以在 GitHub 上跟踪进度并为项目做出贡献。

另一种选择是使用SaaS解决方案,如Okta()或Auth0()。它们都是将 OIDC/OAuth2 作为托管服务的绝佳解决方案,我鼓励您尝试一下。对于本书,我想使用一个解决方案,您可以在本地环境中运行并可靠地重现,而不依赖于可能随时间变化的其他服务,从而使我在此处的说明无效。

在继续之前,让我们停止任何正在运行的容器。打开终端窗口,导航到保存 Docker Compose 文件的文件夹 (polar-deployment/ docker/docker-compose.yml),然后运行以下命令:

$ docker-compose down

我们现在拥有重构边缘服务的所有部分,因此它可以使用依赖于 OIDC/OAuth2、JWT 和 Keycloak 的身份验证策略。最好的部分是它基于标准,并受到所有主要语言和框架(前端,后端,移动,物联网)的支持,包括Spring Security。

11.4 使用 Spring 安全性和 OpenID Connect 对用户进行身份验证

如前所述,Spring 安全性支持多种身份验证策略。边缘服务的当前安全设置通过应用程序本身提供的登录表单处理用户帐户和身份验证。现在您已经了解了OpenID Connect,我们可以重构应用程序,通过OIDC协议将用户身份验证委托给Keycloak。

对OAuth2的支持曾经在一个名为Spring Security OAuth的单独项目中,您将将其用作Spring Cloud Security的一部分,以便在云原生应用程序中采用OAuth2。这两个项目现在都被弃用了,取而代之的是 Spring Security 主项目中引入的对 OAuth2 和 OpenID Connect 的原生、更全面的支持,从版本 5 开始。本章重点介绍如何使用 Spring Security 2 中新的 OIDC/OAuth5 支持来验证 Polar Bookshop 的用户。

注意如果您发现自己正在使用已弃用的 Spring Security OAuth 和 Spring Cloud Security 项目进行项目,您可能需要查看 Laurenţiu Spilcǎ (Manning, 12) 的 Spring Security in Action 的第 15 章到第 2020 章,其中对它们进行了非常详细的解释。

使用 Spring Security 及其 OAuth2/OIDC 支持,本节将向您展示如何对边缘服务执行以下操作:

使用 OpenID Connect 对用户进行身份验证。配置用户注销。提取有关经过身份验证的用户的信息。

让我们开始吧!

11.4.1 添加新的依赖项

首先,我们需要更新边缘服务的依赖项。我们可以用更具体的 OAuth2 客户端依赖替换现有的 Spring Security 启动器依赖项,它增加了对 OIDC/OAuth2 客户端功能的支持。此外,我们可以添加 Spring 安全测试依赖项,它为 Spring 中的测试安全场景提供了额外的支持。

打开边缘服务项目(边缘服务)的 build.gradle 文件并添加新依赖项。请记住在新添加后刷新或重新导入 Gradle 依赖项。

11.5 示例 为 Spring 安全性 OAuth2 客户端添加依赖项

dependencies {  ...  implementation   ➥ 'org.springframework.boot:spring-boot-starter-oauth2-client'   testImplementation 'org.springframework.security:spring-security-test' }

弹簧与Keycloak集成

当选择Keycloak作为授权服务器时,Spring Security提供的本机OpenID Connect/OAuth2支持的替代方案是Keycloak Spring Adapter。它是一个由Keycloak项目本身提供的库,用于与Spring Boot和Spring Security集成,但在Keycloak 17发布后就退役了。

如果您发现自己正在使用 Keycloak 弹簧适配器进行项目,您可能需要查看我关于该主题的文章 () 或 Spring 微服务在行动,第二版的第 9 章,作者是 John Carnell 和 Illary Huaylupo Sánchez(Manning,2021 年)。

11.4.2 配置 Spring Security 和 Keycloak 之间的集成

在 Spring Security 上添加相关依赖项后,我们需要配置与 Keycloak 的集成。在上一节中,我们将 Keycloak 中的边缘服务注册为 OAuth2 客户端,同时定义了客户端标识符(边缘服务)和共享密钥(极点密钥密钥)。现在,我们将使用该信息告诉Spring Security如何与Keycloak进行交互。

在边缘服务项目中打开 application.yml 文件,然后添加以下配置。

清单 11.6 将边缘服务配置为 OAuth2 客户端

spring:  security:    oauth2:      client:        registration:          keycloak:                                 ❶            client-id: edge-service                 ❷            client-secret: polar-keycloak-secret    ❸            scope: openid                           ❹        provider:          keycloak:                                 ❺            issuer-uri:➥        ❻

在 Spring 安全性中标识客户端注册的名称(称为“注册 ID”)。它可以是任何字符串。

Keycloak 中定义的 OAuth2 客户端标识符

客户端用于使用 Keycloak 进行身份验证的共享密钥

客户端希望有权访问的范围列表。openid 作用域在 OAuth2 之上触发 OIDC 身份验证。

与上面几行的“注册 ID”使用相同的名称

Keycloak URL,提供有关特定领域的所有相关 OAuth2 和 OIDC 端点的信息

Spring 安全性中的每个客户端注册都必须有一个标识符(注册 ID)。在此示例中,它是钥匙斗篷。注册标识符用于构建 Spring Security 从 Keycloak 接收授权代码的 URL。默认 URL 模板是 /login/oauth2/code/{registrationId}。对于边缘服务,完整的 URL 是 ,我们已在 Keycloak 中将其配置为有效的重定向 URL。

作用域是 OAuth2 概念,用于限制应用程序对用户资源的访问。您可以将它们视为角色,但适用于应用程序而不是用户。当我们在 OAuth2 之上使用 OpenID Connect 扩展来验证用户的身份时,我们需要包含 openid 范围以通知授权服务器并接收包含有关用户身份验证数据的 ID 令牌。下一章将更多地解释授权上下文中的范围。

现在我们已经定义了与Keycloak的集成,让我们配置Spring Security以应用所需的安全策略。

11.4.3 基本 Spring 安全性配置

在 Spring 安全性中定义和配置安全策略的中心位置是 SecurityWebFilterChain 类。 边缘服务当前配置为要求对所有端点进行用户身份验证,并使用基于登录表单的身份验证策略。让我们将其更改为使用 OIDC 身份验证。

ServerHttpSecurity 对象提供了两种在 Spring 安全性中配置 OAuth2 客户端的方法。使用 oauth2Login(),您可以将应用程序配置为充当 OAuth2 客户端,还可以通过 OpenID Connect 对用户进行身份验证。使用 oauth2Client(),应用程序不会对用户进行身份验证,因此由您来定义另一种身份验证机制。我们想使用 OIDC 身份验证,所以我们将使用 oauth2Login() 和默认配置。更新 SecurityConfig 类,如下所示。

示例 11.7 要求通过 OIDC 对所有端点进行身份验证

@EnableWebFluxSecuritypublic class SecurityConfig {   @Bean  SecurityWebFilterChain springSecurityFilterChain(   ServerHttpSecurity http  ) {    return http      .authorizeExchange(exchange ->        exchange.anyExchange().authenticated())      .oauth2Login(Customizer.withDefaults())      ❶      .build();  }}

通过 OAuth2/OpenID Connect 启用用户身份验证

让我们验证一下这是否正常工作。首先,启动 Redis 和 Keycloak 容器。打开终端窗口,导航到保存 Docker Compose 文件的文件夹 (polar-deployment/docker/docker-compose.yml),然后运行以下命令:

$ docker-compose up -d polar-redis polar-keycloak

然后运行边缘服务应用程序(./gradlew bootRun),打开浏览器窗口,然后前往 。您应该被重定向到 Keycloak 提供的登录页面,您可以在其中进行身份验证为我们之前创建的用户之一(图 11.4)。

图 11.4 边缘服务触发 OIDC 身份验证流后显示的 Polar 书店领域的 Keycloak 登录页面

例如,以伊莎贝尔(伊莎贝尔/密码)身份登录,并注意 Keycloak 在验证提供的凭据后如何将您重定向回边缘服务。由于 Edge 服务不会通过根终结点公开任何内容,因此你将看到一条错误消息(“白标错误页”)。但别担心!这就是我们稍后将集成 Angular 前端的地方。此测试的关键点是,边缘服务要求您在访问其任何端点之前进行身份验证,并且它触发了 OIDC 身份验证流。

试用完 OIDC 身份验证流程后,使用 Ctrl-C 停止应用程序。

如果身份验证成功,Spring 安全性将启动与浏览器的身份验证会话并保存有关用户的信息。在下一节中,你将了解如何检索和使用该信息。

11.4.4 检查经过身份验证的用户上下文

作为身份验证过程的一部分,Spring 安全性定义了一个上下文来保存有关用户的信息并将用户会话映射到 ID 令牌。在本部分中,你将详细了解此上下文、涉及哪些类以及如何检索数据并通过边缘服务中的新 /user 终结点公开数据。

首先,让我们定义一个 User 模型来收集经过身份验证的用户的用户名、名字、姓氏和角色。这与我们在 Keycloak 中注册两个用户时提供的信息相同,也是 ID 令牌中返回的信息。在新的 com.polarbookshop.edgeservice.user 包中,按如下所示创建用户记录。

示例 11.8 创建用户记录以保存有关经过身份验证的用户的信息

package com.polarbookshop.edgeservice.user; import java.util.List; public record User(     ❶  String username,  String firstName,  String lastName,  List<String> roles){}

保存用户数据的不可变数据类

独立于采用的身份验证策略(无论是用户名/密码、OpenID Connect/OAuth2 还是 SAML2),Spring 安全性将有关经过身份验证的用户(也称为主体)的信息保存在身份验证对象中。在OIDC的情况下,主体对象是OidcUser类型,它是Spring Security存储ID令牌的地方。反过来,身份验证保存在 SecurityContext 对象中。

访问当前登录用户的身份验证对象的一种方法是从从 ReactiveSecurityContextHolder(或命令式应用程序的 SecurityContextHolder)检索的相关 SecurityContext 中提取该对象。 图 11.5 说明了所有这些对象如何相互关联。

图 11.5 用于存储有关当前经过身份验证的用户的信息的主类

您可以通过执行以下操作来实现这项工作:

在 com.polarbookshop.edgeservice.user 包中创建用 @RestController 注释的 UserController 类。定义一种方法来处理对新 /user 终结点的 GET 请求。返回当前经过身份验证的用户的用户对象,从 OidcUser 检索必要的信息。为了获得正确的数据,我们可以使用图 11.5 中所示的调用层次结构。

用户控制器类中的结果方法将如下所示:

@GetMapping("user")public Mono<User> getUser() {  return ReactiveSecurityContextHolder.getContext()   ❶    .map(SecurityContext::getAuthentication)          ❷    .map(authentication ->      (OidcUser) authentication.getPrincipal())       ❸    .map(oidcUser ->                                  ❹      new User(        oidcUser.getPreferredUsername(),        oidcUser.getGivenName(),        oidcUser.getFamilyName(),        List.of("employee", "customer")      )  );}

从 ReactiveSecurityContextHolder 获取当前经过身份验证的用户的安全上下文

从安全上下文获取身份验证

从身份验证中获取主体。对于 OIDC,它是 OidcUser 类型。

使用来自 OidcUser 的数据构建用户对象(从 ID 令牌中提取)

下一章重点介绍授权策略,我们将配置 Keycloak 以在 ID 令牌中包含自定义角色声明,并使用该值在 UserController 类中生成 User 对象。在此之前,我们将使用固定的值列表。

对于Spring Web MVC和WebFlux控制器,除了直接使用ReactiveSecurityContextHolder之外,我们还可以使用注解@CurrentSecurityContext和@AuthenticationPrincipal分别注入SecurityContext和主体(在本例中为OidcUser)。

让我们通过将 OidcUser 对象直接作为参数注入来简化 getUser() 方法的实现。用户控制器类的最终结果显示在下面的清单中。

清单 11.9 返回有关当前经过身份验证的用户的信息

package com.polarbookshop.edgeservice.user; import java.util.List;import reactor.core.publisher.Mono;import org.springframework.security.core.annotation.➥AuthenticationPrincipal;import org.springframework.security.oauth2.core.oidc.user.OidcUser;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController; @RestControllerpublic class UserController {   @GetMapping("user")  public Mono<User> getUser(   @AuthenticationPrincipal OidcUser oidcUser    ❶  ) {    var user = new User(                         ❷      oidcUser.getPreferredUsername(),      oidcUser.getGivenName(),      oidcUser.getFamilyName(),      List.of("employee", "customer")    );    return Mono.just(user);                      ❸  }}

注入一个 OidcUser 对象,其中包含有关当前经过身份验证的用户的信息

从 OidcUser 中包含的相关声明构建用户对象

将用户对象包装在反应式发布服务器中,因为边缘服务是反应式应用程序

确保 Keycloak 和 Redis 仍在上一节中运行,运行 Edge 服务应用程序 (./gradlew bootRun),打开隐身浏览器窗口,然后导航到 。Spring 安全性会将您重定向到 Keycloak,这将提示您使用用户名和密码登录。例如,以 Bjorn(bjorn/password)的身份进行身份验证。成功进行身份验证后,将重定向回 /user 终结点。结果如下:

{  "username": "bjorn",  "firstName": "Bjorn",  "lastName": "Vinterberg",  "roles": [    "employee",    "customer"  ]}

注意角色列表包括硬编码值。在下一章中,我们将更改它以返回分配给Keycloak中每个用户的实际角色。

试用完新终结点后,使用 Ctrl-C 停止应用程序,并使用 docker-compose 关闭容器。

考虑一下当您尝试访问 /user 端点并被重定向到 Keycloak 时发生了什么。成功验证用户的凭据后,Keycloak 将边缘服务回调,并为新经过身份验证的用户发送 ID 令牌。然后,边缘服务存储令牌,并将浏览器重定向到所需的端点以及会话 Cookie。从那时起,浏览器与边缘服务之间的任何通信都将使用该会话 Cookie 来标识该用户的经过身份验证的上下文。不会向浏览器公开任何令牌。

ID Token存储在OidcUser中,是身份验证的一部分,最终包含在SecurityContext中。在第 9 章中,我们使用 Spring 会话项目使边缘服务将会话数据存储在外部数据服务 (Redis) 中,以便它可以保持无状态并能够横向扩展。SecurityContext 对象包含在会话数据中,因此会自动存储在 Redis 中,从而使边缘服务可以毫无问题地横向扩展。

检索当前经过身份验证的用户(主体)的另一个选项是从与特定 HTTP 请求(称为交换)关联的上下文中检索。我们将使用该选项来更新速率限制器配置。在第9章中,我们使用Spring Cloud Gateway和Redis实现了速率限制。目前,速率限制是根据每秒收到的请求总数计算的。我们应该更新它以独立地将速率限制应用于每个用户。

打开 RateLimiterConfig 类并配置应如何从请求中提取当前经过身份验证的主体的用户名。如果未定义用户(即请求未经身份验证、匿名),我们将使用默认密钥对所有未经身份验证的请求作为一个整体应用速率限制。

11.10 为每个用户配置速率限制

@Configurationpublic class RateLimiterConfig {   @Bean  KeyResolver keyResolver() {    return exchange -> exchange.getPrincipal()    ❶      .map(Principal::getName)                    ❷      .defaultIfEmpty("anonymous");               ❸  }}

从当前请求(交换)中获取当前经过身份验证的用户(委托人)

从主体中提取用户名

如果请求未经身份验证,它将使用“匿名”作为应用速率限制的默认密钥。

使用 OpenID Connect 对 Polar Bookshop 用户进行身份验证的基本配置到此结束。以下部分将介绍注销在 Spring 安全性中的工作原理,以及我们如何为 OAuth2/OIDC 场景自定义它。

11.4.5 在 Spring 安全性和 Keycloak 中配置用户注销

到目前为止,我们已经解决了在分布式系统中对用户进行身份验证的挑战和解决方案。不过,我们应该考虑用户注销时会发生什么。

在 Spring 安全性中,注销会导致与用户关联的所有会话数据被删除。当使用OpenID Connect/OAuth2时,Spring Security为该用户存储的令牌也会被删除。但是,用户仍将在 Keycloak 中具有活动会话。正如身份验证过程同时涉及 Keycloak 和边缘服务一样,完全注销用户需要将注销请求传播到这两个组件。

默认情况下,对受 Spring 安全性保护的应用程序执行的注销不会影响 Keycloak。幸运的是,Spring Security 提供了“OpenID Connect RP 发起的注销”规范的实现,该规范定义了注销请求应如何从 OAuth2 客户端(信赖方)传播到授权服务器。稍后将了解如何为 Edge 服务配置它。

注意OpenID Connect 规范包括几种不同的会话管理和注销方案。如果您想了解更多信息,我建议您查看 OIDC 会话管理 ()、OIDC 前通道注销 ()、OIDC 反向通道注销 () 和 OIDC RP 启动注销 ()。

Spring 安全性支持通过向框架实现和公开的 /logout 端点发送 POST 请求来注销。我们希望启用 RP 启动的注销方案,以便当用户注销应用程序时,他们也从授权服务器注销。Spring Security 完全支持此场景,并提供了一个 OidcClientInitiatedServerLogoutSuccessHandler 对象,可用于配置如何将注销请求传播到 Keycloak。

假设启用了 RP 启动的注销功能。在这种情况下,在用户成功注销 Spring 安全性后,边缘服务将通过浏览器(使用重定向)向 Keycloak 发送注销请求。接下来,您可能还希望在授权服务器上执行注销操作后将用户重定向回应用程序。

您可以使用 setPostLogoutRedirectUri() 方法配置注销后应将用户重定向到的位置,该方法由 OidcClientInitiatedServerLogoutSuccessHandler 类公开。您可以指定直接 URL,但由于主机名、服务名称和协议(http 与 https)等许多变量,该 URL 在云环境中无法正常工作。Spring 安全团队知道这一点,他们添加了对在运行时动态解析的占位符的支持。您可以使用 {baseUrl} 占位符,而不是硬编码 URL 值。在本地运行边缘服务时,占位符将解析为 。如果您在具有 TLS 终止的代理后面的云中运行它,并且可通过 DNS 名称 polarbookshop.com 访问,它将自动替换为 。

但是,Keycloak 中的客户端配置需要确切的 URL。这就是我们在 Keycloak 中注册边缘服务时将 添加到有效重定向 URL 列表中的原因。在生产中,您必须更新Keycloak中的有效重定向URL列表,以匹配那里使用的实际URL。

图 11.6 说明了我刚才描述的注销场景。

图 11.6 当用户注销时,Spring Security 首先处理请求,然后转发到 Keycloak,最后将用户重定向到应用程序。

由于 Spring 安全性中已默认提供了应用程序的注销功能,因此您只需启用和配置 RP 启动的边缘服务注销:

在 SecurityConfig 类中,定义一个 oidcLogoutSuccessHandler() 方法来构建 OidcClientInitiatedServerLogoutSuccessHandler 对象。使用 setPostLogoutRedirectUri() 方法配置注销后重定向 URL。从 SecurityWebFilterChain bean 中定义的 logout() 配置调用 oidcLogoutSuccessHandler() 方法。

在 SecurityConfig 类中生成的配置如下所示。

清单 11.11 配置 RP 启动的注销和注销时重定向

package com.polarbookshop.edgeservice.config; import org.springframework.context.annotation.Bean;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.reactive.➥ EnableWebFluxSecurity;import org.springframework.security.config.web.server.ServerHttpSecurity;import org.springframework.security.oauth2.client.oidc.web.server.logout. ➥ OidcClientInitiatedServerLogoutSuccessHandler; import org.springframework.security.oauth2.client.registration. ➥ ReactiveClientRegistrationRepository; import org.springframework.security.web.server.SecurityWebFilterChain;import org.springframework.security.web.server.authentication.logout. ➥ ServerLogoutSuccessHandler;  @EnableWebFluxSecuritypublic class SecurityConfig {   @Bean  SecurityWebFilterChain springSecurityFilterChain(    ServerHttpSecurity http,    ReactiveClientRegistrationRepository clientRegistrationRepository   ) {    return http      .authorizeExchange(exchange ->        exchange.anyExchange().authenticated())      .oauth2Login(Customizer.withDefaults())      .logout(logout -> logout.logoutSuccessHandler(               ❶        oidcLogoutSuccessHandler(clientRegistrationRepository)))       .build();  }   private ServerLogoutSuccessHandler oidcLogoutSuccessHandler(     ReactiveClientRegistrationRepository clientRegistrationRepository   ) {     var oidcLogoutSuccessHandler =         new OidcClientInitiatedServerLogoutSuccessHandler(           clientRegistrationRepository);     oidcLogoutSuccessHandler       .setPostLogoutRedirectUri("{baseUrl}");                      ❷    return oidcLogoutSuccessHandler;   } }

为成功完成注销操作的方案定义自定义处理程序

从 OIDC 提供程序注销后,Keycloak 会将用户重定向到从 Spring 动态计算的应用程序基础 URL(本地,它是 )。

注意ReactiveClientRegistrationRepository bean由Spring Boot自动配置,用于存储有关在Keycloak注册的客户端的信息,Spring Security将其用于身份验证/授权目的。在我们的示例中,只有一个客户端:我们之前在 application.yml 文件中配置的客户端。

我暂时不会要求您测试注销功能。在我们向 Polar Bookshop 系统引入 Angular 前端后,原因将显而易见。

基于 OpenID Connect/OAuth2 的用户身份验证功能现已完成,包括注销和可扩展性问题。如果边缘服务使用像 Thymeleaf 这样的模板引擎来构建前端,我们到目前为止所做的工作就足够了。但是,当您将安全的后端应用程序与 Angular 等 SPA 集成时,还需要考虑更多方面。这将是下一节的重点。

11.5 将 Spring 安全性与 SPA 集成

微服务架构和其他分布式系统的 Web 前端部分通常使用 Angular、React 或 Vue 等框架构建为一个或多个单页应用程序。分析如何创建 SPA 不在本书的讨论范围内,但必须了解需要哪些更改来支持此类前端客户端。

到目前为止,您已经通过终端窗口与组成 Polar 书店系统的服务进行了交互。在本节中,我们将添加一个 Angular 应用程序,该应用程序将成为系统的前端。它将由 NGINX 容器提供服务,并可通过边缘服务提供的网关访问。支持 SPA 将需要在 Spring 安全性中进行一些额外的配置,以解决跨源请求共享 (CORS) 和跨站点请求伪造 (CSRF) 等问题。本节介绍如何执行此操作。

11.5.1 运行 Angular 应用程序

Polar Bookshop系统将有一个Angular应用程序作为前端。由于本书没有涉及前端技术和模式,我已经准备了一个。我们只需要决定如何将其包含在 Polar 书店系统中。

一种选择是让边缘服务为 SPA 静态资源提供服务。为前端服务的 Spring Boot 应用程序通常将源代码托管在 src/main/resources 中。当使用像Thymeleaf这样的模板引擎时,这是一个方便的策略,但对于像Angular这样的SPA,我更喜欢将代码保存在一个单独的模块中。SPA 有自己的开发、构建和发布工具,因此拥有专用文件夹更简洁、更易于维护。然后,您可以配置 Spring 引导以在构建时处理 SPA 的静态资源,并将它们包含在最终版本中。

另一种选择是让专门的服务负责为 Angular 静态资源提供服务。这就是我们将用于Polar Bookshop的策略。我已经将 Angular 应用程序打包在 NGINX 容器中。NGINX()提供了HTTP服务器功能,对于提供静态资源(如组成Angular应用程序的HTML,CSS和JavaScript文件)非常方便。

让我们继续在Docker中运行Polar Bookshop前端(polar-ui)。首先,转到你的极化部署存储库,然后打开你的 Docker Compose 文件 (docker/docker-compose.yml)。然后添加配置以运行 polar-ui 并通过端口 9004 公开它。

11.12 将 Angular 应用程序作为容器运行

version: "3.8"services:  ...   polar-ui:    image: "ghcr.io/polarbookshop/polar-ui:v1"   ❶    container_name: "polar-ui"    ports:      - 9004:9004                                ❷    environment:      - PORT=9004                                ❸

我为打包 Angular 应用程序而构建的容器映像

NGINX将在端口9004上为SPA提供服务。

配置 NGINX 服务器端口

与 Polar Bookshop 系统中的其他应用程序一样,我们不希望直接从外部访问 Angular 应用程序。相反,我们希望通过边缘服务提供的网关访问它。为此,我们可以为 Spring Cloud Gateway 添加新路由,将任何静态资源请求转发到 Polar UI 应用程序。

转到边缘服务项目(边缘服务),打开 application.yml 文件,然后按如下所示配置新路由。

清单 11.13 为 SPA 静态资源配置新的网关路由

spring:  gateway:    routes:      - id: spa-route                             ❶        uri: ${SPA_URL:}     ❷        predicates:                               ❸          - Path=/,/*.css,/*.js,/favicon.ico 

路线编号

URI 值来自环境变量,或者来自指定的默认值。

谓词是与根终结点和 SPA 静态资源匹配的路径列表。

Polar UI 应用程序的 URI 是使用环境变量 (SPA_URL) 中的值计算的。如果未定义,将使用在第一个冒号 (:) 符号之后写入的默认值。

注意将 Edge 服务作为容器运行时,请记住配置 SPA_URL 环境变量。在 Docker 上,可以使用容器名称和端口作为值,从而产生 。

让我们测试一下。首先,将 Polar UI 容器与 Redis 和 Keycloak 一起运行。打开终端窗口,导航到保存 Docker Compose 文件的文件夹 (polar-deployment/docker/docker-compose.yml),然后运行以下命令:

$ docker-compose up -d polar-ui polar-redis polar-keycloak

然后再次构建边缘服务项目,并运行应用程序 (./gradlew bootRun)。最后,打开隐身浏览器窗口并导航到 。

Spring 安全性配置为保护所有端点和资源,因此您将被自动重定向到 Keycloak 登录页面。以 Isabelle 或 Bjorn 身份进行身份验证后,系统会将你重定向回为 Angular 前端提供服务的边缘服务根终结点。

目前,您无能为力。当 Spring Security 收到未经身份验证的请求时,它会触发身份验证流程,但如果由于 CORS 问题,它是 AJAX 请求,则身份验证流将不起作用。此外,由于 Spring 安全性启用的 CSRF 保护,POST 请求(包括注销操作)将失败。在以下部分中,我将向您展示如何更新 Spring 安全性配置以克服这些问题。

在继续之前,请使用 Ctrl-C 停止应用程序(但保持容器运行 - 您将需要它们)。

11.5.2 控制身份验证流程

在上一节中,您尝试访问边缘服务主页,并遇到自动重定向到 Keycloak 以提供用户名和密码的情况。当前端由服务器呈现的页面组成时(例如使用 Thymeleaf 时),该行为工作正常,并且很方便,因为它不需要任何额外的配置。如果您尚未通过身份验证,或者您的会话已过期,Spring 安全性将自动触发身份验证流程并将您的浏览器重定向到 Keycloak。

使用单页应用程序,工作方式略有不同。当通过浏览器执行的标准 HTTP GET 请求访问根端点时,后端会返回 Angular 应用程序。完成第一步后,SPA 通过 AJAX 请求与后端交互。当 SPA 向受保护的端点发送未经身份验证的 AJAX 请求时,您不希望 Spring Security 回复重定向到 Keycloak 的 HTTP 302 响应。相反,您希望它返回错误状态(如 HTTP 401 未经授权)的响应。

不对 SPA 使用重定向的主要原因是,您会遇到跨源请求共享 (CORS) 问题。考虑从 提供 SPA 并在 时通过 AJAX 对后端进行 HTTP 调用的情况。通信被阻止,因为两个 URL 没有相同的来源(相同的协议、域和端口)。这是所有 Web 浏览器强制执行的标准同源策略。

CORS 是一种机制,允许服务器通过 AJAX 接受来自基于浏览器的客户端(如 SPA)的 HTTP 调用,即使两者具有不同的来源。在 Polar Bookshop 中,我们通过边缘服务(同源)中实现的网关为 Angular 前端提供服务。因此,这两个组件之间没有任何 CORS 问题。但是,假设 Spring Security 配置为通过重定向到 Keycloak(具有不同的来源)来回复未经身份验证的 AJAX 调用。在这种情况下,请求将被阻止,因为在 AJAX 请求期间不允许重定向到不同的源。

注意要了解有关Spring Security中CORS的更多信息,您可以查看Laurenśiu Spilcă(Manning,10)的Spring Security in Action第2020章,其中对该主题进行了非常详细的解释。有关CORS的全面解释,请参阅Monsur Hossain的CORS在行动(Manning,2014)。

将 Spring 安全性配置更改为对未经身份验证的请求进行 HTTP 401 响应时,由 SPA 来处理错误并调用后端以启动身份验证流。重定向只是 AJAX 请求期间的问题。这里的关键部分是调用后端以启动用户身份验证不是 Angular 发送的 AJAX 请求。相反,它是从浏览器发送的标准 HTTP 调用,如下所示:

login(): void {  window.open('/oauth2/authorization/keycloak', '_self');}

我想强调的是,登录调用不是从 Angular HttpClient 发送的 AJAX 请求。相反,它会指示浏览器调用登录 URL。Spring Security 公开了一个 /oauth2/authorization/{registrationId} 端点,您可以使用它来启动基于 OAuth2/OIDC 的身份验证流。由于边缘服务的客户端注册标识符是密钥,因此登录终结点将为 /oauth2/authorization/ keycloak。

为了实现这一点,我们需要定义一个自定义的 AuthenticationEntryPoint,以指示 Spring Security 在收到对受保护资源的未经身份验证的请求时回复 HTTP 401 状态。该框架已经提供了一个完全适合此方案的 HttpStatusServerEntryPoint 实现,因为它允许您指定在需要用户进行身份验证时要返回的 HTTP 状态。

11.14 在用户未通过身份验证时返回 401

@EnableWebFluxSecuritypublic class SecurityConfig {  ...   @Bean  SecurityWebFilterChain springSecurityFilterChain(    ServerHttpSecurity http,    ReactiveClientRegistrationRepository clientRegistrationRepository  ) {    return http      .authorizeExchange(exchange -> exchange.anyExchange().authenticated())      .exceptionHandling(exceptionHandling ->         exceptionHandling.authenticationEntryPoint(                 ❶          new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)))       .oauth2Login(Customizer.withDefaults())      .logout(logout -> logout.logoutSuccessHandler(      oidcLogoutSuccessHandler(clientRegistrationRepository)))      .build();  }}

当由于用户未通过身份验证而引发异常时,它会使用 HTTP 401 响应进行回复。

此时,Angular 应用程序可以显式拦截 HTTP 401 响应并触发身份验证流。但是,由于 SPA 现在负责启动流,因此我们需要允许对其静态资源的未经身份验证的访问。我们还希望在不进行身份验证的情况下检索目录中的书籍,因此让我们也允许对 /books/** 端点发出 GET 请求。继续并更新 SecurityConfig 类中的 SecurityWebFilterChain Bean,如下所示。

11.15 允许对 SPA 和书籍进行未经身份验证的 GET 请求

@EnableWebFluxSecuritypublic class SecurityConfig {  ...   @Bean  SecurityWebFilterChain springSecurityFilterChain(    ServerHttpSecurity http,    ReactiveClientRegistrationRepository clientRegistrationRepository  ) {    return http      .authorizeExchange(exchange -> exchange        .pathMatchers("/", "/*.css", "/*.js", "/favicon.ico")          .permitAll()                                         ❶        .pathMatchers(HttpMethod.GET, "/books/**")          .permitAll()                                         ❷        .anyExchange().authenticated()                         ❸      )      .exceptionHandling(exceptionHandling -> exceptionHandling        .authenticationEntryPoint(        new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)))      .oauth2Login(Customizer.withDefaults())      .logout(logout -> logout.logoutSuccessHandler(        oidcLogoutSuccessHandler(clientRegistrationRepository)))      .build();  }}

允许未经身份验证地访问 SPA 静态资源

允许对目录中的书籍进行未经身份验证的读取访问

任何其他请求都需要用户身份验证。

现在让我们测试一下边缘服务的工作原理。确保 Polar UI、Redis 和 Keycloak 容器仍在运行。接下来,构建并运行边缘服务应用程序 (./gradlew bootRun),然后从隐身浏览器窗口转到 。首先要注意的是,您不会被重定向到登录页面,而是立即看到 Angular 前端应用程序。您可以通过单击右上角菜单中的“登录”按钮来启动身份验证流程。

登录后,右上角的菜单将包含一个注销按钮,该按钮仅在当前用户通过身份验证成功时显示。单击按钮注销。它应该触发注销流程,但由于 CSRF 问题,它不起作用。您将在下一节中了解如何解决此问题。同时,使用 Ctrl-C 停止应用程序。

11.5.3 防止跨站请求伪造

前端和后端之间的交互基于会话 Cookie。用户使用 OIDC/OAuth2 策略成功进行身份验证后,Spring 将生成一个会话标识符以匹配经过身份验证的上下文,并将其作为 cookie 发送到浏览器。对后端的任何后续请求都必须包含会话 cookie,Spring Security 可以从中检索与特定用户关联的令牌并验证请求。

但是,会话 Cookie 不足以验证请求,这些请求容易受到跨站点请求伪造 (CSRF) 攻击。CSRF 会影响修改 HTTP 请求,如 POST、PUT 和 DELETE。攻击者可以通过伪造旨在造成伤害的请求来诱使用户执行他们不想要的请求。伪造的请求可能会执行诸如从您的银行帐户转账或破坏关键数据之类的操作。

警告许多在线教程和指南展示了如何在配置 Spring 安全性时首先禁用 CSRF 保护。如果不解释原因或考虑后果,这是危险的。我建议保持启用保护,除非有充分的理由不这样做(您将在第 12 章中看到一个很好的理由)。作为一般准则,应保护面向浏览器的应用程序(如边缘服务)免受 CSRF 攻击。

幸运的是,Spring Security 具有针对此类攻击的内置保护。保护基于框架生成的所谓 CSRF 令牌,该令牌在会话开始时提供给客户端,并且需要与任何状态更改请求一起发送。

注意要了解有关Spring Security中的CSRF保护的更多信息,您可以查看Laurenśiu Spilcă(Manning,10)的Spring Security in Action第2020章,其中详细介绍了该主题。

在上一节中,您尝试注销,但请求失败。由于注销操作可以通过对 /logout 端点的 POST 请求获得,因此应用程序希望接收 Spring Security 为该用户会话生成的 CSRF 令牌。默认情况下,生成的 CSRF 令牌作为 HTTP 标头发送到浏览器。但是,Angular 应用程序无法使用它,并期望以 cookie 的形式接收令牌值。Spring 安全性支持此特定要求,但默认情况下不启用。

你可以指示 Spring Security 通过 ServerHttpSecurity 和 CookieServerCsrfTokenRepository 类公开的 csrf() DSL 提供 CSRF 令牌作为 cookie。对于命令式应用程序,这就足够了。但是,对于像边缘服务这样的反应式应用程序,您需要采取额外的步骤来确保实际提供 CsrfToken 值。

在第 8 章中,您了解到需要订阅反应式流才能激活它们。目前,CookieServerCsrfTokenRepository 不能确保订阅 CsrfToken,因此您必须在 WebFilter Bean 中明确提供解决方法。此问题应在 Spring 安全性的未来版本中解决(请参阅 GitHub 上的问题 5766:)。现在,按如下所示更新 SecurityConfig 类。

11.16 配置 CSRF 以支持基于 cookie 的 SPA 策略

@EnableWebFluxSecuritypublic class SecurityConfig {  ...   @Bean  SecurityWebFilterChain springSecurityFilterChain(    ServerHttpSecurity http,    ReactiveClientRegistrationRepository clientRegistrationRepository  ) {    return http      ...      .csrf(csrf -> csrf.csrfTokenRepository(                   ❶        CookieServerCsrfTokenRepository.withHttpOnlyFalse()))       .build();  }   @Bean   WebFilter csrfWebFilter() {                                   ❷    return (exchange, chain) -> {       exchange.getResponse().beforeCommit(() -> Mono.defer(() -> {         Mono<CsrfToken> csrfToken =           exchange.getAttribute(CsrfToken.class.getName());         return csrfToken != null ? csrfToken.then() : Mono.empty();       }));       return chain.filter(exchange);     };   } }

使用基于 cookie 的策略与 Angular 前端交换 CSRF 代币

一个过滤器,其唯一目的是订阅 CsrfToken 反应式流并确保正确提取其值

让我们验证注销流程现在是否正常工作。确保 Polar UI、Redis 和 Keycloak 容器仍处于启动和运行状态。接下来,构建并运行应用程序 (./gradlew bootRun),然后从隐身浏览器窗口转到 。通过单击右上角菜单中的“登录”按钮启动身份验证流程。然后单击注销按钮。在后台,Spring Security现在将接受您的注销请求(Angular将cookie中的CSRF令牌值添加为HTTP标头),终止您的Web会话,将请求传播到Keycloak,最后将您重定向到主页,未经身份验证。

由于此更改,您还可以执行任何 POST、PUT 和 DELETE 请求,而不会收到 CSRF 错误。随意探索 Angular 应用程序。如果启动“目录服务”和“订购服务”,则可以尝试向目录添加新图书、修改图书或下订单。

Isabelle和Bjorn目前都可以执行任何操作,这不是我们想要的,因为不应该允许客户(如Bjorn)管理图书目录。下一章将介绍授权,你将了解如何使用不同的访问策略保护每个端点。但是,在解决授权问题之前,我们需要编写自动测试来涵盖新功能。这将在下一节中介绍。

在继续之前,使用 Ctrl-C 停止应用程序,并使用 docker-compose 关闭所有容器(从 polar-deployment/docker)。

11.6 测试 Spring 安全性和 OpenID 连接

编写自动测试的重要性对开发人员来说通常是显而易见的。尽管如此,在安全性方面,事情可能会变得具有挑战性,并且由于其复杂性,有时最终不会被自动化测试覆盖。幸运的是,Spring 安全性提供了几个实用程序,可帮助您以简单的方式将安全性包含在切片和集成测试中。

在本节中,您将学习如何使用WebTestClient对Spring Security的支持来测试OIDC身份验证和CSRF保护。让我们开始吧。

11.6.1 测试 OIDC 身份验证

在第8章中,我们依靠@SpringWebFlux注释和WebTestClient测试了Spring WebFlux公开的REST控制器。在本章中,我们添加了一个新的控制器(UserController),因此让我们使用不同的安全设置为它编写一些自动测试。

首先,打开边缘服务项目,在 src/test/java 中创建一个用 @WebFluxTest(UserController.class) 注释的 UserControllerTests 类,并自动连接 WebTestClient bean。到目前为止,设置类似于我们在第 8 章中使用的设置:Web 层的切片测试。但是我们需要一些额外的设置来涵盖安全方案,如以下列表所示。

11.17 定义一个类来测试用户控制器的安全策略

@WebFluxTest(UserController.class)@Import(SecurityConfig.class)        ❶class UserControllerTests {   @Autowired  WebTestClient webClient;   @MockBean                          ❷  ReactiveClientRegistrationRepository clientRegistrationRepository;}

导入应用程序的安全配置

一个模拟 bean,用于在检索有关客户端注册的信息时跳过与 Keycloak 的交互

由于我们将边缘服务配置为在请求未经身份验证时返回 HTTP 401 响应,因此让我们验证在调用 /user 终结点而不先进行身份验证时会发生这种情况:

@Testvoid whenNotAuthenticatedThen401() {  webClient    .get()    .uri("/user")    .exchange()    .expectStatus().isUnauthorized();}

为了测试用户被身份验证的场景,我们可以使用 mockOidcLogin(),一个由 SecurityMockServerConfigurers 提供的配置对象来模拟 OIDC 登录,合成一个 ID 令牌,并相应地改变 WebTestClient 中的请求上下文。

/user 端点通过 OidcUser 对象从 ID 令牌读取声明,因此我们需要使用用户名、名字和姓氏构建一个 ID 令牌(目前角色在控制器中硬编码)。以下代码演示如何执行此操作:

@Testvoid whenAuthenticatedThenReturnUser() {  var expectedUser = new User("jon.snow", "Jon", "Snow",    List.of("employee", "customer"));                            ❶   webClient    .mutateWith(configureMockOidcLogin(expectedUser))            ❷    .get()    .uri("/user")    .exchange()    .expectStatus().is2xxSuccessful()    .expectBody(User.class)                                      ❸    .value(user -> assertThat(user).isEqualTo(expectedUser));} private SecurityMockServerConfigurers.OidcLoginMutator➥ configureMockOidcLogin(User expectedUser) {  return SecurityMockServerConfigurers.mockOidcLogin().idToken(   builder -> {                                                  ❹      builder.claim(StandardClaimNames.PREFERRED_USERNAME,        expectedUser.username());      builder.claim(StandardClaimNames.GIVEN_NAME,        expectedUser.firstName());      builder.claim(StandardClaimNames.FAMILY_NAME,        expectedUser.lastName());    });}

预期的经过身份验证的用户

定义基于 OIDC 的身份验证上下文并使用预期的用户

期望用户对象与当前经过身份验证的用户具有相同的信息

构建模拟 ID 令牌

最后,按如下方式运行测试:

$ ./gradlew test --tests UserControllerTests

Spring Security 提供的测试实用程序涵盖了广泛的场景,并与 WebTestClient 很好地集成。在下一节中,您将了解如何使用类似的方法测试 CSRF 保护。

11.6.2 测试 CSRF

在 Spring Security 中,CSRF 保护默认适用于所有变异的 HTTP 请求(例如 POST、PUT 和 DELETE)。正如您在前面的部分中所看到的,Edge 服务接受发往 /logout 端点的 POST 请求以启动注销流,并且此类请求需要有效的 CSRF 令牌才能执行。此外,我们从 OIDC 配置了 RP 发起的注销功能,因此对 /logout 的 POST 请求实际上会导致 HTTP 302 响应,将浏览器重定向到 Keycloak 以将用户从那里注销。

创建一个新的SecurityConfigTests类,并使用您在上一节中学到的相同策略来设置具有安全支持的Spring WebFlux测试,如以下列表所示。

示例 11.18 定义一个用于测试认证流的类

@WebFluxTest@Import(SecurityConfig.class)   ❶class SecurityConfigTests {   @Autowired  WebTestClient webClient;   @MockBean                     ❷  ReactiveClientRegistrationRepository clientRegistrationRepository;}

导入应用程序安全配置

一个模拟 bean,用于在检索有关客户端注册的信息时跳过与 Keycloak 的交互

然后添加一个测试用例,以检查应用程序在使用正确的 OIDC 登录名和 CSRF 上下文向 /logout 发送 HTTP POST 请求后是否返回 HTTP 302 响应。

@Testvoid whenLogoutAuthenticatedAndWithCsrfTokenThen302() {  when(clientRegistrationRepository.findByRegistrationId("test"))    .thenReturn(Mono.just(testClientRegistration()));   webClient    .mutateWith(     SecurityMockServerConfigurers.mockOidcLogin())                 ❶    .mutateWith(SecurityMockServerConfigurers.csrf())               ❷    .post()    .uri("/logout")    .exchange()    .expectStatus().isFound();                                      ❸} private ClientRegistration testClientRegistration() {  return ClientRegistration.withRegistrationId("test")              ❹    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)    .clientId("test")    .authorizationUri(";)    .tokenUri(";)    .redirectUri(";)    .build();}

使用模拟 ID 令牌对用户进行身份验证

增强了提供所需 CSRF 代币的请求

响应是重定向到 Keycloak 以传播注销操作。

Spring Security用来获取URL联系Keycloak的模拟客户端注册

最后,按如下方式运行测试:

$ ./gradlew test --tests SecurityConfigTests

与往常一样,您可以在本书随附的源代码存储库中找到更多测试示例。在安全性方面,单元测试和集成测试对于确保应用程序的正确性至关重要,但还不够。这些测试涵盖默认安全配置,该配置在生产环境中可能有所不同。这就是为什么我们还需要在部署管道的验收阶段进行面向安全的自动测试(如第3章所述),以测试部署在类似生产环境中的应用程序。

极地实验室

到目前为止,用户应该直接访问的唯一应用程序是边缘服务。所有其他 Spring 引导应用程序都在部署它们的环境中相互交互。

同一 Docker 网络或 Kubernetes 集群中的服务到服务交互可以分别使用容器名称或服务名称进行配置。例如,边缘服务通过 Docker 上的 URL(<container-name>:<container-port>)和 Kubernetes 上的 URL(服务名称)将请求转发到 Polar UI。

Keycloak 与众不同,因为它涉及服务到服务的交互(目前,这些只是与边缘服务的交互),以及通过 Web 浏览器与最终用户的交互。在生产中,Keycloak将通过应用程序和用户都将使用的公共URL访问,因此不会有问题。在当地环境怎么样?

由于我们在本地工作时不处理公共 URL,因此我们需要以不同的方式进行配置。在 Docker 上,我们可以通过使用安装软件时自动配置的 特殊 URL 来解决这个问题。它解析为您的本地主机 IP 地址,可以在 Docker 网络内部和外部使用。

在 Kubernetes 上,我们没有一个通用的 URL 来让集群中的 Pod 访问你的本地主机。这意味着边缘服务将通过其服务名称()与Keycloak进行交互。当 Spring 安全性将用户重定向到 Keycloak 登录时,浏览器将返回错误,因为无法在集群外部解析 URL。为了实现这一点,我们可以更新本地 DNS 配置,将极坐标密钥主机名解析为群集 IP 地址。然后,专用入口可以在请求定向到极坐标键斗篷主机名时访问Keycloak。

如果你使用的是 Linux 或 macOS,你可以将 polar-keycloak 主机名映射到 /etc/hosts 文件中的 minikube 本地 IP 地址。在 Linux 上,IP 地址是 minikube ip --profile polar 命令返回的地址(如第 9 章所述)。在macOS上,它将是127.0.0.1。打开“终端”窗口,然后运行以下命令(确保将 <ip-address> 占位符替换为群集 IP 地址,具体取决于您的操作系统):

$ echo "<ip-address> polar-keycloak" | sudo tee -a /etc/hosts

在 Windows 上,您必须将极坐标键斗篷主机名映射到主机文件中的 127.0.0.1。以管理员身份打开 PowerShell 窗口,然后运行以下命令:

$ Add-Content C:\Windows\System32\drivers\etc\hosts "127.0.0.1 polar-keycloak"

我已经更新了用于部署 Polar Bookshop 所有支持服务的脚本,包括 Keycloak 和 Polar UI。您可以从本书随附的代码存储库中的 /Chapter11/11-end/polar-deployment/kubernetes/platform/development 文件夹中获取它们 (),并将它们复制到 polar-deployment 存储库中的相同路径中。该部署还包括为 Keycloak 配置专用入口,接受定向到 polar-keycloak 主机名的请求。

此时你可以运行 ./create-cluster.sh 脚本(polar-deployment/kubernetes/platform/development)来启动一个 minikube 集群,并为 Polar Bookshop 部署所有后备服务。如果你使用的是Linux,你将能够直接访问Keycloak。如果您使用的是macOS或Windows,请记住先运行minikube隧道--profile polar命令。无论哪种方式,您都可以打开浏览器窗口并在 polar-keycloak/ 访问 Keycloak/(包括最后一个斜杠)。

最后,在更新边缘服务的部署脚本以配置 Polar UI 和 Keycloak 的 URL 后,尝试在 Kubernetes 上运行整个系统。您可以参考本书随附的代码库中的第 11/11 章结束文件夹来检查最终结果 ()。

下一章将扩展安全主题。它将介绍如何将身份验证上下文从边缘服务传播到下游应用程序,以及如何配置授权。

总结访问控制系统需要识别(你是谁?),身份验证(你能证明它真的是你吗?)和授权(你可以做什么?)。在云原生应用程序中实现身份验证和授权的常见策略是基于 JWT 作为数据格式,OAuth2 作为授权框架,OpenID Connect 作为身份验证协议。使用 OIDC 身份验证时,客户端应用程序会启动流并委派授权服务器进行实际身份验证。然后,授权服务器向客户端颁发 ID 令牌。ID 令牌包含有关用户身份验证的信息。Keycloak是一种身份和访问管理解决方案,支持OAuth2和OpenID Connect,可用作授权服务器。Spring Security 提供对 OAuth2 和 OpenID Connect 的本机支持,您可以使用它将 Spring 引导应用程序转换为 OAuth2 客户端。在 Spring 安全性中,您可以在 SecurityWebFilterChain Bean 中配置身份验证和授权。要启用 OIDC 身份验证流程,您可以使用 oauth2Login() DSL。默认情况下,Spring 安全性会公开一个 /logout 端点,用于注销用户。在 OIDC/OAuth2 上下文中,我们还需要将注销请求传播到授权服务器(例如 Keycloak)以将用户从那里注销。我们可以通过 Spring Security 通过 OidcClientInitiatedServerLogoutSuccessHandler 类支持的 RP 启动的注销流程来做到这一点。当安全的 Spring 引导应用程序是 SPA 的后端时,我们需要通过 cookie 配置 CSRF 保护,并实现一个身份验证入口点,该入口点在请求未经过身份验证时返回 HTTP 401 响应(而不是默认的 HTTP 302 响应自动重定向到授权服务器)。Spring 安全测试依赖项提供了几个方便的实用程序来测试安全性。WebTestClient Bean 可以通过 OIDC 登录和 CSRF 保护的特定配置来改变其请求上下文来增强。

标签: #angular获取cookie