使用 OAuth 2.0 保护 REST API
使用 OAuth 2.0 保护 REST API
- 使用 OAuth 2.0 保护 REST API
- 1.1 不安全的api
- 1.2 安全默认值
- 安全默认值
- Spring Security 默认值
- 需要对所有请求进行身份验证
- Servlet、过滤器和调度程序
- 安全优势
- 使用所有请求的安全标头进行响应
- 缓存标头
- 严格传输安全标头
- 内容类型选项
- 需要对所有具有副作用的请求进行 CSRF 缓解
- 允许使用默认用户进行 HTTP 基本身份验证
- 对安全故障做出 RESTfully 响应
- 使用应用程序防火墙防止恶意请求
- 测试例子
- 2. 认证
- 2.1 添加身份验证
- 内容协商
- 认证流程
- 鉴权结果
- 后续请求
- 2.2 HTTP Basic 的限制
- 长期证书
- 授权绕过
- 敏感数据泄露
- 2.3 OAuth 2.0 和 JWT
- OAuth 2.0 版本
- JSON Web 令牌
- 注意说明:有状态 JWT
- Spring Security 支持
- 测试例子
- 3. 授权
- 3.1 添加请求授权
- 用`authorizeHttpRequests`
- 避免常见的失误
- 使用 Coarse-Grained 授权
- 静态资源
- RESTful 资源
- Catch-all 规则
- 使用场景
- `.authenticated()`
- `.denyAll()`
- 3.2 添加方法授权
- 启用方法安全性
- 保护方法
- 保护服务
- 防止不安全的直接对象引用
- 我们不能查询数据库吗?
- 3.3 添加数据授权
- 启用 Spring Data 授权
- 防止用户数据泄露
- 方法签名的清晰度
- 3.4 添加委托授权
- 使用授权服务器
- 但是,我没有授权服务器
- 测试例子
使用 OAuth 2.0 保护 REST API
原文地址:Lab: The Unsecured API - Spring Academy
1.1 不安全的api
启动boot
[~/exercises] $ ./gradlew bootRun
...
<==========---> 80% EXECUTING [1m 15s]
> IDLE
> IDLE
> IDLE
> IDLE
> :bootRun
访问boot
[~/exercises] $ http :8080/cashcards
注意:上面的命令使用的是HTTPie。它相当于旋度,但比旋度更棒。
忽视API端点的安全至少有三个后果:
•内容是公开的——你无法控制谁能看到这些信息
•内容是匿名的——你不知道是谁在问
•内容不受保护-不良行为者可以利用基于浏览器的漏洞
1.2 安全默认值
安全默认值
在本节中,您将了解 Spring Security 的一些默认值,以及这与您将默认安全原则和最小权限原则应用于应用程序有何关系。
默认安全是一项原则,鼓励您确保默认设置是安全的。这样,如果应用程序发现自己在生产环境中使用默认值,它就不是安全漏洞。
最小权限原则是一项原则,它鼓励您将每条数据视为一种拥有的特权,并赋予最终用户尽可能低的特权,使他们能够很好地完成工作。
Spring Security 采用这两种理念来自动保护 REST API。
Spring Security 默认值
当 Spring Security 位于 Classpath 上时, Spring Boot 会为 REST API 使用以下默认值配置您的应用程序:
- 需要对所有请求进行身份验证
- 使用所有请求的安全标头进行响应
- 需要对所有具有副作用的请求进行 CSRF 缓解
- 允许使用默认用户进行 HTTP 基本身份验证
- 对安全故障做出 RESTfully 响应
- 使用应用程序防火墙防止恶意请求
让我们逐一查看这些原则,并将它们与 Secure by Default 和 Principle of Least Privilege 原则联系起来。
需要对所有请求进行身份验证
无论终端节点是您生成的还是 Boot 生成的,所有 dispatch 中的所有请求都需要身份验证。
无论端点的性质如何, Spring Security 都会应用一个 Servlet 过滤器,该过滤器会检查每个请求,如果请求未经身份验证,则拒绝它。
这是 Spring Security 的安全默认值之一。
Servlet、过滤器和调度程序
为了更好地理解这一点,我们需要介绍少量的 Servlet API 术语。
Java Servlet API 是 Java 中的模块,用于在应用程序内部处理 HTTP 请求。使用 servlet 术语,给定的 HTTP 请求可以通过多个分派。每个 dispatch 在到达单个 servlet 的途中都可以被多个过滤器拦截。
Servlet 处理 HTTP 请求并生成 HTTP 响应。您可以将 servlet 想象成一个 “迷你服务器”。
过滤器拦截 HTTP 请求以处理横切关注点。通常,过滤器会以某种方式丰富请求,或者拒绝请求,从而阻止其到达 Servlet。
分派表示 HTTP 请求通过一组过滤器及其目标 Servlet 进行的单次传递。通常,HTTP 请求首先通过 REQUEST 分派,但随后也可以通过 ERROR 分派、FORWARD 分派等传递。
用 Spring 术语来说,Spring MVC 构成了一个 servlet,Spring Security 构成了一组过滤器,而 Spring Boot 附带了一个嵌入式容器,该容器执行为单个请求提供服务所需的各种调度。
所有这些都意味着 Spring Security 默认值要求对每个 dispatch 进行身份验证。
安全优势
这种安排的好处是,谁创建了终端节点并不重要。如果是您、Boot 或第三方,则 Spring Security 的 servlet 过滤器会在任何 servlet(“迷你服务器”)可以处理请求之前拦截请求。
这意味着,当你包含 Spring Security 时,即使不存在的端点也会返回 HTTP 响应状态代码,而不是 - 不存在的端点的默认 Spring Boot 响应。之所以采取这种严格的政策,是因为最低权限原则。此原则表示,您应该只提供最终用户有权知道的信息。
401 Unauthorized
404 Not Found
那么有什么大不了的呢?不存在的端点有什么特权?
出于安全目的,即使哪些 URI 有效也是特权信息。您可以想象一下,如果有人请求 index.jsp 或 /admin。如果 Spring Security 在这些情况下返回 a 而不是 a ,则意味着向不良行为者暗示存在给定的端点!不良行为者可以使用此提示来枚举 REST API 的有效 URI,找出底层易受攻击的技术,并加速其攻击。401
好的,既然每个请求都需要身份验证,您可能想知道…我应该如何命令我的 API?如果他们需要身份验证,难道不应该有用户名和密码或其他东西吗?
坚持。我们很快就会到达那里。不过,首先,让我们谈谈 Spring Security 实施的一些防御措施。
使用所有请求的安全标头进行响应
HTTP 标头允许客户端和服务器在 HTTP 请求和响应中相互交换其他信息。无论请求是否经过身份验证, Spring Security 默认情况下始终使用某些 Headers 进行响应。每个标头默认为可用的最安全值。
缓存标头
第一个是缓存控制标头。一类基于浏览器的漏洞是 HTTP 响应缓存在浏览器中。例如,假设您的 REST API 返回了以下内容:
[{"amount": 123.45,"id": 99,"owner": "sarah1"},{"amount": 1.0,"id": 100,"owner": "sarah1"}
]
然后,该响应可以缓存在浏览器中,以便以后由用户本地计算机上的不良行为者检索。或者,更实际地说,即使最终用户自己失去了访问权限或从客户端应用程序撤销了访问权限,浏览器仍可能能够从其缓存中检索该敏感数据。
Spring Security 对 Cache-Control 和其他 Headers 应用安全设置以缓解此类漏洞。
严格传输安全标头
第二个是 Strict Transport Security 标头。此标头强制浏览器在指定时间段内将请求升级到 HTTPS。
***注意:***由于这是针对 HTTPS 请求的,因此默认情况下不会为 HTTP 请求写入。鉴于此,您可能在通过 HTTP 进行的本地测试中看不到它。
HTTPS 长期以来已被证明是安全部署的关键组件。中间人攻击使查看和修改最终用户和 REST API 之间传递的数据成为可能。
HTTPS 可以缓解此类攻击,并且 Strict Transport Security 标头告诉浏览器不要通过 HTTP 向此 REST API 发送任何请求。相反,浏览器应自动将任何 HTTP 请求升级到 HTTPS。
内容类型选项
我们此时要讨论的第三个也是最后一个 header 是 header。此标头告诉浏览器不要尝试猜测响应的内容类型。
X-Content-Type-Options
不良行为者隐藏的常见位置是 HTTP 协议模糊,应用程序试图理解、消除歧义和猜测请求或响应的意图。例如,浏览器可能会查看以 开头的响应,并合理地猜测内容类型是 – 即网页。有时这种猜测是不安全的。例如,图像可能包含脚本内容,并且浏览器可能会被欺骗猜测并作为 JavaScript 执行。太疯狂了,对吧?
<html>
text/html
steal-my-password.jpg
Spring Security 通过默认发出安全设置来解决此问题。
X-Content-Type-Options
需要对所有具有副作用的请求进行 CSRF 缓解
REST API 面临风险的另一个地方是第三方网站能够在未经用户同意的情况下向它们发出请求。
这是可能的,因为默认情况下,浏览器会自动将所有 cookie 和 HTTP Basic 身份验证详细信息发送到任何非 XHR 端点。
例如,请看一下这个所谓的 image 请求:
<img src="https://mybank.example.org/account/32?transfer=25&toAccount=45" />
哎呀!此请求将由浏览器执行。这样做是因为在响应返回之前,浏览器无法知道 URL 是否指向图像。到那时,损害已经造成。
可以想象,浏览器甚至在第三方网站上发出此请求。默认情况下,浏览器也会默认向其发送所有 的 Cookie 和 HTTP Basic 凭据。这意味着,如果您的用户已登录,第三方应用程序可以命令您的 REST API,而无需进一步保护。
mybank.example.org
因此, Spring Security 会自动保护这些具有副作用的端点,例如 POST、PUT 和 DELETE。它通过向客户端发送一个特殊令牌来实现此目的,该令牌应该在后续请求中使用。令牌的传输方式使第三方无法看到它。因此,当它被返回时, Spring Security 认为它是合法的来自 Client 端。
允许使用默认用户进行 HTTP 基本身份验证
你一直在想这个问题,不是吗?
Spring Security 会生成一个名为 user的默认用户。不过,它的密码是在每次启动时生成的。
这样做的原因是,如果您不小心将 Spring Security 默认值部署到 production,则没有人可以使用默认用户名和密码来命令您的应用程序。这是创建默认安全的应用程序的另一个经典实例,换句话说,创建默认设置为安全的应用程序。
要找出密码,您可以查看此字符串的 Boot 启动日志:
Using generated security password: fc7e0357-7d82-4a9c-bae7-798887f7d3b3
该字符串中的 UUID 是密码。每次应用程序启动时,情况都会有所不同。
如前所述,默认情况下,Spring Security 将使用 HTTP Basic 身份验证标准接受此用户名和密码,您将有机会稍后练习。
对安全故障做出 RESTfully 响应
当请求中的凭据错误或缺失时,Spring Security 会使用状态代码进行响应。默认情况下,它还将发送相应的标头以指示预期的身份验证类型。a 的隐含含义是请求未经身份验证。
401 Unauthorized
401
当凭证良好但请求未获得授权时,它会使用状态代码进行响应,例如当最终用户尝试执行仅限管理员的请求时。a 的隐含含义是请求未经授权。
403 Forbidden
403
使用应用程序防火墙防止恶意请求
不良行为者可能会通过多种其他方式尝试和滥用您的 REST API。对于其中许多请求,最佳做法是直接拒绝请求。
Spring Security 通过添加应用程序防火墙来帮助您解决这个问题,默认情况下,该应用程序防火墙拒绝包含双重编码和几个不安全字符(如回车符和换行符)的请求。使用 Spring Security 的防火墙有助于缓解所有类别的漏洞。
测试例子
build.gradle如果加了
implementation 'org.springframework.boot:spring-boot-starter-security'
测试会失败,加上
testImplementation ‘org.springframework.security:spring-security-test’
,然后在测试类加上@WithMockUser,会默认给加上user用户,测试成功
Spring Security不对没有CSRF令牌的post进行授权。
你可以使用Spring Security的MockMvc RequestPostProcessors之一(即csrf())在MockMvc声明中添加这一点。
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
...
void shouldCreateANewCashCard() throws Exception {String location = this.mvc.perform(post("/cashcards").with(csrf())...
重写启动项目
[~/exercises] $ ./gradlew bootRun> Task :bootRun
...
Using generated security password: 9b88ab54-c3e6-4634-905e-ac1cfd11ecde
...
请求
# 发现无法获取数据返回401无权限
http :8080/cashcards
# 发现返回数据
http -a user:9b88ab54-c3e6-4634-905e-ac1cfd11ecde :8080/cashcards
# 发现返回404无页面
http -a user:9b88ab54-c3e6-4634-905e-ac1cfd11ecde :8080/non-existent-endpoint
post请求需要更高的级别
[~/exercises] $ http -a user:<password-here> :8080/cashcards "Accept: application/json" amount=1 owner=sarah1
返回
HTTP/1.1 401
...
测试应用程序防火墙防止恶意请求
[~/exercises] $ http :8080/admin
HTTP/1.1 401
测试
[~/exercises] $ http :8080/admin%2Faction
HTTP/1.1 400
...
在这里,恶意行为者将编码的“斜杠”或/作为%2F添加到URL中,从而试图访问/admin/action。恶意行为者的猜测是,如果请求是/admin/action,那么Spring Security可能会将其与模式/admin/**进行比较,并让它通过。
2. 认证
2.1 添加身份验证
在本模块中,您将了解 Spring Security 作为身份验证请求的框架。这意味着确认:
- 调用者的身份 - 发出请求的人,有时称为客户端或代理
- 委托人的身份 - 请求是关于谁的,通常是最终用户
- 请求的完整性 - 证明请求未被中介修改
确保其中每一个都可能非常复杂,这就是为什么转向安全协议和框架来支持具有战略重要性的原因。
***注意:***Principal 是一个通用术语,表示发出请求的“谁”。在本课程中,“谁”是最终用户。我们使用 principal 的原因是,有时 “who” 不是一个人,而是另一台机器。
内容协商
Spring Security 的默认设置使用 Form Login 和 HTTP Basic 身份验证方案确认主体的身份。它使用内容协商在两者之间进行选择。
例如,如果浏览器导航到 API 中未经身份验证的端点,则默认情况下, Spring Security 会重定向到其原始登录页面,如下所示:
另一方面,如果有人发出未经身份验证的 REST 请求,如下所示:
[~/exercises] $ http :8080
然后,Spring Security 使用 Headers 来指示它所需的身份验证方案:WWW-Authenticate
WWW-Authenticate: Basic
相反,如果 REST 请求提供遵循 HTTP Basic 方案的标头:Authorization
[~/exercises] $ http :8080 "Authorization: Basic dXNlcjpwYXNzd29yZA=="
***注意:***如果您将参数传递给 HTTPie,则 HTTPie 的命令将为您添加标头。在http上面,我们手动添加了Authorization: Basic -a标题以用于教育目的。
Spring Security 看到 Basic 方案并执行其 HTTP Basic 身份验证支持。
让我们更详细地探讨这个过程。
认证流程
为了进一步分解这一点,您可以将 Spring Security 的身份验证支持分为三个部分,而不管它使用哪种身份验证方案:
- 它将请求材料解析为凭证。
- 它测试该凭证。
- 如果凭证通过,它会将该凭证转换为 Principal 和 Authorities。
在上面的例子中,你可以看到这三个步骤的实际效果:
- Spring Security 解码 Base64 编码的用户名和密码。在本例中,密码是凭据。
- 它针对用户存储测试此用户名和密码。具体来说,对于密码,它会对密码进行哈希处理,并将其与用户的密码哈希值进行比较。
- 如果密码匹配,它将加载相应的用户和权限,并将其存储在其安全上下文中。生成的 user 是我们前面提到的 Principal。
在 Spring Security 中,所有身份验证方案都遵循这种通用方法,我们很快就会看到另一个示例。
鉴权结果
在 Spring Security 术语中,结果是一个Authentication实例。作为一个类,它按以下方式建模:
认证
- 委托人 (“who”)
- 凭据(“证明”)
- 权限(“权限”)
您的应用程序可以通过各种方式检索此Authentication实例,我们稍后会看到。
后续请求
一些身份验证方案是有状态的,而另一些则是无状态的。Stateful 表示您的应用程序会记住有关先前请求的信息。Stateless 意味着您的应用程序不会记住任何以前的请求。
两种默认身份验证方案是每种方案的一个很好的示例。表单登录是有状态身份验证方案的一个示例。它将已登录的用户存储在会话中。只要在后续请求中返回会话的标识符,最终用户就不需要再次提供凭证。对于许多网站,这就是为什么您不需要在网站上访问的每个新页面都登录的原因(
HTTP Basic 是无状态身份验证方案的一个示例。由于它不会记住之前请求的任何内容,因此您需要在每个请求中为其提供用户名和密码。
***注意:***请记住,Spring Security 默认激活 HTTP Basic 和 Form Login 身份验证方案。您可以直接使用自定义实例指定它们和其他实例,您将在未来的模块中了解更多信息。不过,现在,我们将依靠 Spring Boot 的自动配置来为我们做出正确的决定。
2.2 HTTP Basic 的限制
Spring Security 默认激活 HTTP Basic,因为它是一种简单的入门方法。不过,它有其局限性。
概括地说,这些限制是:
- 长期证书
- Authorization Bypass 和
- 敏感数据泄露
在讨论它们之前,请考虑在客户端和 REST API 之间使用 HTTP Basic 的应用程序的常见安排:
在此图中,有一个客户端应用程序,它使用您的用户名和密码代表您与 REST API 通信。这方面的一个例子是第三方预算应用程序,它想要调用我们虚构的现金卡 REST API 并导入交易。
长期证书
要了解第一个限制,请考虑以下事项:您上次更改最不常用在线帐户的密码是什么时候?在许多情况下,答案是几年左右的!
如果 REST API 将您的用户名和密码作为凭据,则意味着只要您的密码有效,任何获取您的用户名和密码的人都可以冒充您。这包括不良行为者以及第三方应用程序。
即使您可以每周更改所有系统中的所有密码,您中的哪些帐户可以向不良行为者授予一整周的访问权限?(提示!答案是没有!
鉴于此,HTTP Basic 的主要限制是它使用需要最终用户更改它的长期凭证。
授权绕过
另一个限制是,当您将 REST API 用户名和密码提供给第三方客户端时,该应用程序现在拥有您的用户名和密码。
虽然这看起来很明显,但它确实意味着您现在需要问:您是否相信该应用程序只会将您的用户名和密码用于您希望他们使用的目的?此外,您可以相信该应用程序不会受到您不信任的不良行为者的损害。
例如,想象一下我们虚构的第三方预算应用程序。您可能希望它从 REST API 读取现金卡信息是有道理的。但是你也想让它添加和删除现金卡吗?也许不是!
如果有一种方法可以拥有一个凭证,除了是短期的,还可以指示您授权客户对您的数据执行哪些作,那就太好了。
敏感数据泄露
请记住,HTTP Basic 是无状态的。每当第三方客户端应用程序调用 REST API 时,它都需要在您每次发出 HTTP 请求时交出您的用户名和密码。
此外,这意味着客户端应用程序需要将您的用户名和密码以纯文本形式保存在某处,以便它可以将它们重复传递给 REST API。
这意味着被拦截的单个 HTTP 请求或客户端应用程序的单个内存转储可能会泄露您的密码!
因此,除了拥有足够智能的短期凭证来授权特定作外,您的密码不应被任何第三方在任何地方持有(明文或其他方式)。在将所有这些在 Spring Security 中付诸实践之前,让我们再来看一课来描述这个问题的解决方案。
2.3 OAuth 2.0 和 JWT
在本课中,您将了解使用 JWT 的 OAuth 2.0 不记名身份验证,以及它如何解决您在上一课中发现的 HTTP Basic 限制。
OAuth 2.0 版本
回想一下,一个主要的安全问题是密码是长期高度敏感的凭据。最好拥有仅持续几分钟的凭证。但是,对于我们人类来说,如此频繁地更改密码并仍然完成任何事情是不现实的!
OAuth 2.0 是一种行业标准的授权协议,已被数千家公司采用,并被数百万个应用程序使用,它提供了一个实现此目的的框架。
简而言之,OAuth 2.0 描述了三个参与者:
- 客户端应用程序,它想要访问您的数据并为您提供服务 - 通常是 Web、移动或桌面应用程序。
- 资源服务器,用于保存和保护您的数据 - 通常是一个 REST API。
- 授权服务器,授权客户端访问您的数据。
这三个参与者以以下方式相互交互:
- 客户端应用程序向授权服务器请求命令资源服务器的权限。
- 授权服务器决定是否授予权限(稍后将详细介绍)。
- 如果授权服务器授予权限,它将创建或“铸造”一个访问令牌,该令牌将在短时间后过期。此访问令牌描述客户端被授予的权限
- 客户端向资源服务器发出请求,包括访问令牌。
- 资源服务器验证访问令牌是否具有正确的权限并做出相应的响应。
如您所见,客户端应用程序永远不会看到您的密码。
不过,资源服务器仍然需要对主体进行身份验证;这就是 Access Token 变得如此重要的地方。虽然令牌比密码更安全,但它们执行与凭证相同的重要安全功能,因此需要保持安全。
我们如何安全地创建和管理它们?现在让我们来了解一下。
JSON Web 令牌
JSON Web 令牌 (JWT) 是一种用于对访问令牌进行编码的行业标准格式。换句话说,当授权服务器铸造访问令牌时,它可以以广泛采用的 JWT 格式写入它。
解码后的 JWT 最基本是一组标头和声明:
- 标头包含有关令牌的元数据,例如资源服务器应如何处理它。
- 声明是令牌断言的事实,如令牌表示的主体。它们被称为“声明”,因为它们仍然需要由资源服务器进行验证 – JWT“声明”这些事实是真实的。
示例可能如下所示:
{"typ": "JWT","alg": "RS256"
}
+headers
{"aud": "https://cashcard.example.org","exp": 1689364985,"iat": 1689361385,"iss": "https://issuer.example.org","scp": ["cashcard:read", "cashcard:write"],"sub": "sarah1"
}
+claims
看看所有这些数据!这一切意味着什么?现在让我们看看其中的几个:
- iss 声明标识铸造令牌的授权服务器。
- exp 声明指示令牌何时过期。
- scp 声明指示授权服务器授予的权限集。
- sub 声明是对令牌表示的主体的引用。
JWT 的签名是一个关键信息。将签名视为您在合同上的签名。一个好的签名只能由一个实体产生,这为我们提供了所谓的不可否认性,或者证明合同是由您签署的,并且只有您签署。
在加密中,签名还提供消息完整性,或证明消息之后没有被任何人更改。在这种情况下,您可以考虑商店罐头或罐子上的防篡改密封。如果密封破损,您不应该从商店购买,因为这意味着可能有人篡改了它。
铸造过程的一部分是让授权服务器签署 JWT。然后,资源服务器验证该签名。这允许资源服务器确认请求的完整性以及委托人的身份。
注意说明:有状态 JWT
希望以有状态的方式使用 JWT 是一种常见的诱惑,例如表示会话。由于此身份验证方案是无状态的(如 HTTP Basic),因此这几乎总是一个坏主意。
例如,假设 JWT 的过期时间无法更新,但会话的过期时间会在每次请求时更新。还要考虑一个人可以注销,因此会话可以过期,但 JWT 不能过期(因为无法编辑其过期时间)。当我们尝试以有状态的方式(如会话管理)使用无状态令牌时,就会出现这些重要的不匹配。
Spring Security 支持
Spring Security 支持所有三个参与者。在本课程中,我们将重点介绍其资源服务器支持,它与 REST API 非常一致。我们将配置应用程序以识别和解析 JWT,验证它们,并相应地制定主体。
对于资源服务器,让我们回顾一下您之前学习的有关 Spring Security 如何处理身份验证的步骤:
-
*它将请求材料解析为凭证。*Spring Security 查找 Authorization: Bearer ${JWT}.
-
*它测试该凭证。*Spring Security 使用JwtDecoder实例查询授权服务器以获取密钥,使用这些密钥来验证 JWT 签名,并验证它是否来自受信任的颁发者并且仍在其到期窗口内。
-
如果认证通过,Spring Security 会将该凭据转换为一个
principal
(主体)和一组authorities
(权限)。
Spring Security 会将 JWT 的 claims(声明)作为principal
存储。它获取scope声明并将每个单独的值解析为SCOPE_${value}模式
然后,此主体和权限由 Spring Security 存储,并在请求的其余部分访问。
测试例子
build.gradle添加依赖
dependencies {...implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
}
application.yml配置
spring:security:oauth2:resourceserver:jwt:public-key-location: classpath:authz.pub
测试
[~/exercises] $ ./gradlew test
application.yml打印更多日志
logging:level:org.springframework.security: TRACE
启动
./gradlew bootRun
测试
http :8080/cashcards
输出token
[~/exercises] $ echo $TOKEN
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwczovL2Nhc2hjYXJkLmV4YW1wbGUub3JnIiwiZXhwIjoxNzQ3MDQ5MjU0LCJpYXQiOjE3NDcwMTMyNTQsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUub3JnIiwic2NvcGUiOlsiY2FzaGNhcmQ6cmVhZCIsImNhc2hjYXJkOndyaXRlIl0sInN1YiI6InNhcmFoMSJ9.eSWIO4C8VrIk1P6T0-aPhuEFgc2ltL6qaFxN4oCPZs6gEtelEm5Na6n8zKG6Cayb7AQguWEzLHjoxKEyHXpcjVawMYsgU9SrAGBm0Jx5nbs7ba1vV_j5LTZ33jIwyK9hKrfz8dHtMnt7OMinsHbgsvrovBGei-dgq9FeWnxkBSNMJO8uX0vjESBjPqWN-q7EhKZphNK3zjeVYhn8JMZG4iPI4Elp3csQGLKOc9oE50GKpjAIK_pRjH7jVsqOt6qWD7yHvLLHrozP8vCcFNiI69lRbELHH58US9_Z3EPRwXHi5PTNBBzN5ygiVvk5LJaneh9S9bVuGTlkxoVZ4VMsYQ
解码token
[~/exercises] $ jwt decode $TOKEN
Token claims
------------
{"aud": "https://cashcard.example.org","exp": 1747049254,"iat": 1747013254,"iss": "https://issuer.example.org","scope": ["cashcard:read","cashcard:write"],"sub": "sarah1"
}
请求
[~/exercises] http :8080/cashcards "Authorization: Bearer $TOKEN"
添加audiences
logging.level:org.springframework.security: TRACEspring:security:oauth2:resourceserver:jwt:public-key-location: classpath:authz.pubaudiences: cashcard-client # <== Add this!
测试
package example.cashcard;import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.test.web.servlet.MockMvc;import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class CashCardSpringSecurityTests {@AutowiredJwtEncoder jwtEncoder;@Autowiredprivate MockMvc mvc;private String mint() {return mint(consumer -> {});}
//生成jwt签名private String mint(Consumer<JwtClaimsSet.Builder> consumer) {JwtClaimsSet.Builder builder = JwtClaimsSet.builder().issuedAt(Instant.now()).expiresAt(Instant.now().plusSeconds(100000)).subject("sarah1").issuer("http://localhost:9000").audience(Arrays.asList("cashcard-client")).claim("scp", Arrays.asList("cashcard:read", "cashcard:write"));consumer.accept(builder);JwtEncoderParameters parameters = JwtEncoderParameters.from(builder.build());return this.jwtEncoder.encode(parameters).getTokenValue();}@TestConfigurationstatic class TestJwtConfiguration {@BeanJwtEncoder jwtEncoder(@Value("classpath:authz.pub") RSAPublicKey pub,@Value("classpath:authz.pem") RSAPrivateKey pem) {RSAKey key = new RSAKey.Builder(pub).privateKey(pem).build();return new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(key)));}}
//普通验证@Test
void shouldRequireValidTokens() throws Exception {String token = mint();this.mvc.perform(get("/cashcards/100").header("Authorization", "Bearer " + token)).andExpect(status().isOk());
}
//验证audience
@Test
void shouldNotAllowTokensWithAnInvalidAudience() throws Exception {String token = mint((claims) -> claims.audience(List.of("https://wrong")));this.mvc.perform(get("/cashcards/100").header("Authorization", "Bearer " + token)).andExpect(status().isUnauthorized()).andExpect(header().string("WWW-Authenticate", containsString("aud claim is not valid")));
}
//验证过期
@Test
void shouldNotAllowTokensThatAreExpired() throws Exception {String token = mint((claims) -> claims.issuedAt(Instant.now().minusSeconds(3600)).expiresAt(Instant.now().minusSeconds(3599)));this.mvc.perform(get("/cashcards/100").header("Authorization", "Bearer " + token)).andExpect(status().isUnauthorized()).andExpect(header().string("WWW-Authenticate", containsString("Jwt expired")));
}
}
注解
自定义错误
3. 授权
3.1 添加请求授权
在本课中,您将了解如何向应用程序添加请求级授权规则。最后,您将了解请求级授权在哪些情况下是最佳模型。
用authorizeHttpRequests
默认情况下,请求级授权在支持 Spring Security 的应用程序中处于活动状态。默认规则是 “all requests require authentication”。
正如您在此时已经多次看到的那样,它看起来像这样:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())// ...
}
DSL 中还有其他几种用于标准请求匹配器的方法。主要的有:
authorizeHttpRequests
-
requestMatchers(String...):此方法将授权配置为根据给定的基于 ant 的 URI 模式应用给定的规则
-
anyRequest():此方法将授权配置为应用于任何请求
规则是从上到下处理的,与 if-else-if 语句的工作方式相同。这意味着匹配的第一条规则是适用的规则。
避免常见的失误
鉴于 first-match-wins 范式,这是一个糟糕的用法示例。你能说出哪里出问题吗?authorizeHttpRequests
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated().requestMatchers("/cashcards").hasAuthority("SCOPE_cashcard:read"))// ...
}
如果您不太确定,这里有一个提示。如果这些是语句,它们将如下所示:
// if-else-if statement example
if (true) { // any request means that every request matches, right?System.out.println("any request");
} else if (the request matches "/cashcards") {System.out.println("cash cards");
}
在这种情况下,当请求为 ?或?或?/cashcards /error /23jk4nw3kljwne
没错!因为被列在第一位,所以总是会被挑选的。anyRequest
幸运的是,如果你使用得太早,Spring Security 会为你发现这一点。但请注意,还有其他方法可以意外地犯这个错误,比如先列出,然后再列出。anyRequest /cashcards/** /cashcards/{id}
请考虑以下代码:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/cashcards/**").authenticated().requestMatchers("/cashcards/{id}").hasAuthority("SCOPE_cashcard:read"))// ...
}
上述代码的意图可能是什么?它可以说:
如果路径为 ,则需要读取权限,否则,它只需要身份验证。/cashcards/{id}
但是,这是错误的!
这是因为规则始终从上到下处理,就像 if-else-if 语句一样。
由于 比 更具体,因此应该首先列出,如下所示:/cashcards/{id} /cashcards/**
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/cashcards/{id}").hasAuthority("SCOPE_cashcard:read").requestMatchers("/cashcards/**").authenticated())// ...
}
请注意,这两个代码示例之间的差异很细微,但很重要!
使用 Coarse-Grained 授权
好了,现在你更了解了这些陷阱。我们来谈谈如何有效地使用粗粒度的请求匹配规则。
这些规则在三个主要方面很有帮助:
- 静态资源
- RESTful 资源
- Catch-all 规则
让我们详细讨论一下这些。
静态资源
粗粒度授权的第一个常见用途是静态资源,如 CSS、Javascript 和图像文件。
等。。。我还需要保护我的静态资源?!
是的!首先,请记住 Javascript 和 CSS 是代码,最小权限原则说未经授权的人不应该访问他们不需要的东西。如果未经授权的用户看到 Java 代码的哪些部分,您是可以接受的?可能很少或没有!
其次,浏览器是一个危险的地方!Spring Security 通过确保每个请求都使用您在本课程前面学到的安全 Headers 进行响应来提供帮助。
在许多情况下,您的静态资源都是公共的,如下所示:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/error", "/css/**", "/js/**", "/images/**").permitAll()// ...)// ...
}
permitAll()
表示允许请求而无需任何身份验证。换句话说,所有非授权筛选器仍然适用。
***提示:***该端点是用于报告错误的 Spring Boot 端点。希望允许这样做是很常见的,至少在开发过程中是这样,这样你总是可以看到 Boot 报告的任何错误。/error
RESTful 资源
如果您可以根据组织 REST 资源的方式来阐明授权规则,那么请求级授权是一个很好的选择。
例如,您可以使用 HTTP 方法将 Cash Card 应用程序分解为读取和写入作,如下所示:
模式 | 需要权限 |
---|---|
GET /cashcards/{id} | 读 |
GET /cashcards/ | 读 |
POST /cashcards | 写 |
由于在我们的应用程序中只有 s require read,因此您可以利用 Spring Security 的 if-else-if 模式匹配,如下所示:GET
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize.requestMatchers(GET, "/cashcards/**").hasAuthority("SCOPE_cashcard:read").requestMatchers("/cashcards/**").hasAuthority("SCOPE_cashcard:write")// ...)// ...
}
这些规则的作用是说:
如果请求是针对 cashcard 资源,则如果它是 ,则需要范围;否则需要 scope。GET cashcard:read cashcard:write
这种粗粒度检查很好,因为在现金卡资源上引入新作时,您不必记住添加新规则。不必记住是一种很好的安全姿态!
Catch-all 规则
我们已经看到了 Spring-Security 的 catch-all 规则的一点强大功能,但让我们用最基本且始终需要的规则来结束它。anyRequest()
当你在最后列出时,这就是你的陈述。当您添加此内容时,这意味着无论您可能忘记了什么或您可能不知道什么要求,一切都至少将获得此级别的安全性。
始终在您的定义末尾至少有一个anyRequest()!
您可以按照我们到目前为止的做法来做,如下所示:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize// ....anyRequest().authenticated())// ...
}
或者你可以更激进地这样做:
@Bean
SecurityFilterChain appSecurity(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorize) -> authorize// ....anyRequest().denyAll())// ...
}
后者的效果是最小特权原则的更激进版本。这意味着您预计您未明确允许的任何内容都会被拒绝给所有人。
方法 | 用途 | 已认证用户 | 未认证用户 | 响应状态码 |
---|---|---|---|---|
.authenticated() | 要求用户必须已认证 | 允许访问 | 重定向到登录页面或返回 401 | 401 Unauthorized |
.denyAll() | 完全禁止访问 | 禁止访问 | 禁止访问 | 403 Forbidden |
使用场景
.authenticated()
- 适用场景:当你希望某些资源或路径仅限于已登录的用户访问时使用。
- 示例:用户个人资料页面、用户管理页面等。
.denyAll()
- 适用场景:当你希望某些资源或路径完全禁止访问时使用。
- 示例:调试接口、内部测试页面等,这些页面不应该被任何用户访问。
3.2 添加方法授权
在本课中,您将学习如何在 Spring Security 中激活方法授权并将其付诸实践。我们将特别关注方法授权可以执行请求授权不能执行的作。
启用方法安全性
首先,请注意 Spring Security 默认情况下不启用方法安全性。这是因为 Spring Security 支持多组方法注释,并且它需要知道你想使用哪组。
在本课中,我们将重点介绍所谓的“pre-post”注释,因为这是 Spring Security 的默认设置。
要启用这些注释,请像这样注释任何类:
@EnableMethodSecurity
@Configuration
public class SecurityConfig { ... }
完成此作后,您可以在整个 Spring 应用程序中使用方法授权。
让我们深入了解如何做到这一点。
保护方法
添加将 Spring Security 配置为拦截和保护任何公共 Spring 管理的类或方法,这些类或方法使用其 pre-post 注释进行注释。本课程我们需要的是:@EnableMethodSecurity
- @PreAuthorize(“rule”):阻止此方法调用,除非给定规则通过
- @PostAuthorize(“rule”):除非给定规则通过,否则阻止此方法返回
***注意:***还有 @PreFilter和 @PostFilter,尽管它们的使用并不常见且超出了本课程的范围。此外,Spring Security 历史上还支持另外两种 Comments 模型。欢迎您阅读参考资料以了解更多信息。
在本课程的前面部分,您了解了方法安全性的两个主要用例是:
- 保护应用程序的非 HTTP 层,以及
- 执行精细授权。
现在让我们仔细看看这些。
保护服务
方法安全性对于保护应用程序的非 HTTP 层非常有用。
考虑一个只有客户服务代表才能使用的 Spring Management 组件。使用如下配置:
@Service
public class CustomerService {@PreAuthorize("hasRole(‘CUSTOMER_SERVICE')")public Customer cancelAccount(UUID id);
}
现在,只有具有该角色的用户才能取消账户。CUSTOMER_SERVICE
从战略上讲,在我们的一些应用程序不使用 HTTP 的情况下,在服务层发布授权规则是非常好的。
防止不安全的直接对象引用
方法安全性对于精细授权也很有用,不安全的直接对象引用缓解措施就是一个主要示例。
考虑到在我们的现金卡应用程序中,任何有权限的人都可以请求,即使该卡不属于他们!显然,我们希望保护用户的数据;只有拥有卡片的用户才能看到其内容。cashcard:read /cashcards/99
您将如何用您已经知道的来防止这种情况发生?
例如,在返回 cashcard 之前,您可以检查它是否属于已登录的用户,如下所示:
@GetMapping("/{id}")
public ResponseEntity<CashCard> findById(@PathParam("id") String id, @CurrentOwner String owner) {CashCard card = ...if (!card.getOwner().equals(authentication.getName())) {throws new AccessDeniedException("denied");}// ...
}
这很好。
但!好消息是 Spring Security 支持这些类型的细粒度授权检查。不要将授权逻辑嵌入到方法中,而是使用如下:@PostAuthorize
@GetMapping("/{id}")
@PostAuthorize("returnObject.body.owner == authentication.name")
public ResponseEntity<CashCard> findById(@PathParam("id") String id) {CashCard card = ...// ...
}
Spring Security 将执行我们之前描述的相同检查,并为您抛出任何需要的异常。这样,除非现金卡实际属于已登录的用户,否则无法退回。
当我们在这里时,让我们明确语法。 是一个特殊变量,Spring Security 理解为该方法的返回值。由于 a 是从此方法返回的,因此表示实例。returnObject ResponseEntity returnObject ResponseEntity
其余部分是标准的 SPEL,可以通过以下方式在概念上进行扩展:
// Expanded example:
ResponseEntity<CashCard> returnObject = findById(id);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (returnObject.getBody().getOwner().equals(authentication.getName())) {return returnObject;
} else {throw new AccessDeniedException("access is denied");
}
我们不能查询数据库吗?
您将有机会在即将到来的实验中练习第二个使用案例,但在我们开始之前,您可能想知道这种方法的效率。为什么我们不这样做,以便未经授权的访问永远不会进入内存呢?
@GetMapping("/{id}")
public ResponseEntity<CashCard> findById(@PathParam("id") String id, @CurrentOwner String owner) {CashCard card = this.cashcards.findByIdAndOwner(id, owner);// ...
}
与许多编码决策一样,也存在权衡。
此方法的性能优势在于,如果访问未经授权,则应用程序不会构造对象,因为查询不会返回任何结果。CashCard
缺点是应用程序无法再区分 403 和 404。在这两种情况下 – 未经授权的访问和无效的 ID – 都没有结果。CashCard
3.3 添加数据授权
在本模块的整个课程中,我们已经明确指出,授权通常发生在应用程序的多个层。您已经了解了如何授权请求及其相应的控制器层。然后,您将阅读有关授权服务层的信息。
在本课中,您将了解如何在数据库层实施授权规则。我们将通过将 Spring Security 身份验证材料添加到 Spring Data 查询中来实现此目的。
启用 Spring Data 授权
与方法授权一样,默认情况下不启用数据授权。此外,默认情况下它不在 Classpath 上,因此我们需要做更多的工作才能使用它。
要激活数据授权,您需要将spring-security-data依赖项添加到应用程序的build.gradle文件中:
dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'testImplementation 'org.springframework.boot:spring-boot-starter-test'// add the spring-security-data dependency:implementation 'org.springframework.security:spring-security-data'
}
完成此作后,您现在可以在查询中引用Authentication实例,如下所示:
@Query("SELECT * FROM cash_card cc WHERE cc.owner = :#{authentication.name}")
如您所见,它支持我们在PreparedStatement方法安全性中已经看到的相同 SPEL 语法,这意味着这实际上与在以下
String query = "SELECT * FROM cash_card cc WHERE cc.owner = ?";
String owner = SecurityContextHolder.getContext().getAuthentication().getName();
PreparedStatement ps = connection.prepareStatement(query);
ps.setValue(1, owner);
防止用户数据泄露
数据授权支持很好,因为它可以帮助您防止意外地在用户之间泄露敏感信息。
例如,考虑我们的 Cash Card API 中的终端节点:
GET /cashcards
@GetMapping
public ResponseEntity<Iterable<CashCard>> findAll() {Iterable<CashCard> cards = this.cashcards.findAll();// ...
}
让控制器处理程序方法调用 repository 方法是相当合理的,但是您真的要返回*整个数据库中的所有现金卡吗?*findAll
当然不是!现金卡所有者真正想要的是只取回属于他们的卡。到目前为止,我们已经通过首先在存储库上调用该方法来实现了这一点:findByOwner
@GetMapping
public ResponseEntity<Iterable<CashCard>> findAll(@CurrentOwner String owner) {Iterable<CashCard> cards = this.cashcards.findByOwner(owner);// ...
}
现在,我们可以做得更好,通过更改 的查询以包含 Authentication 实例中的用户,将用户逻辑移动到存储库,如下所示:findAll
@Query("SELECT * FROM cash_card cc WHERE cc.owner = :#{authentication.name}")
Iterable<CashCard> findAll();
这样做有两个好处:首先,它删除了样板代码,其次,它确保服务不会意外调用和泄露用户数据。我们已将卡所有者逻辑从可能的许多服务集中到这个存储库调用。因此,我们减少了安全“表面积”。findAll
方法签名的清晰度
与我们在前面的课程中讨论的其他授权技术一样,数据授权也有其他选择及其相关的权衡。
在这种情况下,您可能更喜欢给读者的清晰度;很明显,所有者包含在查询中。相比之下,你不能只通过 的名字来判断 所有者是否包括在内。findByOwner(String owner) findAll
要解决此问题,您可以通过始终声明适当的方法,然后覆盖该方法来防止泄露用户数据,如下所示:findBy findAll
Iterable<CashCard> findByOwner(String owner);
default Iterable<CashCard> findAll() {throw new UnsupportedOperationException("unsupported,please use findByOwner instead");
}
3.4 添加委托授权
外部实体可以通过多种方式决定授权。
如您所知,本课程重点介绍 OAuth 2.0 故事,它以 OAuth 2.0 范围的方式传达这些授权决策。到目前为止,在实验室中,我们一直在通过使用预先铸造的 JWT 来模拟这些授权决策。
在最后一课中,您将学习如何将资源服务器连接到授权服务器,该服务器将为我们做出授权决策并铸造 JWT。
使用授权服务器
要将资源服务器指向授权服务器,您需要添加spring-security-oauth2-resource-server依赖项并将 DSL 调用添加到oauth2ResourceServer过滤器链定义中。
除此之外,资源服务器还需要知道授权服务器的位置。您可以使用 Spring Boot 属性执行此作,如下所示:
spring:security:oauth2:resourceserver:jwt:issuer-uri: https://example.org/oauth2
这在性质上类似于您在本课程早期学到的房产。不同之处在于 Spring Security 可以使用issuer-uri属性来计算提供一组公钥的授权服务器端点,从而有效地替换public-key-location属性。
在启动时,Spring Security 使用此属性来制定对授权服务器的 OIDC 发现请求。例如,它将 https://example.org/oauth2 并将其转换为 https://example.org/oauth2/.well-known/openid-configuration 这将产生类似于以下内容的结果:
{// …“jwks_url” : “https://example.org/oauth2/jwks”// …
端点是资源服务器在应用程序的整个生命周期中用于检索、缓存和定期重新检索授权服务器的公钥的源。jwks_url
Spring Security 对此值所做的是创建您在身份验证模块期间了解的 bean。它看起来像这样:JwtDecoder
@Bean
JwtDecoder jwtDecoder() {return new SupplierJwtDecoder(() -> JwtDecoders.fromIssuerLocation(issuerUri).build());
}
**注意:**用于将对授权服务器的调用推迟到第一个请求。这使得解码器在重启期间更具弹性,因为从那时起,授权服务器就不需要运行。SupplierJwtDecoder
启动后,要与资源服务器通信,您现在必须使用授权服务器支持的任何授权流向授权服务器请求令牌。REST API 最常见的两种方式是授权码授予流和客户端凭证授予流。我们将在下一个实验中了解客户端凭证授予流程。
但是,我没有授权服务器
有时 REST API 的需求非常简单,以至于开发人员希望让 REST API 铸造自己的代币。当然,您可以验证自签名令牌,但首先要问自己“为什么我要自签名令牌?
例如,开发人员可能希望将用户的用户名和密码提供给 REST API,并将其交换为 JWT,这样就不必一遍又一遍地使用用户名和密码。只要这是它停止的地方 – 并且 JWT 没有成为会话的表示 – 那很好。虽然它超出了本课程的范围,但您已经在测试中看到可以使用 Spring Security 来协助完成此作。CashCard JwtEncoder
当您问自己时,事情可能会变得非常棘手:您如何知道用户是否仍处于登录状态、会话是否已过期、是否已注销,或者是否需要强制删除?这些都是有状态的问题。由于 JWT 是无状态且不可撤销的,因此您不能单独使用 JWT 来做出此类安全决策。
虽然您可以在设计中添加状态来解决这些问题,但请记住,Spring Security 已经支持经过严格审查的安全标准,例如 OIDC/OAuth 2.0。授权服务器与 Spring Security 的 OAuth 2.0 客户端支持相结合,可帮助您以基于标准的方式满足这些需求。
测试例子
- 配置授权服务器
spring:security:oauth2:resourceserver:jwt:issuer-uri: http://localhost:9000audiences: cashcard-client
启动应用
[~/exercises] $ ./gradlew bootRun
...
> IDLE
> :bootRun
请求应用
[~/exercises] $ http -A bearer -a $LOCAL_TOKEN :8080/cashcardsHTTP/1.1 401
...
启动授权服务器
[~/exercises] $ docker run --rm --name sso -p 9000:9000 ghcr.io/vmware-tanzu-learning/course-secure-rest-api-oauth2-code/sso:latest
http -a <client_id>:<client_secret> --form <auth_server_endpoint> grant_type=<the_grant_type> scope=<scopes>
这些参数是用来干什么的?
client_id
- 描述:客户端应用程序的公开标识符。授权服务器使用它来识别客户端应用程序,并确定它是否有权限访问受保护的资源。
- 示例值:
cashcard-client
- 作用:
client_id
用于向授权服务器唯一标识客户端应用程序。
client_secret
- 描述:只有客户端应用程序和授权服务器知道的私有值。它用于验证客户端应用程序的真实性,并确保它有权限请求访问受保护的资源。
- 示例值:
secret
- 作用:
client_secret
与client_id
一起用于客户端认证,确保请求是由合法的客户端发起的。
auth_server_endpoint
- 描述:授权服务器的端点,用于获取令牌。
- 示例值:
:9000/oauth2/token
- 作用:客户端应用程序向这个端点发送请求,以获取访问令牌。
grant_type
- 描述:OAuth 2.0 中的一个参数,用于指定正在发起的授权请求的类型。这种授权类型用于客户端应用程序在不需要用户交互的情况下获取访问令牌。
- 示例值:
client_credentials
- 作用:
grant_type
告诉授权服务器客户端请求令牌的方式。client_credentials
表示客户端使用自己的凭据(client_id
和client_secret
)来获取令牌。
scope
- 描述:定义了客户端应用程序请求的权限范围。它限制了访问令牌可以访问的资源。
- 示例值:
cashcard:read
- 作用:
scope
确定了访问令牌的权限范围,确保客户端应用程序只能访问其被授权的资源。
现在我们有了这一点上下文,我们可以运行以下命令来生成一个有效的JWT。
[~/exercises] $ http -a cashcard-client:secret --form :9000/oauth2/token grant_type=client_credentials scope=cashcard:readHTTP/1.1 200
...
{"access_token": "eyJraWQiOiJhOEJVNk9uQW1kNkRNQnI4OXliZGJ2VGJOVHVjYmNlcEFNOWhsZzB0ekRJIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJzYXJhaDEiLCJhdWQiOiJjYXNoY2FyZC1jbGllbnQiLCJuYmYiOjE2OTkzMTIxODUsInNjb3BlIjpbImNhc2hjYXJkOnJlYWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwiZXhwIjoxNjk5MzEyNDg1LCJpYXQiOjE2OTkzMTIxODV9.BKNRmZdDYhd0umVURrvYFZG16BoEe0qZdv-JoOrxqCjbIAMjcKYKhAaF560IG2OHzp8BYCnz0Zh_rZNqu6m4Hf3CjArToOI2tq_lWpqaeN1V49ZHMLoVnxnLtu3GOAYQymz9dImNcaJa6ijpC-qDtGd0uxrrQCAFl1fnoTUir6mCQY4lcDOZ9Ly2mLB-3iMsataRwfRWWoYGVXXDeYhBmw6PzNSbxdbZOBc6sy0YW3YZC9c8w-HQLFS4Ry2oODxmOUJr_-fXMCpqW0dtWE7hwnwyYWTod4uq74jKMhYeKAYH-xrj9sYJJOmZXVcAmdGKjmkZJbDoOdpkRNrxQn0nBA","expires_in": 299,"scope": "cashcard:read","token_type": "Bearer"
}
复制access_token:处的值并解码。请记住,您的令牌将是不同的,因此请务必从终端窗格中复制它。
[~/exercises] $ jwt decode eyJra...<snip>...n0nBA
...
Token claims
------------
{"aud": "cashcard-client","exp": 1699312485,"iat": 1699312185,"iss": "http://localhost:9000","nbf": 1699312185,"scope": ["cashcard:read"],"sub": "sarah1"
}
我们稍后将使用这个令牌,因此为了方便起见,将其导出为REQUESTED_TOKEN。同样,请确保从终端窗格复制令牌。
[~/exercises] $ export REQUESTED_TOKEN=eyJra...<snip>...n0nBA
使用token
[~/exercises] $ http -A bearer -a $REQUESTED_TOKEN :8080/cashcardsHTTP/1.1 200
...
[{"amount": 123.45,"id": 99,"owner": "sarah1"},{"amount": 1.0,"id": 100,"owner": "sarah1"}
]