JWTKit

Documentation Team Chat MIT License Continuous Integration Swift 6.0+ SSWG Incubation Level: Graduated


🔑 使用 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 中所指定的那样。以下功能受到支持

以下算法,如 RFC 7518 § 3RFC 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

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 的 truefalse 关键字的 JWT 实现。

JWK

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

HMAC 是最简单的 JWT 签名算法。它使用一个可以同时签名和验证令牌的密钥。密钥可以是任意长度。

要将 HMAC 密钥添加到密钥集合,请使用 addHS256addHS384addHS512 方法

// Add HMAC with SHA-256 signer.
await keys.add(hmac: "secret", digestAlgorithm: .sha256)

重要提示

密码学是一个复杂的主题,算法的选择会直接影响数据的完整性、安全性和隐私性。本 README 不试图对这些问题进行有意义的讨论;包作者建议在做出最终决定之前进行您自己的研究。

ECDSA

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 密钥后,您可以使用以下方法将其添加到密钥集合中

// Add ECDSA with SHA-256 algorithm
await keys.add(ecdsa: key)

EdDSA

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 是一种非对称算法。它使用公钥来验证令牌,并使用私钥来签名令牌。

警告

不再建议新应用程序使用 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)

声明 (Claims)

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 未定义的常见声明类型提供了额外的助手

自定义解析和序列化

JWTParserJWTSerializer 协议允许您为您的 payload 类型定义自定义解析和序列化。当您需要使用非标准的 JWT 格式时,这非常有用。

例如,您可能需要将 b64 标头设置为 false,这不会对 payload 进行 base64 编码。您可以创建自己的 JWTParserJWTSerializer 来处理此问题。

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 编码器和解码器

如果您不需要指定自定义解析和序列化,但您确实需要使用自定义 JSON 编码器或解码器,则可以使用 DefaultJWTParserDefaultJWTSerializer 类型来创建具有自定义 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"),
    ]),
]