Menu

Node.js JWT:签发、验证并保护 Express 路由

JWT 是由三段 base64url 组成的签名令牌,服务器在用户登录时签发,并在每个请求中检查它。在 Node.js 中,你用 jose 或 jsonwebtoken 包和一个密钥来签发和验证它。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

JWT(JSON Web Token)是由三段 base64url 组成的字符串 header.payload.signature,服务器在用户登录时把它交给用户,之后的每个请求都会检查它。签名能证明令牌是服务器签发的、没有被任何人改动过,但 payload 只是经过编码,并没有加密,所以任何人都能读取它。在 Node.js 中,你用 jose 包中的 new SignJWT(payload).sign(secret) 创建令牌,用 jwtVerify(token, secret) 检查令牌。

它先打印 3 parts,再打印 user-42 admin:jwtVerify 检查了签名和过期时间,并返回了 payload。把传给 jwtVerify 的密钥改动一个字符再运行:验证会以 signature verification failed 失败。

JWT 里有什么

header 和 payload 都是 JSON,用 base64url 编码。解码它们不需要任何密钥,所以 JWT 里永远不能放密码或其他任何机密信息。

前两行打印 {"alg":"HS256"} 和 {"name":"Ada","role":"editor"},任何持有令牌的人都能读到。签名(HS256 的签名有 43 个字符)才是阻止别人篡改的东西:把 editor 改成 admin,签名就对不上了。

部分包含的内容是否保密
Header算法,例如 HS256否,可以读取
Payload声明(claims):sub(用户 id)、exp(过期时间)、iat(签发时间),以及你自己的字段否,可以读取
Signature对前两部分做的 HMAC 或 RSA 签名需要密钥才能生成

过期与验证失败

setExpirationTime('15m') 会写入一个 exp 声明,超过这个时间的令牌会被 jwtVerify 拒绝。每次拒绝都是一个带 code 的错误,所以你能区分过期的令牌和伪造的令牌。

第一次检查以 ERR_JWT_EXPIRED 失败,第二次(签名被改过)以 ERR_JWS_SIGNATURE_VERIFICATION_FAILED 失败。对你的 API 来说两者含义相同(都返回 401),但把错误码记录下来有助于调试。

用 Bearer 令牌保护 Express 路由

客户端登录一次,保存令牌,然后在 Authorization 请求头中以 Bearer <token> 的形式发送它。一个中间件负责验证令牌,并把 payload 放到 req.user 上供路由使用。

第一个请求是不带令牌的 GET /me,得到 401 和 Missing token。发送 POST /login,请求体为 {"username": "ada", "password": "lovelace"},从响应中复制 token,然后带着请求头 Authorization: Bearer <token> 再发送一次 GET /me:响应是 {"id":"user-1","name":"ada"}。

jsonwebtoken 包

jsonwebtoken(npm install jsonwebtoken)是另一个常见的选择。它的 sign 和 verify 是同步的(可选传入回调),不返回 Promise:

const jwt = require('jsonwebtoken');

const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
    subject: 'user-42',
    expiresIn: '1h',
});

const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid

新代码请用 jose:它没有任何依赖,还能在边缘运行时和浏览器中运行。两个库生成的都是标准 JWT,所以一个库签发的令牌可以用另一个库验证。

常见错误

用 decode 代替 verify。 jwt.decode 和 decodeJwt 读取 payload 时不检查签名,所以伪造的令牌也能通过。只信任 verify 返回的内容。

密钥太短或被提交到代码库。 能被猜到的 HS256 密钥会让攻击者自己签发令牌。至少使用 32 个随机字节,从 process.env 中读取,永远不要提交它。

令牌永不过期。 没有 exp,泄露的令牌会永远有效。一定要设置过期时间。

常见问题

客户端应该把 JWT 存在哪里?

在浏览器应用中,设置了 HttpOnly 和 Secure 的 cookie 最安全,因为页面脚本读不到它。localStorage 更简单,但页面上的任何脚本,包括被注入的脚本,都能读取这个令牌。移动应用使用平台提供的安全存储。

使用 JWT 时如何让用户退出登录?

服务器无法作废一个已经签发的令牌;它在过期前一直有效。在客户端删除它,让访问令牌的有效期保持很短;如果需要立即退出登录,就在服务器端维护一份已吊销令牌 id(jti)的列表,并在每个请求中检查它。

选 HS256 还是 RS256?

HS256 用同一个共享密钥签名和验证,适合单个后端。RS256 用私钥签名、用公钥验证,所以其他服务可以检查令牌,却无法创建令牌。

JWT 的有效期应该设多长?

访问令牌的有效期通常是 5 至 60 分钟。需要更长的会话时,搭配一个存放得更谨慎的刷新令牌,客户端用它换取新的访问令牌。

Coddy 编程语言插图

和 Coddy 一起学编程

开始