• 亮色
  • 深色
  • 自动
  • RSS 订阅

    RFC 7519

    2024-06-06

    关键字

    JWT声明(claims)载荷(payload)JWSJWEHash-based Message Authentication Code(HMAC)Message Authentication Code(MAC)

    声明和载荷的含义有些类似,或许可以理解为同一含义。

    HMACMAC 的一种具体实现。

    摘要

    JSON Web Token (JWT) 是一种紧凑、URL 安全的方式,用于双方传输声明(claims)。JWT 中的 claims 被编码为一个 JSON 对象,此 JSON 对象常作为 JSON Web Signature (JWS) 结构的载荷(payload)或者是 JSON Web Encryption (JWE) 结构的明文,从而使得声明 claims 可以被数字签名,或者通过 Message Authentication CodeMAC)加密(加密是可选的)进行保护的目的。

    可以认为 JWT 包含了 JWS 和 JWE

    介绍

    JSON Web Token (JWT) 是一种紧凑的声明的表示形式,旨在用于空间受限的环境,例如 HTTP Authorization Header 和 URI 查询参数。JWT 总是使用 JWS 紧凑序列化或 JWE 紧凑序列化来表示。

    JWT 的发音与英语单词 “jot” 相同。

    术语

    • JSON Web Token(JWT):将一组 claims 表示为 JSON 的字符串,字符串以 JWS 或 JWE 进行编码,使 claims 能够数字化签名或通过 MAC 完整性保护或加密。
    • JWT Claims Set:包含 JWT claims 的 JSON 对象。是 JWT 的核心内容。
    • Claim:关于主体的一条信息声明。声明表示为一个由声明名称(Claim Name)和声明值(Claim Value)组成的键值对。
    • Claim Name:声明表示中的名称部分。始终是字符串。
    • Claim Value:声明表示中的值部分。可以是任何 JSON 值。
    • Nested JWT:使用嵌套签名和 / 或加密的 JWT。
    • Unsecured JWT:其声明没有经过完整性保护或加密的 JWT。
    • Collision-Resistant Name:一种名称,它所属的命名空间确保名称分配时不可能与其他名称冲突。例如,域名、对象标识符(OID)和通用唯一标识符(UUID)。
    • StringOrURI:一个字符串值,如果包含 “:” 字符,则值必须是 URI。
    • NumericDate:JSON 数值,表示从 1970 年 1 月 1 日 00:00:00 UTC 到指定的 UTC 日期 / 时间的 秒数,不考虑闰秒。

    JWT

    (省略掉一些 摘要 里已经说过的东西)

    JOSE 头描述了应用于 JWT claims 的加密操作。如果 JOSE 头是用于 JWS 的,则 JWT 被表示为 JWS,并且 claims 被数字签名或 MAC 签名。如果 JOSE 头是用于 JWE 的,则 JWT 被表示为 JWE,并且 claims 被加密。

    JOSE 头包含了有关 JWT 如何处理的信息,如算法类型和密钥信息。

    JWT 被表示为一系列 URL 安全的部分,通过句点(.)字符分隔。每个部分包含一个 base64url 编码的值。

    JWT 的编码格式取决于最终 JWS 或 JWE 的表示方式,可以使用 JWS Compact Serialization 或 JWE Compact Serialization 进行表示。

    JWT 可以包含在另一个 JWS 或 JWE 结构中,形成嵌套 JWT,从而实现嵌套的签名和加密操作。

    JWT 示例

    • JOSE 头。这个 JOSE 表明编码对象是个 JWT,且 JWT 是使用 MAC SHA-256 算法进行 MAC 签名的 JWS:

      {
        "typ": "JWT",
        "alg": "HS256"
      }
      

      其 base64 编码如下:

      eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
      
    • JWT Claims Set:

      {
        "iss": "joe",
        "exp": 1300819380,
        "http://example.com/is_root": true
      }
      

      其 base64 编码如下:

      eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
      
    • 使用 HMAC SHA-256 算法计算 base64 编码后的 JOSE Header 和 base64 编码后的 JWS Payload 的 MAC 值,再将得出的 HMAC(MAC) 值进行 base64url 编码,得到 JWS 签名,像这样:

      dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
      

    最终的 JWT 类似如下:

    eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
    .
    eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
    .
    dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    

    JWT Claims

    JWT Claims Set 是一个 JSON 对象,其中键必须是唯一的,如果存在重复的键,那么整个 JWT 都应该被拒绝或是只取最后一个重复的成员。

    其中的内容必须是与上下文相关的,不同的应用场景可能对 JWT 的声明内容有不同的要求。

    JWT Claims Set 名称分为如下三类。

    1. Registered Claim Names

    这一类的 claims 名称已经在 IANA(Internet Assigned Numbers Authority)建立的 “JSON Web Token Claims” 注册表 中注册。

    这些 claims 名称的定义不是说在所有情况下都强制使用或实现,只是提供一组有用的、可互操作的声明的起点。使用 JWT 的应用程序应该定义它们使用的特定 claims 以及它们何时是必需的或可选的。这些名称都比较简短,因为 JWT 的一个核心目标是让 claims 尽可能轻量级。

    “iss” (Issuer) Claim

    • 可选的
    • StringOrURI 值

    iss 用于标识发行 JWT 的主体。通常是特定于应用程序的。iss 的值是一个区分大小写的字符串。

    “sub” (Subject) Claim

    • 可选的
    • StringOrURI 值

    sub 用于标识 JWT 的主题主体。必须在 JWT 颁发方的作用域里是唯一的(唯一 ID)。该声明的处理通常是特定于应用程序的。

    “aud” (Audience) Claim

    • 可选的
    • StringOrURI 数组值

    aud 用于标识 JWT 颁发的对象(接收者)。

    使用 JWT 的主体都应该在 claims 中用 aud 这个值来标识自己。以便在接收到 JWT 后判断此 JWT 是不是发给自己的。

    如果 JWT 里的 aud 没有包含当前接收到此 JWT 的主体的标识,那么此 JWT 应该被接收此 JWT 的主体丢弃,因为此 JWT 不是为其生成的。

    主要是为了验证接收方,防止被攻击者恶意地重放给其他地方。此外也可以允许多个接收方,使得 JWT 可以在不同的应用、服务或系统间重用,而且也可以更好地控制 JWT 传播和使用范围。

    “exp” (Expiration Time) Claim

    • 可选的
    • NumericDate

    exp 用于标识 JWT 的过期时间。在这段时间过后(now >= exp),JWT 不能再使用。

    实现者可以为时钟偏差提供一些小的余地,通常不超过几分钟,以考虑时钟之间的不准确性。

    如果 JWT 中没有这个值,那么处理方就无法验证 JWT 的过期状态,可能会导致 已经过期的 JWT 被使用或接受处理。

    “nbf” (Not Before) Claim

    • 可选的
    • NumericDate

    nbf 用来标识 JWT 的启用时间。在这段时间过后(now >= nbf),JWT 才能被使用。

    实现者可以为时钟偏差提供一些小的余地,通常不超过几分钟,以考虑时钟之间的不准确性。

    “iat” (Issued At) Claim

    • 可选的
    • NumericDate

    iat 用于标识 JWT 的发行时间(生成时间)。可以帮助使用方确定 JWT 的可用时间。

    “jti” (JWT ID) Claim

    • 可选的
    • unique

    jti 用于提供 JWT 的唯一标识符,以区分不同的 JWT 实例。

    这个标识必须确保不可能将相同的值意外地分配给不同的数据对象。如果程序有多个 JWT 颁发者,那么不同的颁发者之间必须避免碰撞。

    这是为了防止 JWT 被重放。通过在 JWT 中包含唯一的 jti 值,可以确保在一段时间内不会重复的产生相同的 JWT,从而增加了 JWT 的安全性。此外还可以通过 jti 来区分不同的 JWT 实例(虽然也有其他方法可以区分)。

    2. Public Claim Names

    这类 claims 必须满足以下条件之一:

    • 已经存在于 JSON Web Token (JWT)
    • 能够避免与其他 claims 产生冲突,确保名称的唯一性

    3. Private Claim Names

    这类 claims 可以不存在于 JSON Web Token (JWT) 上,也非 2. Public Claim Names 中所说的能够避免与其他 claims 产生冲突。与 2. Public Claim Names 不同,Private Claim Names 可能存在碰撞,并且应谨慎使用。

    使用时应该谨慎,尽量避免碰撞,并确保 JWT 的正确解析和处理。

    JOSE Header

    JOSE Header 里包含的 JSON 对象成员描述了应用于 JWT 的加密操作。这些操作确保了 JWT 的安全性和完整性。加密操作包括签名(JWS)或加密(JWE)。

    除了描述加密操作外,还可以包含 JWT 的其他附加属性。

    “typ” (Type) Header Parameter

    • 可选的
    • 参数值:JWT

    typ 参数用于 JWT 应用程序声明 JWT 的类型。

    如果一个请求里同时包含多种类型的数据,例如同时接收 jsonxmljwt 格式,像下面这样的请求体:

    {
      "dataObjects": [
        {
          "type": "json",
          "content": {
            "name": "Alice",
            "email": "alice@example.com"
          }
        },
        {
          "type": "xml",
          "content": "<user><name>Bob</name><email>bob@example.com</email></user>"
        },
        {
          "type": "jwt",
          "content": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJleGFtcGxlQGV4YW1wbGUuY29tIiwiYXVkIjoidXNlcnMiLCJleHAiOjE2MTYyMzkwMjIsImlhdCI6MTYxNjIzNTQyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
        }
      ]
    }
    

    那么就可以通过 typejwt 来判断哪些是 jwt 对象,但是为了向后兼容,推荐是全大写形式的 JWT

    如果明确知道对象是 JWT,那么可以不使用这个参数。

    “cty” (Content Type) Header Parameter

    • 可选的
    • 如果存在嵌套签名(JWS)或是嵌套加密(JWE),则必带此参数,否则不推荐使用
    • 参数值:JWT

    这个参数主要是用来传达 JWT 的结构信息。

    假设本身有个 JWT,此 JWT 内另外包含了 JWT。那么此时外部的 JWT 的 JOSE Header 可能如下所示:

    {
      "alg": "RS256",
      "cty": "JWT"
    }
    

    而内部的则是:

    {
      "alg": "HS256"
    }
    

    应用程序里,处理这样的 JWT 时,首先处理外部的 JWT 的 Header,检查 ctx 参数,若是 JWT,则将 payload 解析为嵌套的 JWT,并进行相应的处理。

    Replicating Claims as Header Parameters

    这里说明了如何在使用加密 JWT 的应用里,处理没有(无法)加密的 claims。

    有些程序里,某些 claims 以未加密的形式表示。这些未加密的 claims 可以帮助应用程序在解密 JWT 之前,根据这些 claims 决定是否处理或者如何处理 JWT。

    例如,一个应用程序可能需要在解密 JWT 之前,根据某些 claims(如 isssubaud)的值来决定是否接受这个 JWT。

    假设一个应用程序收到一个加密的 JWT(JWE),其中包含一些敏感的 claims。但是为了提高效率和安全性,应用程序希望在解密之前先根据一些基本的声明(如 iss(颁发者)、sub(主题)和 aud(受众))来决定是否进一步处理这个 JWT。为了实现这一点,这些声明可以以未加密的形式包含在 JWT 的头部参数中。

    Tip

    有些时候可能会把身份验证强相关,但是不希望 JWT 接收者看到的东西(例如 3. Private Claim Names 定义的东西)一起放到 claims 里,并进行加密处理,但是接收者为了判断是不是颁发给自己的 JWT,又需要 claims 里一些内容,就可以把这些内容放到 JOSE Header 里。

    示例:

    const jwe = require('jose')
    
    // JWT 声明集
    const payload = {
      iss: 'exampleIssuer',
      sub: 'exampleSubject',
      aud: 'exampleAudience',
      exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 小时过期
    }
    
    // JOSE Header
    const header = {
      alg: 'RSA-OAEP',
      enc: 'A256GCM',
      iss: payload.iss,
      sub: payload.sub,
      aud: payload.aud
    }
    
    // 加密 JWT
    const encryptedJWT = jwe.JWE.encrypt(JSON.stringify(payload), publicKey, {
      alg: header.alg,
      enc: header.enc,
      iss: header.iss,
      sub: header.sub,
      aud: header.aud
    })
    
    console.log(encryptedJWT)
    

    不安全的 JWT

    有时候可能 JWT 的内容会以 JWT 以外(非 JWS 和 JWE)的形式进行了加密或是通过其他方式确保了安全,这时就可能省略 JWT 内部的签名或加密操作。此时的 JWT 的 JOSE Header 的 alg 参数值应该是 "none",表示不使用任何签名算法。而 JWS Signature 签名部分则为空字符串。

    以这种方式创建的 JWT 称为 不安全的 JWT。

    像下面这样:

    eyJhbGciOiJub25lIn0.eyJpc3MiOiJleGFtcGxlSXNzdWVyIiwic3ViIjoiZXhhbXBsZVN1YmplY3QiLCJhdWQiOiJleGFtcGxlQXVkaWVuY2UiLCJleHAiOjE2MjI0NzA0MjB9.
    

    这类 JWT 主要用于如下场景:

    1. JWT 本身被包含在一个更大的数据结构中,而那个数据结构已经通过其他方式进行了加密或是签名,此时 JWT 就不需要再进行签名。
    2. 如果 JWT 的传输通道已经足够安全,例如能够保证仅使用 HTTPS 进行传输,且使用者能够确保 JWT 的完整性以及真实性,也可以使用不安全的 JWT。
    3. 对一些性能要求极高的场景,由于签名和加密可能引发较大的计算开销,如果能够确保 JWT 的安全性通过其他方式得到保证,也可以选择不安全的 JWT 来提高性能。

    JWT 的创建与验证

    创建 JWT

    1. 创建所需声明的 claims set
    2. 将 claims set 的内容转化为字节序列
    3. 创建 JOSE Header,其中内容必须符合 JWSJWE 标准
    4. 根据第三步执行下面其中一个步骤
      1. 如果是 JWS,那么则使用第 2 步的字节序列作为 payload 创建 JWS。创建步骤必须遵循 JWS 标准
      2. 如果是 JWE,那么则使用第 2 步的字节序列作为明文创建 JWE。创建步骤必须遵循 JWE 标准
    5. 可选的,嵌套签名或是加密操作
    6. 将生成的 JWS 或 JWE 作为最终的 JWT

    验证 JWT

    1. 确保 JWT 至少包含一个 ".",以便能够分割 Header、Payload 和 Signature
    2. 提取编码的 JOSE Header
    3. 使用 base64url 解码提取到的 JOSE Header,确保没有换行符、空白符或其他额外字符(后面这个确保,在具有 JSON 格式解析的语言里应该不需要额外操作了)
    4. 验证解码后的八位字节序列是否符合本标准的有效 JSON 对象
    5. 验证 JSON Header 的内容只存在能够被理解的参数和值,可以指定不理解时被忽略的参数
    6. 确定 JWT 类型
    7. 根据 JWT 类型选择使用 JWS 或 JWE 规范进行验证
      1. 对于 JWS,按照 JWS 规范进行验证,将 JWS Payload 进行 base64url 解码得到消息
      2. 对于 JWE,按照 JWE 规范进行验证,将结果明文作为消息
    8. 检查 JOSE Header 内容是否存在 cty 内容为 "JWT",如果有,则表明此 JWT 为嵌套签名或嵌套加密的 JWT,于是将上一步取得的消息从步骤 1 开始执行
    9. 将消息进行 base74url 解码,确保没有换行符、空白符或其他额外字符串(后面这个确保,在具有 JSON 格式解析的语言里应该不需要额外操作了)
    10. 验证解码结果为有效的 JSON 对象,将结果作为 JWT Claims

    字符串比较规则

    这一节主要是说有些应用可能会在大小写敏感的内容里传递大小写不敏感的内容,所以应用可能需要有个约定,例如全转换成小写。

    只有 typcty 不需要遵循这个。

    实施要求

    JSON Web Algorithms 中指定的签名和 MAC 算法中,必须支持的算法只有 HMAC SHA-256(HS256)和 none,但是也建议支持如下算法:

    • SHA-256 哈希算法(RS256)
    • 椭圆曲线数字签名算法(ECDSA)
    • P-256 曲线
    • SHA-256 哈希算法 (“ES256”)

    对其他的算法和密钥大小的支持是可选的

    对加密 JWT 的支持是可选的。如果实现提供加密功能,则符合规范的实现必须实现 JWA 中指定的加密算法中的 RSAES-PKCS1-v1_5(使用 2048 位密钥)(RSA1_5)、AES 密钥包装(使用 128 位和 256 位密钥)(A128KWA256KW),以及使用 AES-CBC 和 HMAC SHA-2 的复合认证加密算法(A128CBC-HS256A256CBC-HS512)。强烈建议实现还支持使用椭圆曲线迪菲 - 赫尔曼 (ECDH) 短暂静态协议 (ECDH-ES) 来协商用于包装内容加密密钥的密钥 (ECDH-ES+A128KWECDH-ES+A256KW),以及使用 AES 在 Galois/Counter Mode (GCM) 中的 128 位和 256 位密钥 (A128GCMA256GCM)。对其他算法和密钥大小的支持是可选的。

    对于嵌套 JWT 的支持也是可选的。

    其他

    后面的内容貌似就和日常中 JWT 的使用和实现没什么关系。

    参考

    更多