浅谈 JWT (JSON Web Tokens)

· · 科技·工程

本文假定您有基础的 Web 知识,故不会对某些概念进行说明。

您亦可以在我的博客看到这篇文章。

JWT 是什么?

JWT,全称是 JSON Web Token,可是说是当下最流行的一种跨域的认证方案。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJHYXZpbiIsImlhdCI6MTc3NDk3MDgyMywiZXhwIjoxNzc0OTc0NDIzfQ.M1tneo2EbPuE5QLdDkGNJk0V9QQ2-ZOXYyRqqID5HH3mUqec4DHQsinG5F_ovDdKlsNeyg1vDnN5vDMf_tEocHIHiBCsfX14YuCWb6lRZ9prJkbfJHXwqGu94yPskGo5dfX7_D_lmNznAOloFK_0GEvhpNmIBrwe9bw0drRRTH8BPKmR_xscCSKT-mQxfbgXGSfKohxPnjhl9PZVnXe5q_2D5cVu3PIr6yeOmbAI-Ju8C1U9cC0DqM4P8m53WktbjyxW2k8uSIOvVOnaNIP7y5CMTmC-pG4JYMzHCxn5uMIZqw5Fw3FHR25i2MQfk5j9bs2n28qRl-gur-RCM3lwUg

上面就是一个符合规范的 JWT,其分为三个部分,用 . 隔开,依次是 Header,Payload 与 Signature。

Header 部分是一个 JSON 对象,存储了 JWT 的 Metadata,如下面所示。

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

其中的 alg 是用来签名的算法,其中默认算法是 HS256,也就是 HMAC SHA256,这里的 RS256 则是 RSA SHA256。有关签名算法会在后文探讨。

typ 则表示 Token 的类型,JWT 都统一写成 JWT

值得注意的是,我们最终拿到的 JWT 中这部分实际上是通过了 Base64URL 转换的。

Payload 部分解码如下。

{
  "sub": "Gavin",
  "iat": 1774970823,
  "exp": 1774974423
}

这里则是存放实际的数据,RFC 7519 实际上定义了七个官方字段,但我们也可以定义任何私有字段。

七个官方字段:

同样的,最终呈现在 JWT 中的信息经过了 Base64URL 转换。

Signature 的部分是对 Header 和 Payload 的签名。首先,指定一个(如果使用的是不对称加密算法,这里的两次应该是对)密钥,然后使用 Header 指定的加密算法将 Header.Payload 产生签名。

至此,我们就了解了一个 JWT 的组成。

注意,JWT 的 Payload 部分本身是不加密的,请勿在里面存放敏感信息。

Base64URL

前文频繁提到的 Base64URL 是 Base64 编码标准的一个变体,它的设计目的是使编码结果可以作为文件名或 URL 地址使用。在标准 Base64 字母表中包含了一些对于 URL 和文件名来说无效的字符,因此 Base64URL 进行了适当的修改以避免这些问题。

Base64URL 采用与标准 Base64 相同的算法,但在以下方面有所不同:

如何使用 JWT?

服务端签发 JWT 后,客户端与服务端通信只要带上 JWT 即可,一种普遍的做法是将 JWT 存放于请求头的 Authorization 里。

Authorization: Bearer <token>

服务端收到 Token 后,只需要拿密钥验证签名即可。

结束了?

遇到跨域处理,对称性加密就没那么好用了。这个时候,我们就需要用到非对称性加密。比如上文提到的 RS256 就是一种非对称性加密,即每次生成一对公私钥,使用私钥加密,公钥验证。

身份提供服务将公钥暴露出去,其他服务端接收到来自此服务的 Token 只需要找到公钥验证即可。

暴露公钥也有相关的规范,也就是 JWK,全称 JSON Web Key。

JWK 是用于表示加密密钥的基于 JSON 的格式,其格式也有 RFC 7517 规约。

其常见参数总结如下:

视密钥签名算法而定可能有其他参数。

例如在文章一开头给出的示例中,我们的 RS256 公钥如下。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArQOjkox78MYLBZEAhuOP
L2bTDulSpjl6G6qARp2/njwh+zvtHKGU63QwAhCtJS37k9rZio5tDbBmtlJwlrgw
f+UWgc7GtV9EC1Jgdwd6yGpsI8ZWs7UWH/eJDwi6NP2qbwBha7UtQyzf1cOfJk5y
lT6FyCsHGy1ssYiEK42keXd7RtuoS6C/QhfgWyf28Mwjj63TBmuOY6fZ8UPhvQCe
dmHkfW9tgWHOsz3qI5vKrY9MifDnh1wU6k6/A9MtXAKHd95aBEjHzF+y1rAly+rE
ZnefLvt/gvIqaoa0Yy2VpKqTSu17u+AoyDskUDolIN55w/2Z7OQR/8poHKTOSpwF
9wIDAQAB
-----END PUBLIC KEY-----

其对应的 JWK 则为下面所示。

{
    "kty": "RSA",
    "key_ops": ["verify"],
    "n": "vumBc5W0z-bDNVf-z_qR2qoey9EXp3YhsWCGBjbxxjfJvDUT9ptqENCxAOh4uZlAjWOkkUa0Ako104cY5A4myvrJbuxbLai7oknq6d5pBZxYJHlA5XroQ2vfDe3mcHUePcMIsSMo-hZQ5bcTvKxGW2idKuPWUxAUtnkxZgjF9uzh3IqpFHyZ5swN4zCZ89JLGUb7Vzb5bthziqm71cXpDQPx-RvJZ47DAQfioMplogRAh_VdnVPRknHxABbn9eTR_OhcxdhGTPSb4Bz-1iagZydyID4CPYOYRY9Y_7-q4c_zx1b-M8HHWmFpI3quN21N42LvAJK6nL7hGCQrm9m9lQ",
    "e": "AQAB",
    "kid": "key-H-_edi7gYRGFhkfiFHpUpw",
    "alg": "RS256",
    "use": "sig"
}

这里的 kid 是通过提取公钥的 DER 字节流,计算 SHA-256 哈希,然后转成 URL 安全的 Base64 字符串作为唯一的 kid。笔者查询了一下,并没有发现通用的 kid 生成规则,暂且这么办吧。

当需要将多个 JWK 组合在一起时,它们会被组织成一个 JSON Web 密钥 Set(JWKS),仅包含上述 JWK 的 JWKS 如下面所示。

{
  "keys": [
    {
      "kty": "RSA",
      "key_ops": [
        "verify"
      ],
      "n": "vumBc5W0z-bDNVf-z_qR2qoey9EXp3YhsWCGBjbxxjfJvDUT9ptqENCxAOh4uZlAjWOkkUa0Ako104cY5A4myvrJbuxbLai7oknq6d5pBZxYJHlA5XroQ2vfDe3mcHUePcMIsSMo-hZQ5bcTvKxGW2idKuPWUxAUtnkxZgjF9uzh3IqpFHyZ5swN4zCZ89JLGUb7Vzb5bthziqm71cXpDQPx-RvJZ47DAQfioMplogRAh_VdnVPRknHxABbn9eTR_OhcxdhGTPSb4Bz-1iagZydyID4CPYOYRY9Y_7-q4c_zx1b-M8HHWmFpI3quN21N42LvAJK6nL7hGCQrm9m9lQ",
      "e": "AQAB",
      "kid": "key-H-_edi7gYRGFhkfiFHpUpw",
      "alg": "RS256",
      "use": "sig"
    }
  ]
}

一般,我们使用 /.well-known/jwks.json 暴露 JWKS。

一些小技巧

阮一峰老师的博客写了几条 JWT 相关的特点,可以看看。

强制过期

有时候我们会遇到需要强制废除 Token 的场景,例如更改密码时。由于 JWT 是无状态的,我们需要额外的逻辑做到这点。

有一个很显然的方案是在服务端存一个 token_version,每次需要强制过期时只需把这个版本改掉,后面接到的 JWT 如果版本不同,判定为无效就行了。

但这个方案不好跨站,这里介绍一个很牛的方案。我们将原先的一个 token 拆分成两个,分别是 access_tokenrefresh_token。前者的有效期相对较短,一般为 15 分钟,且不用携带 version 信息,其他站点验证身份使用这个 token。refresh_token 的有效期则相对较长,并且需要携带 version 信息,用于向身份提供服务请求 access_token,服务器只需判断 refresh_tokenversion 版本是否过时即可。

完结撒花 o( ̄▽ ̄)ブ!