JSON Web Token

JSON Web Token（JWT，建议读音 dʒ，与英语单词“jot”同音）是一项拟议中的互联网标准，用于建立带有可选签名和／或可选加密的数据，其有效载荷中保存用于断言若干声明的 JSON。这些令牌可以使用共享秘密，或使用公钥／私钥进行签名。

例如，服务器可以生成一个带有“已作为管理员登录”这一声明的令牌，并将其提供给客户端。客户端随后便可使用该令牌来证明自己已以管理员身份登录。令牌可以由某一方的私钥（通常是服务器的私钥）签名，以便任何一方随后都能验证令牌是否合法。若另一方通过适当且可信的方式持有对应的公钥，则其也能够验证该令牌的合法性。这些会话令牌被设计为紧凑、URL安全、且易于使用，尤其适用于网页浏览器中的单点登录（SSO）场景。JWT 声明通常可用于在身份提供者与服务提供者之间传递已认证用户的身份信息，或按业务流程需要传递其他类型的声明。

JWT 依赖其他基于 JSON 的标准：JSON Web Signature 与 JSON Web Encryption。

## 结构

标头：用于标识生成签名时所使用的算法。在下面的例子中，`HS256` 表示此令牌使用 HMAC-SHA256 进行签名。

常见的密码学算法包括使用 SHA-256 的 HMAC（HS256），以及使用 SHA-256 的 RSA 数字签名（RS256）。JWA（JSON Web Algorithms）RFC 7518 还为认证与加密引入了更多算法。

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

有效载荷：包含一组声明。JWT 规范定义了七个已注册声明名称，它们是通常包含在令牌中的标准字段。此外，也常会根据令牌用途加入自定义声明。

以下示例包含标准的签发时间声明（`iat`）以及一个自定义声明（`loggedInAs`）。

```json
{
  "loggedInAs": "admin",
  "iat": 1422779638
}
```

签名：用于安全地验证令牌。签名的计算方式是先将标头与有效载荷分别使用 Base64url 编码，再以句点将二者连接成字符串。随后，将该字符串输入标头中指定的密码学算法中。本例使用带有共享秘密的 HMAC-SHA256（也定义了公钥算法）。Base64url 编码与 Base64 类似，但使用不同的非字母数字字符，并省略填充字符。

```javascript
HMAC_SHA256(
  secret,
  base64urlEncoding(header) + '.' +
  base64urlEncoding(payload)
)
```

以上三部分分别使用 Base64url 编码 编码，再以句点连接，构成 JWT：

```typescript
const token: string = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
```

以上数据配合秘密“secretkey”会生成如下令牌：

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN
_oWnFSRgCzcmJmMjLiuyu5CSpyHI
```

（上述 JSON 字符串会被格式化为不带换行与空格的 UTF-8 字节数组。这一点很重要，因为哪怕数据仅有细微变化，都会影响最终生成的令牌。）

由此生成的令牌可以方便地嵌入到 HTML 与 HTTP 中。

## 用途

在认证场景中，当用户成功登录后，通常会返回一个 JSON Web Token（JWT）。该令牌应通过安全机制发送给客户端，例如使用HTTP-only Cookie。通常不建议将 JWT 存储于浏览器的本地存储机制中，例如 localStorage 或 sessionStorage。这是因为运行在客户端的 JavaScript（包括浏览器扩展）可以访问这些存储机制，从而暴露 JWT 并危及安全性。若要在需要与跨来源 API 进行认证时使用 HTTP-only Cookie，较佳做法是使用 `credentials` 属性，告知浏览器在 Fetch 调用时自动将 Cookie 发送至外部 API，例如：

```javascript
fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include' // 告知浏览器一并发送 Cookie 等凭证
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));
```

使用这种方式时，JWT 不会暴露给客户端侧 JavaScript；这是在遵循安全最佳实践的同时使用 JWT 的较佳方式。对于无人值守的进程，客户端也可以直接使用预共享密钥生成并签署自己的 JWT，然后将其传递给兼容 OAuth 的服务，如下所示：

```mime
POST /oauth2/token
Content-type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJhb...
```

若客户端传递了有效的 JWT 断言，服务器将生成一个可用于调用应用程序的 `access_token`，并将其返回给客户端：

```json
{
  "access_token": "eyJhb...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

当客户端希望访问受保护的路由或资源时，用户代理通常会发送 JWT，一般是在 `Authorization` HTTP标头中使用 `Bearer` 方案。该标头内容可能如下所示：

```
Authorization: Bearer eyJhbGci''...&lt;snip&gt;...''yu5CSpyHI
```

这是一种无状态认证机制，因为用户状态并不会保存在服务器内存中。服务器上的受保护路由会检查 `Authorization` 标头中的 JWT 是否有效；若存在且有效，用户便可访问受保护资源。由于 JWT 是自包含的，所需信息都已包含其中，因此可减少多次查询数据库的需要。

## 标准字段

代码｜名称｜说明

`iss`｜签发者｜标识签发 JWT 的主体，例如组织名称或网站 URL。

`sub`｜主题｜标识 JWT 的主题，例如用户名或账号编号。

`aud`｜受众｜标识 JWT 的预期接收方。每个预期会处理该 JWT 的主体都必须在受众声明中以某个值标识自己。若处理该声明的主体在存在 `aud` 声明的情况下，无法以其中某个值标识自己，则该 JWT 必须被拒绝。

`exp`｜过期时间｜标识 JWT 在该时间及之后不得再被接受处理。其值必须为 NumericDate：即整数或小数，表示自 1970-01-01 00:00:00Z 起经过的秒数。

`nbf`｜生效时间｜标识 JWT 将从何时开始被接受处理。该值必须为 NumericDate。

`iat`｜签发时间｜标识 JWT 被签发的时间。该值必须为 NumericDate。

`jti`｜JWT ID｜令牌的区分大小写唯一标识符，即使在不同签发者之间也应保持唯一。

`typ`｜令牌类型｜若存在，则必须设为一个已注册的 IANA 媒体类型。

`cty`｜内容类型｜若使用嵌套签名或嵌套加密，建议将其设为 `JWT`；否则应省略此字段。

`alg`｜算法｜签发者可以自由设置用于验证令牌签名的算法。然而，一些受支持算法并不安全。

`kid`｜密钥 ID｜提示客户端使用了哪把密钥来生成令牌签名。服务器会将此值与已存档密钥匹配，以验证签名有效且令牌真实。

`x5c`｜X.509 证书链｜一个 RFC4945 格式的证书链，对应于生成令牌签名时所用的私钥。服务器将使用此信息验证签名有效且令牌真实。

`x5u`｜X.509 证书链 URL｜一个 URL，服务器可从中获取与生成令牌签名所用私钥对应的证书链。服务器将取得并使用这些信息验证签名真实。

`crit`｜关键｜一组服务器必须理解的标头列表，只有在理解这些标头后，才可接受该令牌为有效。

代码｜名称｜说明

当前已注册声明名称的列表可由 IANA 的 JSON Web Token Claims Registry 获取。

## 实现

JWT 已有许多语言与框架的实现，包括但不限于：

- .NET（C#、VB.Net 等）
- C
- C++
- Clojure
- Common Lisp
- Dart
- Elixir
- Erlang
- Go
- Haskell
- Java
- JavaScript
- Julia
- Lua
- Node.js
- OCaml
- Perl
- PHP
- PL/SQL
- PowerShell
- Python
- Racket
- Raku
- Ruby
- Rust
- Scala
- Swift

## 漏洞

JSON Web Token 可包含会话状态。但如果项目需求允许在 JWT 到期前使会话失效，那么服务就不能再仅凭令牌本身信任其中的断言。若要验证令牌中记录的会话未被撤销，则必须将令牌断言与数据存储进行比对。这样一来，令牌就不再是无状态的，从而削弱了 JWT 的主要优势。

安全顾问 Tim McLean 曾报告，某些 JWT 函式库在使用 `alg` 字段验证令牌时存在漏洞，最常见的情况是接受了 `alg=none` 的令牌。尽管这些漏洞后来已被修补，McLean 仍建议彻底弃用 `alg` 字段，以防止类似的实现混淆。尽管如此，现实中仍不断发现新的 `alg=none` 漏洞；在 2018 至 2021 年间，已有四个CVE与此原因有关。

若设计得当，开发者可以通过采取以下预防措施来应对算法漏洞：

- 不要仅凭 JWT 标头驱动验证流程
- 了解相关算法（避免只依赖 字段）
- 使用适当大小的密钥

2017 年，一些 JWT 函式库被发现容易受到无效椭圆曲线攻击。

也有人认为，由于标准中提供了大量不同的加密算法与选项，JSON Web Token 难以被安全地使用，因此网页前端与后端都应改用其他替代标准。

## 参考文献

- jwt.io — 由 Auth0 维护的 JWT 专门网站，提供工具与文档

## 基本资料

- 状态：拟议标准
- 首次出版：2010年12月28日
- version_date：2015年5月
- 组织：互联网工程任务组
- 缩写：JWT
- 委员会：互联网工程指导组
- 作者：Michael B. Jones、John Bradley、Nat Sakimura
- 基础标准：JSON、JSON Web Signature（JWS）、JSON Web Encryption（JWE）
- 领域：数据交换
- 网站： RFC 7519