🔑 使用 SwiftCrypto 的 JSON Web Token 签名和验证 (HMAC, RSA, PSS, ECDSA, EdDSA)。
JWTKit 支持 Swift 6 及更高版本支持的所有平台。
使用 SPM 字符串可以轻松地将依赖项包含到您的 Package.swift
文件中
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0")
并将其添加到您的目标依赖项中
.product(name: "JWTKit", package: "jwt-kit")
JWTKit 提供了用于签名和验证 JSON Web Token 的 API,如 RFC 7519 中所指定的那样。以下功能受到支持
JWK
, JWKS
)以下算法,如 RFC 7518 § 3 和 RFC 8037 § 3 中所定义的那样,均支持签名和验证
JWS | 算法 | 描述 |
---|---|---|
HS256 | HMAC256 | 使用 SHA-256 的 HMAC |
HS384 | HMAC384 | 使用 SHA-384 的 HMAC |
HS512 | HMAC512 | 使用 SHA-512 的 HMAC |
RS256 | RSA256 | 使用 SHA-256 的 RSASSA-PKCS1-v1_5 |
RS384 | RSA384 | 使用 SHA-384 的 RSASSA-PKCS1-v1_5 |
RS512 | RSA512 | 使用 SHA-512 的 RSASSA-PKCS1-v1_5 |
PS256 | RSA256PSS | 使用 SHA-256 的 RSASSA-PSS |
PS384 | RSA384PSS | 使用 SHA-384 的 RSASSA-PSS |
PS512 | RSA512PSS | 使用 SHA-512 的 RSASSA-PSS |
ES256 | ECDSA256 | 使用曲线 P-256 和 SHA-256 的 ECDSA |
ES384 | ECDSA384 | 使用曲线 P-384 和 SHA-384 的 ECDSA |
ES512 | ECDSA512 | 使用曲线 P-521 和 SHA-512 的 ECDSA |
EdDSA | EdDSA | 使用 Ed25519 的 EdDSA |
none | None | 无数字签名或 MAC |
vapor/jwt 包提供了与 Vapor 的一流集成,并推荐给所有想要使用 JWTKit 的 Vapor 项目。
JWTKeyCollection
对象用于加载签名密钥和密钥集,以及签名和验证令牌
import JWTKit
// Signs and verifies JWTs
let keys = JWTKeyCollection()
要将签名密钥添加到集合,请为相应的算法使用 add
方法
// Registers an HS256 (HMAC-SHA-256) signer.
await keys.add(hmac: "secret", digestAlgorithm: .sha256)
此示例使用了非常安全的密钥 "secret"
。
您还可以向密钥添加可选的密钥标识符 (kid
)
// Registers an HS256 (HMAC-SHA-256) signer with a key identifier.
await keys.add(hmac: "secret", digestAlgorithm: .sha256, kid: "my-key")
当您有多个密钥并且需要选择正确的密钥进行验证时,这非常有用。根据 JWT 标头中定义的 kid
,将选择正确的密钥进行验证。如果您不提供 kid
,则密钥将作为默认密钥添加到集合中。
注意
如果添加的多个密钥都没有 kid
,则只会存储最后一个密钥,之前的密钥将被覆盖,这意味着如果您想要存储多个密钥,则需要为每个密钥提供一个 kid
。
为了确保线程安全,JWTKeyCollection
是一个 actor
。这意味着它的所有方法都是 async
的,并且必须被 await
。
我们可以生成 JWT,也称为签名。为了演示这一点,让我们创建一个 payload。payload 类型的每个属性都对应于令牌中的一个声明 (claim)。 JWTKit 为 RFC 7519 指定的所有声明提供了预定义的类型,以及一些方便的类型,用于处理自定义声明。对于示例令牌,payload 看起来像这样
struct ExamplePayload: JWTPayload {
var sub: SubjectClaim
var exp: ExpirationClaim
var admin: BoolClaim
func verify(using key: some JWTAlgorithm) throws {
try self.exp.verifyNotExpired()
}
}
// Create a new instance of our JWTPayload
let payload = ExamplePayload(
subject: "vapor",
expiration: .init(value: .distantFuture),
isAdmin: true
)
然后,将 payload 传递给 JWTKeyCollection.sign
。
// Sign the payload, returning the JWT as String
let jwt = try await keys.sign(payload, kid: "my-key")
print(jwt)
在这里,我们向 JWT 添加了一个自定义标头。任何键值对都可以添加到标头中。在这种情况下,kid
将用于从 JWTKeyCollection
中查找正确的密钥以进行验证。
您应该看到打印出的 JWT。这可以反馈到 verify
方法中以访问 payload。
让我们尝试验证以下示例 JWT
let exampleJWT = """
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo
"""
您可以通过访问 jwt.io 并将令牌粘贴到调试器中来检查此令牌的内容。在“Verify Signature”部分中将密钥设置为 secret
。
要验证令牌,必须知道 payload 的格式。在这种情况下,我们知道 payload 的类型为 ExamplePayload
。使用此 payload,JWTKeyCollection
对象可以处理和验证示例 JWT,并在成功时返回其 payload
// Parse the JWT, verify its signature and decode its content
let payload = try await keys.verify(exampleJWT, as: ExamplePayload.self)
print(payload)
如果一切正常,此代码将打印如下内容
TestPayload(
sub: SubjectClaim(value: "vapor"),
exp: ExpirationClaim(value: 4001-01-01 00:00:00 +0000),
admin: BoolClaim(value: true)
)
注意
示例 payload 的 admin
属性不必使用 BoolClaim
类型;简单的 Bool
也可以工作。JWTKit 提供了 BoolClaim
类型,是为了方便处理许多将布尔值编码为 JSON 字符串(例如 "true"
和 "false"
)而不是使用 JSON 的 true
和 false
关键字的 JWT 实现。
JSON Web Key (JWK) 是一种 JavaScript 对象表示法 (JSON) 数据结构,用于表示加密密钥,在 RFC7517 中定义。这些通常用于为客户端提供用于验证 JWT 的密钥。例如,Apple 将其使用 Apple 登录 JWKS 托管在 URL https://appleid.apple.com/auth/keys
上。
您可以将此 JSON Web Key Set (JWKS) 添加到您的 JWTSigners
#if !canImport(Darwin)
import FoundationEssentials
#else
import Foundation
#endif
import JWTKit
let rsaModulus = "..."
let json = """
{
"keys": [
{"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"},
{"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"},
]
}
"""
// Create key collection and add JWKS
let keys = try await JWTKeyCollection().add(jwksJSON: json)
您现在可以将来自 Apple 的 JWT 传递给 verify
方法。JWT 标头中的密钥标识符 (kid
) 将用于自动选择正确的密钥进行验证。 JWKS 可能包含 JWTKit 支持的任何密钥类型。
HMAC 是最简单的 JWT 签名算法。它使用一个可以同时签名和验证令牌的密钥。密钥可以是任意长度。
要将 HMAC 密钥添加到密钥集合,请使用 addHS256
、addHS384
或 addHS512
方法
// Add HMAC with SHA-256 signer.
await keys.add(hmac: "secret", digestAlgorithm: .sha256)
重要提示
密码学是一个复杂的主题,算法的选择会直接影响数据的完整性、安全性和隐私性。本 README 不试图对这些问题进行有意义的讨论;包作者建议在做出最终决定之前进行您自己的研究。
ECDSA 是一种基于椭圆曲线密码学的现代非对称算法。它使用公钥来验证令牌,并使用私钥来签名令牌。
您可以使用 PEM 文件加载 ECDSA 密钥
let ecdsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"
// Initialize an ECDSA key with public pem.
let key = try ES256PublicKey(pem: ecdsaPublicKey)
获得 ECDSA 密钥后,您可以使用以下方法将其添加到密钥集合中
addES256
:使用 SHA-256 的 ECDSAaddES384
:使用 SHA-384 的 ECDSAaddES512
:使用 SHA-512 的 ECDSA// Add ECDSA with SHA-256 algorithm
await keys.add(ecdsa: key)
EdDSA 是一种现代算法,被认为比 RSA 和 ECDSA 更安全。它基于 Edwards 曲线数字签名算法。JWTKit 当前唯一支持的曲线是 Ed25519。
您可以使用其坐标创建 EdDSA 密钥
// Initialize an EdDSA key with public PEM
let publicKey = try EdDSA.PublicKey(x: "...", curve: .ed25519)
// Initialize an EdDSA key with private PEM
let privateKey = try EdDSA.PrivateKey(x: "...", d: "...", curve: .ed25519)
// Add public key to the key collection
await keys.add(eddsa: publicKey)
// Add private key to the key collection
await keys.add(eddsa: privateKey)
RSA 是一种非对称算法。它使用公钥来验证令牌,并使用私钥来签名令牌。
警告
不再建议新应用程序使用 RSA。如果可能,请改用 EdDSA 或 ECDSA。Infosec Insights 2020 年 6 月的博客文章 “ECDSA vs RSA: Everything You Need to Know” 详细讨论了两者之间的差异。
要创建 RSA 签名器,首先初始化一个 RSAKey
。这可以通过传入组件来完成
// Initialize an RSA key with components.
let key = try Insecure.RSA.PrivateKey(
modulus: "...",
exponent: "...",
privateExponent: "..."
)
相同的初始化器可以用于没有 privateExponent
参数的公钥。
您也可以选择加载 PEM 文件
let rsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----"
// Initialize an RSA key with public PEM
let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)
使用 Insecure.RSA.PrivateKey(pem:)
加载私有 RSA pem 密钥,使用 Insecure.RSA.PublicKey(certificatePEM:)
加载 X.509 证书。获得 RSA 密钥后,您可以根据摘要和填充,使用专用方法将其添加到密钥集合中
// Add RSA with SHA-256 algorithm
await keys.add(rsa: key, digestAlgorithm: .sha256)
// Add RSA with SHA-512 and PSS padding algorithm
await keys.add(pss: key, digestAlgorithm: .sha512)
JWTKit 包含了几个助手,用于实现 RFC § 4.1 定义的“标准” JWT 声明
声明 (Claim) | 类型 | 验证方法 |
---|---|---|
aud |
AudienceClaim |
verifyIntendedAudience(includes:) |
exp |
ExpirationClaim |
verifyNotExpired(currentDate:) |
jti |
IDClaim |
n/a |
iat |
IssuedAtClaim |
n/a |
iss |
IssuerClaim |
n/a |
nbf |
NotBeforeClaim |
verifyNotBefore(currentDate:) |
sub |
SubjectClaim |
n/a |
在可能的情况下,应在 verify(using:)
方法中验证 payload 的所有声明;那些没有自己的验证方法的声明可以手动验证。
为 RFC 未定义的常见声明类型提供了额外的助手
BoolClaim
:可用于任何值是布尔标志的声明。将识别布尔 JSON 值和字符串 "true"
和 "false"
。GoogleHostedDomainClaim
:用于 GoogleIdentityToken
供应商令牌类型。JWTMultiValueClaim
:用于声明的协议,例如 AudienceClaim
,它可以选择性地编码为具有多个值的数组。JWTUnixEpochClaim
:用于声明的协议,例如 ExpirationClaim
和 IssuedAtClaim
,其值是自 UNIX 纪元(1970 年 1 月 1 日午夜)以来的秒数计数。LocaleClaim
:声明的值是 BCP 47 语言标记。也由 GoogleIdentityToken
使用。JWTParser
和 JWTSerializer
协议允许您为您的 payload 类型定义自定义解析和序列化。当您需要使用非标准的 JWT 格式时,这非常有用。
例如,您可能需要将 b64
标头设置为 false,这不会对 payload 进行 base64 编码。您可以创建自己的 JWTParser
和 JWTSerializer
来处理此问题。
struct CustomSerializer: JWTSerializer {
// Here you can set a custom encoder or just leave this as default
var jsonEncoder: JWTJSONEncoder = .defaultForJWT
// This method should return the payload in the way you want/need it
func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -> Data {
// Check if the b64 header is set. If it is, base64URL encode the payload, don't otherwise
if header.b64?.asBool == true {
try Data(jsonEncoder.encode(payload).base64URLEncodedBytes())
} else {
try jsonEncoder.encode(payload)
}
}
}
struct CustomParser: JWTParser {
// Here you can set a custom decoder or just leave this as default
var jsonDecoder: JWTJSONDecoder = .defaultForJWT
// This method parses the token into a tuple containing the various token's elements
func parse<Payload>(_ token: some DataProtocol, as: Payload.Type) throws -> (header: JWTHeader, payload: Payload, signature: Data) where Payload: JWTPayload {
// A helper method is provided to split the token correctly
let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token)
// The header is usually always encoded the same way
let header = try jsonDecoder.decode(JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes()))
// If the b64 header field is non present or true, base64URL decode the payload, don't otherwise
let payload = if header.b64?.asBool ?? true {
try jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes()))
} else {
try jsonDecoder.decode(Payload.self, from: .init(encodedPayload))
}
// The signature is usually also always encoded the same way
let signature = Data(encodedSignature.base64URLDecodedBytes())
return (header: header, payload: payload, signature: signature)
}
}
然后像这样使用它们
let keyCollection = await JWTKeyCollection().add(
hmac: "secret",
digestAlgorithm: .sha256,
parser: CustomParser(),
serializer: CustomSerializer()
)
let payload = TestPayload(sub: "vapor", name: "Foo", admin: false, exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)))
let token = try await keyCollection.sign(payload, header: ["b64": true])
如果您不需要指定自定义解析和序列化,但您确实需要使用自定义 JSON 编码器或解码器,则可以使用 DefaultJWTParser
和 DefaultJWTSerializer
类型来创建具有自定义 JSON 编码器和解码器的 JWTKeyCollection
。
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let parser = DefaultJWTParser(jsonDecoder: decoder)
let serializer = DefaultJWTSerializer(jsonEncoder: encoder)
let keyCollection = await JWTKeyCollection().add(
hmac: "secret",
digestAlgorithm: .sha256,
parser: parser,
serializer: serializer
)
使用 SwiftPM 在您的包上运行以下命令,将 MyTarget
替换为您的目标名称
cd /path/to/project/root/directory
swift package add-dependency https://github.com/vapor/jwt-kit.git --from 5.0.0
swift package add-target-dependency JWTKit MyTarget
或手动将以下内容添加到您的 Package.swift
文件中
dependencies: [
.package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0")
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.target(name: "JWTKit"),
]),
]