JWT¶
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑而独立的方式,用于在各方之间作为 JSON 对象安全地传输信息。此信息可以被验证和信任,因为它经过数字签名。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
入门¶
使用 JWT 的第一步是将依赖项添加到你的 Package.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: "5.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。
import JWT
// 添加具有 SHA-256 的 HMAC 算法的签名者。
app.jwt.signers.use(.hs256(key: "secret"))
HS256
签名者需要一个密钥来初始化。与其他签名者不同,这个单一密钥用于签名 和 验证令牌。在下面了解算法的更多信息。
Payload¶
让我们尝试验证以下 JWT 示例。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
你可以访问 jwt.io 网站并将该令牌粘贴到调试器中来检查该令牌的内容。将 “Verify Signature” 部分中的密钥设置为 secret
。
我们需要创建一个符合 JWTPayload
的结构来表示 JWT 的结构。我们将使用 JWT 包含的 声明 来处理常见的字段,如 sub
和 exp
。
// 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
来获取和验证它。将以下路由添加到你的项目中。
// 从请求中获取并验证 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 请求来测试路由。
GET /me HTTP/1.1
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
如果一切正常,将返回 200 OK 响应并打印 payload:
TestPayload(
subject: "vapor",
expiration: 4001-01-01 00:00:00 +0000,
isAdmin: true
)
签名¶
此包还可以 生成 JWT,也称为签名。为了演示这一点,让我们使用上一节中的 TestPayload
。将以下路由添加到你的项目中。
// 生成并返回一个新的 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 请求来测试路由。
POST /login HTTP/1.1
你应该会看到在 200 OK 响应中返回的新生成的令牌。
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
}
认证¶
了解 JWT 与 Vapor 的身份验证 API 结合使用的更多信息,请访问 认证 → JWT。
算法(Algorithms)¶
Vapor 的 JWT API 支持使用以下算法验证和签名令牌。
HMAC¶
HMAC 是最简单的 JWT 签名算法。它使用一个既可以签名又可以验证令牌的密钥。密钥可以是任意长度。
hs256
:带有 SHA-256 的 HMAChs384
:带有 SHA-384 的 HMAChs512
:带有 SHA-512 的 HMAC
// 添加带有 SHA-256 的 HMAC 算法的签名者。
app.jwt.signers.use(.hs256(key: "secret"))
RSA¶
RSA 是最常用的 JWT 签名算法。它支持不同的公钥和私钥。这意味着可以分发公钥来验证 JWT 的真实性,而生成它们的私钥是保密的。
要创建 RSA 签名者,首先初始化一个 RSAKey
。这可以通过传入组件来完成。
// 使用组件初始化 RSA 密钥。
let key = RSAKey(
modulus: "...",
exponent: "...",
// 仅包含在私钥中。
privateExponent: "..."
)
你还可以选择加载 PEM 文件:
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 的 RSArs384
:带有 SHA-384 的 RSArs512
:带有 SHA-512 的 RSA
// 添加带有 SHA-256 的 RSA 算法的签名者。
try app.jwt.signers.use(.rs256(key: .public(pem: rsaPublicKey)))
ECDSA¶
ECDSA 是一种更现代的算法,类似于 RSA。对于给定的密钥长度,它被认为比 RSA1 更安全。然而,在做出决定之前,你应该自己研究一下。
与 RSA 一样,你可以使用 PEM 文件加载 ECDSA 密钥:
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。这对测试很有用。
let key = try ECDSAKey.generate()
拥有 ECDSAKey 后,你可以使用它来创建 ECDSA 签名者。
es256
:带有 SHA-256 的 ECDSAes384
:带有 SHA-384 的 ECDSAes512
:带有 SHA-512 的 ECDSA
// 添加带有 SHA-256 的 ECDSA 算法的签名者
try app.jwt.signers.use(.es256(key: .public(pem: ecdsaPublicKey)))
密钥标识符 (kid)¶
如果你使用多个算法,则可以使用密钥标识符(kid
s)来区分它们。配置算法时,请传递 kid 参数。
// 添加名为 ”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
参数。
// 使用签名者 ”a“ 进行签名
req.jwt.sign(payload, kid: "a")
这将自动将签名者的名字包括在 JWT 头的 kid
字段中。在验证 JWT 时,此字段将用于查找适当的签名者。
// 使用 ”kid“ 头部指定的签名者进行验证。
// 如果没有 ”kid“ 头部,则使用默认的签名者
let payload = try req.jwt.verify(as: TestPayload.self)
由于 JWKs 已包含 kid
值,因此你无需在配置期间指定它们。
// JWKs 已经包含 ”kid“ 字段。
let jwk: JWK = ...
app.jwt.signers.use(jwk: jwk)
声明(Claims)¶
Vapor 的 JWT 包包括几个用于实现常见 JWT 声明的辅助函数。
声明 | 类型 | 验证方法 |
---|---|---|
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) 的 JavaScript 对象表示法 (JSON) 数据结构,它们通常用于向客户端提供用于验证 JWT 的密钥。
例如,Apple 将他们的 Sign in with Apple JWKS 托管在以下 URL 中。
GET https://appleid.apple.com/auth/keys
你可以将此 JSON Web 密钥集 (JWKS) 添加到你的 JWTSigners
中。
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)¶
Vapor 提供了用于处理来自以下热门发行商的 JWT 的 API。
Apple¶
首先,配置你的 Apple 应用程序标识符。
// 配置 Apple 应用标识符。
app.jwt.apple.applicationIdentifier = "..."
然后,使用 req.jwt.apple
辅助函数获取并验证 Apple JWT。
// 从 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 域名。
// 配置 Google 应用标识符和域名。
app.jwt.google.applicationIdentifier = "..."
app.jwt.google.gSuiteDomainName = "..."
然后,使用 req.jwt.google
辅助函数获取并验证 Google JWT。
// 从 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 应用程序标识符。
// 配置 Microsoft 应用标识符.
app.jwt.microsoft.applicationIdentifier = "..."
然后,使用 req.jwt.microsoft
辅助函数获取并验证 Microsoft JWT。
// 从 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
}
-
https://www.ssl.com/article/comparing-ecdsa-vs-rsa/ ↩