mirror of https://github.com/vapor/docs.git
441 lines
13 KiB
Markdown
441 lines
13 KiB
Markdown
# JWT
|
||
|
||
JSON Web Token (JWT) 是一种开放标准 ([RFC 7519](https://tools.ietf.org/html/rfc7519)),它定义了一种紧凑而独立的方式,用于在各方之间作为 JSON 对象安全地传输信息。此信息可以被验证和信任,因为它经过数字签名。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
|
||
|
||
## 入门
|
||
|
||
使用 JWT 的第一步是将依赖项添加到你的 [Package.swift](../getting-started/spm.zh.md#package-manifest) 文件中。
|
||
|
||
```swift
|
||
// swift-tools-version:5.2
|
||
import PackageDescription
|
||
|
||
let package = Package(
|
||
name: "my-app",
|
||
dependencies: [
|
||
// Other dependencies...
|
||
.package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"),
|
||
],
|
||
targets: [
|
||
.target(name: "App", dependencies: [
|
||
// Other dependencies...
|
||
.product(name: "JWT", package: "jwt")
|
||
]),
|
||
// Other targets...
|
||
]
|
||
)
|
||
```
|
||
|
||
如果你直接在 Xcode 中编辑清单,它会在文件保存时自动获取更改并获取新的依赖项。否则,在终端运行 `swift package resolve` 命令以获取新的依赖项。
|
||
|
||
### 配置
|
||
|
||
JWT 模块在 `Application` 中增加了一个新的属性 `jwt`,用于配置。要签名或验证 JWT,你需要添加一个签名者。最简单的签名算法是 `HS256` 或带有 SHA-256 的 HMAC。
|
||
|
||
```swift
|
||
import JWT
|
||
|
||
// 添加具有 SHA-256 的 HMAC 算法的签名者。
|
||
app.jwt.signers.use(.hs256(key: "secret"))
|
||
```
|
||
|
||
`HS256` 签名者需要一个密钥来初始化。与其他签名者不同,这个单一密钥用于签名 _和_ 验证令牌。在下面了解[算法](#algorithms)的更多信息。
|
||
|
||
### Payload
|
||
|
||
让我们尝试验证以下 JWT 示例。
|
||
|
||
```swift
|
||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
|
||
```
|
||
|
||
你可以访问 [jwt.io](https://jwt.io) 网站并将该令牌粘贴到调试器中来检查该令牌的内容。将 “Verify Signature” 部分中的密钥设置为 `secret`。
|
||
|
||
我们需要创建一个符合 `JWTPayload` 的结构来表示 JWT 的结构。我们将使用 JWT 包含的 [声明](#claims) 来处理常见的字段,如 `sub`和 `exp`。
|
||
|
||
```swift
|
||
// JWT payload 结构。
|
||
struct TestPayload: JWTPayload {
|
||
// 将较长的 Swift 属性名称映射到 JWT payload 中使用的缩写密钥。
|
||
enum CodingKeys: String, CodingKey {
|
||
case subject = "sub"
|
||
case expiration = "exp"
|
||
case isAdmin = "admin"
|
||
}
|
||
|
||
// "sub" (主题) 声明标识了作为 JWT 主题的主体。
|
||
var subject: SubjectClaim
|
||
|
||
// “exp” (过期时间) 声明标识了过期时间,过期后 JWT 绝对不能被接受处理。
|
||
var expiration: ExpirationClaim
|
||
|
||
// 自定义数据。
|
||
// 如果为真,则该用户为管理员。
|
||
var isAdmin: Bool
|
||
|
||
// 在这里运行额外的签名验证逻辑。
|
||
// 因为我们有 ExpirationClaim,我们将调用其 verify 方法。
|
||
func verify(using signer: JWTSigner) throws {
|
||
try self.expiration.verifyNotExpired()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 验证
|
||
|
||
现在我们有了一个 `JWTPayload`,我们可以将上面的 JWT 附加到一个请求中,并使用 `req.jwt` 来获取和验证它。将以下路由添加到你的项目中。
|
||
|
||
```swift
|
||
// 从请求中获取并验证 JWT。
|
||
app.get("me") { req -> HTTPStatus in
|
||
let payload = try req.jwt.verify(as: TestPayload.self)
|
||
print(payload)
|
||
return .ok
|
||
}
|
||
```
|
||
|
||
`req.jwt.verify` 辅助函数将检查 `Authorization` 请求头中的不记名令牌。如果存在,它将解析 JWT 并验证其签名和声明。如果这些步骤中的任何一个失败,则将抛出 _401未经授权_ 的错误。
|
||
|
||
通过发送以下 HTTP 请求来测试路由。
|
||
|
||
```http
|
||
GET /me HTTP/1.1
|
||
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
|
||
```
|
||
|
||
如果一切正常,将返回 *200 OK* 响应并打印 payload:
|
||
|
||
```swift
|
||
TestPayload(
|
||
subject: "vapor",
|
||
expiration: 4001-01-01 00:00:00 +0000,
|
||
isAdmin: true
|
||
)
|
||
```
|
||
|
||
### 签名
|
||
|
||
此包还可以 _生成_ JWT,也称为签名。为了演示这一点,让我们使用上一节中的 `TestPayload`。将以下路由添加到你的项目中。
|
||
|
||
```swift
|
||
// 生成并返回一个新的 JWT。
|
||
app.post("login") { req -> [String: String] in
|
||
// 创建一个 JWTPayload 实例
|
||
let payload = TestPayload(
|
||
subject: "vapor",
|
||
expiration: .init(value: .distantFuture),
|
||
isAdmin: true
|
||
)
|
||
// 返回签名的 JWT。
|
||
return try [
|
||
"token": req.jwt.sign(payload)
|
||
]
|
||
}
|
||
```
|
||
|
||
`req.jwt.sign` 辅助函数将使用默认配置的签名器来序列化和签名 `JWTPayLoad`。编码后的 JWT 以 `String` 形式返回。
|
||
|
||
通过发送以下 HTTP 请求来测试路由。
|
||
|
||
```http
|
||
POST /login HTTP/1.1
|
||
```
|
||
|
||
你应该会看到在 _200 OK_ 响应中返回的新生成的令牌。
|
||
|
||
```json
|
||
{
|
||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
|
||
}
|
||
```
|
||
|
||
## 认证
|
||
|
||
了解 JWT 与 Vapor 的身份验证 API 结合使用的更多信息,请访问 [认证 → JWT](authentication.md#jwt)。
|
||
|
||
## 算法(Algorithms)
|
||
|
||
Vapor 的 JWT API 支持使用以下算法验证和签名令牌。
|
||
|
||
### HMAC
|
||
|
||
HMAC 是最简单的 JWT 签名算法。它使用一个既可以签名又可以验证令牌的密钥。密钥可以是任意长度。
|
||
|
||
- `hs256`:带有 SHA-256 的 HMAC
|
||
- `hs384`:带有 SHA-384 的 HMAC
|
||
- `hs512`:带有 SHA-512 的 HMAC
|
||
|
||
```swift
|
||
// 添加带有 SHA-256 的 HMAC 算法的签名者。
|
||
app.jwt.signers.use(.hs256(key: "secret"))
|
||
```
|
||
|
||
### RSA
|
||
|
||
RSA 是最常用的 JWT 签名算法。它支持不同的公钥和私钥。这意味着可以分发公钥来验证 JWT 的真实性,而生成它们的私钥是保密的。
|
||
|
||
要创建 RSA 签名者,首先初始化一个 `RSAKey`。这可以通过传入组件来完成。
|
||
|
||
```swift
|
||
// 使用组件初始化 RSA 密钥。
|
||
let key = RSAKey(
|
||
modulus: "...",
|
||
exponent: "...",
|
||
// 仅包含在私钥中。
|
||
privateExponent: "..."
|
||
)
|
||
```
|
||
|
||
你还可以选择加载 PEM 文件:
|
||
|
||
```swift
|
||
let rsaPublicKey = """
|
||
-----BEGIN PUBLIC KEY-----
|
||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx
|
||
PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv
|
||
7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/
|
||
aX4rbSL49Z3dAQn8vQIDAQAB
|
||
-----END PUBLIC KEY-----
|
||
"""
|
||
|
||
// 使用公共 pem 初始化 RSA 密钥。
|
||
let key = RSAKey.public(pem: rsaPublicKey)
|
||
```
|
||
|
||
使用 `.private` 加载 RSA PEM 私钥。它们以以下内容开头:
|
||
|
||
```
|
||
-----BEGIN RSA PRIVATE KEY-----
|
||
```
|
||
|
||
获得 RSAKey 后,你可以使用它来创建 RSA 签名者。
|
||
|
||
- `rs256`:带有 SHA-256 的 RSA
|
||
- `rs384`:带有 SHA-384 的 RSA
|
||
- `rs512`:带有 SHA-512 的 RSA
|
||
|
||
```swift
|
||
// 添加带有 SHA-256 的 RSA 算法的签名者。
|
||
try app.jwt.signers.use(.rs256(key: .public(pem: rsaPublicKey)))
|
||
```
|
||
|
||
### ECDSA
|
||
|
||
ECDSA 是一种更现代的算法,类似于 RSA。对于给定的密钥长度,它被认为比 RSA[^1] 更安全。然而,在做出决定之前,你应该自己研究一下。
|
||
|
||
[^1]: https://www.ssl.com/article/comparing-ecdsa-vs-rsa/
|
||
|
||
与 RSA 一样,你可以使用 PEM 文件加载 ECDSA 密钥:
|
||
|
||
```swift
|
||
let ecdsaPublicKey = """
|
||
-----BEGIN PUBLIC KEY-----
|
||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
|
||
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
|
||
-----END PUBLIC KEY-----
|
||
"""
|
||
|
||
// 使用公共 PEM 初始化 ECDSA 密钥。
|
||
let key = ECDSAKey.public(pem: ecdsaPublicKey)
|
||
```
|
||
|
||
使用 `.private` 加载私有 ECDSA PEM 密钥。它们以以下内容开头:
|
||
|
||
```
|
||
-----BEGIN PRIVATE KEY-----
|
||
```
|
||
|
||
你还可以使用 `generate()` 方法随机生成 ECDSA。这对测试很有用。
|
||
|
||
```swift
|
||
let key = try ECDSAKey.generate()
|
||
```
|
||
|
||
拥有 ECDSAKey 后,你可以使用它来创建 ECDSA 签名者。
|
||
|
||
- `es256`:带有 SHA-256 的 ECDSA
|
||
- `es384`:带有 SHA-384 的 ECDSA
|
||
- `es512`:带有 SHA-512 的 ECDSA
|
||
|
||
```swift
|
||
// 添加带有 SHA-256 的 ECDSA 算法的签名者
|
||
try app.jwt.signers.use(.es256(key: .public(pem: ecdsaPublicKey)))
|
||
```
|
||
|
||
### 密钥标识符 (kid)
|
||
|
||
如果你使用多个算法,则可以使用密钥标识符(`kid`s)来区分它们。配置算法时,请传递 kid 参数。
|
||
|
||
```swift
|
||
// 添加名为 ”a“ 带有 SHA-256 的 HMAC 算法的签名者
|
||
app.jwt.signers.use(.hs256(key: "foo"), kid: "a")
|
||
// 添加名为 ”b“ 带有 SHA-256 的 HMAC 算法的签名者
|
||
app.jwt.signers.use(.hs256(key: "bar"), kid: "b")
|
||
```
|
||
|
||
在对 JWT 签名时,传递所需签名者的 `kid` 参数。
|
||
|
||
```swift
|
||
// 使用签名者 ”a“ 进行签名
|
||
req.jwt.sign(payload, kid: "a")
|
||
```
|
||
|
||
这将自动将签名者的名字包括在 JWT 头的 `kid` 字段中。在验证 JWT 时,此字段将用于查找适当的签名者。
|
||
|
||
```swift
|
||
// 使用 ”kid“ 头部指定的签名者进行验证。
|
||
// 如果没有 ”kid“ 头部,则使用默认的签名者
|
||
let payload = try req.jwt.verify(as: TestPayload.self)
|
||
```
|
||
|
||
由于 [JWKs](#jwk) 已包含 `kid` 值,因此你无需在配置期间指定它们。
|
||
|
||
```swift
|
||
// JWKs 已经包含 ”kid“ 字段。
|
||
let jwk: JWK = ...
|
||
app.jwt.signers.use(jwk: jwk)
|
||
```
|
||
|
||
## 声明(Claims)
|
||
|
||
Vapor 的 JWT 包包括几个用于实现常见 [JWT 声明](https://tools.ietf.org/html/rfc7519#section-4.1)的辅助函数。
|
||
|
||
|声明|类型|验证方法|
|
||
|---|---|---|
|
||
|`aud`|`AudienceClaim`|`verifyIntendedAudience(includes:)`|
|
||
|`exp`|`ExpirationClaim`|`verifyNotExpired(currentDate:)`|
|
||
|`jti`|`IDClaim`|n/a|
|
||
|`iat`|`IssuedAtClaim`|n/a|
|
||
|`iss`|`IssuerClaim`|n/a|
|
||
|`locale`|`LocaleClaim`|n/a|
|
||
|`nbf`|`NotBeforeClaim`|`verifyNotBefore(currentDate:)`|
|
||
|`sub`|`SubjectClaim`|n/a|
|
||
|
||
所有声明都应该在 `JWTPayload.verify` 方法中进行验证。如果声明有特殊的验证方法,你可以使用它。否则,使用 `value` 访问声明的值并检查它是否有效。
|
||
|
||
## JWK
|
||
|
||
|
||
JSON Web Key (JWK) 是一种表示密钥 ([RFC7517](https://tools.ietf.org/html/rfc7517)) 的 JavaScript 对象表示法 (JSON) 数据结构,它们通常用于向客户端提供用于验证 JWT 的密钥。
|
||
|
||
例如,Apple 将他们的 _Sign in with Apple_ JWKS 托管在以下 URL 中。
|
||
|
||
```http
|
||
GET https://appleid.apple.com/auth/keys
|
||
```
|
||
|
||
你可以将此 JSON Web 密钥集 (JWKS) 添加到你的 `JWTSigners` 中。
|
||
|
||
```swift
|
||
import JWT
|
||
import Vapor
|
||
|
||
// 下载 JWKS.
|
||
// 如果需要,这可以异步完成。
|
||
let jwksData = try Data(
|
||
contentsOf: URL(string: "https://appleid.apple.com/auth/keys")!
|
||
)
|
||
|
||
// 对下载的 JSON 进行解码。
|
||
let jwks = try JSONDecoder().decode(JWKS.self, from: jwksData)
|
||
|
||
// 创建签名者并添加 JWKS。
|
||
try app.jwt.signers.use(jwks: jwks)
|
||
```
|
||
|
||
现在可以将 JWT 从 Apple 传递给 `verify` 方法。JWT 报头中的密钥标识符 (`kid`) 会自动选择正确的密钥进行验证。
|
||
|
||
在撰写本文时,JWK 只支持 RSA 密钥。此外,JWT 发行商可能会轮换他们的 JWK,这意味着你偶尔需要重新下载。有关自动执行此操作的 API,请参阅下面的 Vapor 支持的 JWT [供应商](#vendors)列表。
|
||
|
||
## 发行商(Vendors)
|
||
|
||
Vapor 提供了用于处理来自以下热门发行商的 JWT 的 API。
|
||
|
||
### Apple
|
||
|
||
首先,配置你的 Apple 应用程序标识符。
|
||
|
||
```swift
|
||
// 配置 Apple 应用标识符。
|
||
app.jwt.apple.applicationIdentifier = "..."
|
||
```
|
||
|
||
然后,使用 `req.jwt.apple` 辅助函数获取并验证 Apple JWT。
|
||
|
||
```swift
|
||
// 从 Authorization 头获取并验证 Apple JWT。
|
||
app.get("apple") { req -> EventLoopFuture<HTTPStatus> in
|
||
req.jwt.apple.verify().map { token in
|
||
print(token) // Apple 身份令牌
|
||
return .ok
|
||
}
|
||
}
|
||
|
||
// Or
|
||
|
||
app.get("apple") { req async throws -> HTTPStatus in
|
||
let token = try await req.jwt.apple.verify()
|
||
print(token) // Apple 身份令牌
|
||
return .ok
|
||
}
|
||
```
|
||
|
||
### Google
|
||
|
||
首先,配置你的 Google 应用标识符和 G Suite 域名。
|
||
|
||
```swift
|
||
// 配置 Google 应用标识符和域名。
|
||
app.jwt.google.applicationIdentifier = "..."
|
||
app.jwt.google.gSuiteDomainName = "..."
|
||
```
|
||
|
||
然后,使用 `req.jwt.google` 辅助函数获取并验证 Google JWT。
|
||
|
||
```swift
|
||
// 从 Authorization 头获取并验证 Google JWT。
|
||
app.get("google") { req -> EventLoopFuture<HTTPStatus> in
|
||
req.jwt.google.verify().map { token in
|
||
print(token) // Google 身份令牌
|
||
return .ok
|
||
}
|
||
}
|
||
|
||
// 或
|
||
|
||
app.get("google") { req async throws -> HTTPStatus in
|
||
let token = try await req.jwt.google.verify()
|
||
print(token) // Google 身份令牌
|
||
return .ok
|
||
}
|
||
```
|
||
|
||
### Microsoft
|
||
|
||
首先,配置你的 Microsoft 应用程序标识符。
|
||
|
||
```swift
|
||
// 配置 Microsoft 应用标识符.
|
||
app.jwt.microsoft.applicationIdentifier = "..."
|
||
```
|
||
|
||
然后,使用 `req.jwt.microsoft` 辅助函数获取并验证 Microsoft JWT。
|
||
|
||
```swift
|
||
// 从 Authorization 头获取并验证 Microsoft JWT。
|
||
app.get("microsoft") { req -> EventLoopFuture<HTTPStatus> in
|
||
req.jwt.microsoft.verify().map { token in
|
||
print(token) // Microsoft 身份令牌
|
||
return .ok
|
||
}
|
||
}
|
||
|
||
// 或
|
||
|
||
app.get("microsoft") { req async throws -> HTTPStatus in
|
||
let token = try await req.jwt.microsoft.verify()
|
||
print(token) // Microsoft 身份令牌
|
||
return .ok
|
||
}
|
||
```
|