跳到内容

JWT#

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑而独立的方式,用于在各方之间作为 JSON 对象安全地传输信息。此信息可以被验证和信任,因为它经过数字签名。

JWT 在 Web 应用程序中特别有用,通常用于无状态的身份认证/授权以及信息交换。你可以在上面链接的规范或 jwt.io 上阅读更多关于 JWT 背后原理的信息。

Vapor 通过 JWT 模块为 JWT 提供了一流的支持。该模块构建在 JWTKit 库之上,这是一个基于 SwiftCrypto 的 JWT 标准的 Swift 实现。JWTKit 为多种算法提供了签名者和验证者,包括 HMAC、ECDSA、EdDSA 和 RSA。

入门#

在 Vapor 应用程序中使用 JWT 的第一步是将 JWT 依赖项添加到项目的 Package.swift 文件中:

// swift-tools-version:5.10
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...
    ]
)

配置#

添加依赖项之后,你就可以开始在应用程序中使用 JWT 模块了。JWT 模块在 Application 中新增了一个 jwt 属性用于配置,其内部实现由 JWTKit 库提供。

密钥集合#

jwt 对象带有一个 keys 属性,它是 JWTKit 的 JWTKeyCollection 的实例。该集合用于存储和管理用于签名和验证 JWT 的密钥。JWTKeyCollection 是一个 actor,这意味着对该集合的所有操作都是串行化且线程安全的。

要签名或验证 JWT,你需要向该集合添加一个密钥。这通常在 configure.swift 文件中完成:

import JWT

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

这会向密钥链添加一个以 SHA-256 作为摘要算法的 HMAC 密钥,在 JWA 表示法中即为 HS256。有关可用算法的更多信息,请查看下方的算法部分。

Note

请务必将 "secret" 替换为实际的密钥。该密钥应妥善保管,最好放在配置文件或环境变量中。

签名#

添加好的密钥即可用于对 JWT 进行签名。为此,你首先需要一些东西来签名,即一个「payload」。 这个 payload 就是一个包含你想要传输的数据的 JSON 对象。你可以通过让你的结构体遵循 JWTPayload 协议来创建自定义 payload:

// JWT payload structure.
struct TestPayload: JWTPayload {
    // Maps the longer Swift property names to the
    // shortened keys used in the JWT payload.
    enum CodingKeys: String, CodingKey {
        case subject = "sub"
        case expiration = "exp"
        case isAdmin = "admin"
    }

    // The "sub" (subject) claim identifies the principal that is the
    // subject of the JWT.
    var subject: SubjectClaim

    // The "exp" (expiration time) claim identifies the expiration time on
    // or after which the JWT MUST NOT be accepted for processing.
    var expiration: ExpirationClaim

    // Custom data.
    // If true, the user is an admin.
    var isAdmin: Bool

    // Run any additional verification logic beyond
    // signature verification here.
    // Since we have an ExpirationClaim, we will
    // call its verify method.
    func verify(using algorithm: some JWTAlgorithm) async throws {
        try self.expiration.verifyNotExpired()
    }
}

对 payload 签名是通过调用 JWT 模块的 sign 方法完成的,例如在路由处理程序中:

app.post("login") { req async throws -> [String: String] in
    let payload = TestPayload(
        subject: "vapor",
        expiration: .init(value: .distantFuture),
        isAdmin: true
    )
    return try await ["token": req.jwt.sign(payload)]
}

当向该端点发起请求时,它会在响应体中以 String 形式返回已签名的 JWT,如果一切顺利,你会看到类似这样的内容:

{
   "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo"
}

你可以使用 jwt.io 调试器对该令牌进行解码和验证。调试器会显示 JWT 的 payload(应该是你之前指定的数据)和 header,并且你可以使用签名 JWT 所用的密钥来验证签名。

验证#

当令牌被发送你的应用程序时,你可以通过调用 JWT 模块的 verify 方法来验证该令牌的真实性:

// Fetch and verify JWT from incoming request.
app.get("me") { req async throws -> HTTPStatus in
    let payload = try await req.jwt.verify(as: TestPayload.self)
    print(payload)
    return .ok
}

req.jwt.verify 辅助函数会检查 Authorization 请求头中的不记名令牌。如果存在,它将解析该 JWT 并验证其签名和声明。如果这些步骤中的任何一个失败,将抛出 401 Unauthorized 错误。

通过发送以下 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中找到。

算法(Algorithms)#

JWT 可以使用多种算法进行签名。

要向密钥链添加密钥,以下每种算法都提供了一个 add 方法的重载:

HMAC#

HMAC (基于哈希的消息认证码) 是一种对称算法,使用一个密钥对 JWT 进行签名和验证。Vapor 支持以下 HMAC 算法:

  • HS256:带有 SHA-256 的 HMAC

  • HS384:带有 SHA-384 的 HMAC

  • HS512:带有 SHA-512 的 HMAC

// Add an HS256 key.
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)

ECDSA#

ECDSA (椭圆曲线数字签名算法) 是一种非对称算法,使用一对公钥/私钥对 JWT 进行签名和验证。它依赖于椭圆曲线相关的数学原理。Vapor 支持以下 ECDSA 算法:

  • ES256:使用 P-256 曲线和 SHA-256 的 ECDSA

  • ES384:使用 P-384 曲线和 SHA-384 的 ECDSA

  • ES512:使用 P-521 曲线和 SHA-512 的 ECDSA

所有算法都提供公钥和私钥,例如 ES256PublicKeyES256PrivateKey。你可以使用 PEM 格式添加 ECDSA 密钥:

let ecdsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""

// Initialize an ECDSA key with public PEM.
let key = try ES256PublicKey(pem: ecdsaPublicKey)

或者生成随机密钥(对测试很有用):

let key = ES256PrivateKey()

将密钥添加到密钥链:

await app.jwt.keys.add(ecdsa: key)

EdDSA#

EdDSA (爱德华兹曲线数字签名算法) 是一种非对称算法,使用一对公钥/私钥对 JWT 进行签名和验证。它与 ECDSA 类似,两者都依赖于 DSA 算法,但 EdDSA 基于爱德华兹曲线,这是另一族椭圆曲线,并且在性能上略有提升。不过它也更新,因此支持范围较窄。Vapor 只支持使用 Ed25519 曲线的 EdDSA 算法。

你可以使用其(base-64 编码的 String)坐标来创建 EdDSA 密钥,如果是公钥则用 x,如果是私钥则用 d

let publicKey = try EdDSA.PublicKey(x: "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE", curve: .ed25519)

let privateKey = try EdDSA.PrivateKey(d: "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=", curve: .ed25519)

你也可以生成随机密钥:

let key = EdDSA.PrivateKey(curve: .ed25519)

将密钥添加到密钥链:

await app.jwt.keys.add(eddsa: key)

RSA#

RSA (Rivest-Shamir-Adleman) 是一种非对称算法,使用一对公钥/私钥对 JWT 进行签名和验证。

Warning

如你所见,RSA 密钥被置于 Insecure 命名空间之下,以阻止新用户使用它们。这是因为 RSA 被认为不如 ECDSA 和 EdDSA 安全,应仅出于兼容性原因使用。 如果可能,请改用其他算法。

Vapor 支持以下 RSA 算法:

  • RS256:带有 SHA-256 的 RSA

  • RS384:带有 SHA-384 的 RSA

  • RS512:带有 SHA-512 的 RSA

你可以使用 PEM 格式创建 RSA 密钥:

let rsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx
PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv
7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/
aX4rbSL49Z3dAQn8vQIDAQAB
-----END PUBLIC KEY-----
"""

// Initialize an RSA key with public pem.
let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey)

或者使用其各个组成部分:

// Initialize an RSA private key with components.
let key = try Insecure.RSA.PrivateKey(
    modulus: modulus, 
    exponent: publicExponent, 
    privateExponent: privateExponent
)

Warning

该软件包不支持小于 2048 位的 RSA 密钥。

然后你可以将密钥添加到密钥集合中:

await app.jwt.keys.add(rsa: key, digestAlgorithm: .sha256)

PSS#

除了 RSA-PKCS1v1.5 算法之外,Vapor 还支持 RSA-PSS 算法。PSS (概率签名方案) 是一种更安全的 RSA 签名填充方案。建议在可能的情况下优先使用 PSS 而非 PKCS1v1.5。

该算法仅在签名阶段有所不同,这意味着密钥与 RSA 相同,不过在将其添加到密钥集合时,你需要指定填充方案:

await app.jwt.keys.add(pss: key, digestAlgorithm: .sha256)

密钥标识符 (kid)#

向密钥集合添加密钥时,你还可以指定一个密钥标识符 (kid)。这是该密钥的唯一标识符,可用于在集合中查找该密钥。

// Add HMAC with SHA-256 key named "a".
await app.jwt.keys.add(hmac: "foo", digestAlgorithm: .sha256, kid: "a")

如果你不指定 kid,该密钥将被指定为默认密钥。

Note

如果你添加另一个不带 kid 的密钥,默认密钥将会被覆盖。

在对 JWT 签名时,你可以指定要使用的 kid

let token = try await req.jwt.sign(payload, kid: "a")

而在验证时,kid 会自动从 JWT 的 header 中提取出来,并用于在集合中查找相应的密钥。verify 方法上还有一个 iteratingKeys 参数,用于指定当找不到对应的 kid 时,是否遍历集合中的所有密钥。

声明(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) 是一种表示密钥的 JSON 数据结构 (RFC7517)。这些密钥通常用于向客户端提供用于验证 JWT 的密钥。

例如,Apple 将他们的 Sign in with Apple JWKS 托管在以下 URL 中。

GET https://appleid.apple.com/auth/keys

Vapor 提供了将 JWK 添加到密钥集合的工具:

let privateKey = """
{
    "kty": "RSA",
    "d": "\(rsaPrivateExponent)",
    "e": "AQAB",
    "use": "sig",
    "kid": "1234",
    "alg": "RS256",
    "n": "\(rsaModulus)"
}
"""

let jwk = try JWK(json: privateKey)
try await app.jwt.keys.use(jwk: jwk)

这会将该 JWK 添加到密钥集合中,之后你就可以像使用其他密钥一样用它来签名和验证 JWT。

JWKs#

如果你有多个 JWK,你同样可以将它们添加进来:

let json = """
{
    "keys": [
        {"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"},
        {"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"},
    ]
}
"""

try await app.jwt.keys.use(jwksJSON: json)

发行商(Vendors)#

Vapor 提供了用于处理来自以下热门发行商的 JWT 的 API。

Apple#

首先,配置你的 Apple 应用程序标识符。

// Configure Apple app identifier.
app.jwt.apple.applicationIdentifier = "..."

然后,使用 req.jwt.apple 辅助函数获取并验证 Apple JWT。

// Fetch and verify Apple JWT from Authorization header.
app.get("apple") { req async throws -> HTTPStatus in
    let token = try await req.jwt.apple.verify()
    print(token) // AppleIdentityToken
    return .ok
}

Google#

首先,配置你的 Google 应用标识符和 G Suite 域名。

// Configure Google app identifier and domain name.
app.jwt.google.applicationIdentifier = "..."
app.jwt.google.gSuiteDomainName = "..."

然后,使用 req.jwt.google 辅助函数获取并验证 Google JWT。

// Fetch and verify Google JWT from Authorization header.
app.get("google") { req async throws -> HTTPStatus in
    let token = try await req.jwt.google.verify()
    print(token) // GoogleIdentityToken
    return .ok
}

Microsoft#

首先,配置你的 Microsoft 应用程序标识符。

// Configure Microsoft app identifier.
app.jwt.microsoft.applicationIdentifier = "..."

然后,使用 req.jwt.microsoft 辅助函数获取并验证 Microsoft JWT。

// Fetch and verify Microsoft JWT from Authorization header.
app.get("microsoft") { req async throws -> HTTPStatus in
    let token = try await req.jwt.microsoft.verify()
    print(token) // MicrosoftIdentityToken
    return .ok
}
编辑此页