如何创建签名的 JWT
- 选择 算法。头部(JSON) 中的
alg字段会自动跟随所选算法。 - 编辑 载荷(JSON)。+ iat 设为现在 会插入当前时间,+ exp 设为 1 小时后 会把过期时间设为一小时之后;两者都使用秒,这是 JWT 规范的要求。
- 对于 HS256、HS384 或 HS512,输入 密钥,并把 密钥格式 设为文本、Base64 或 Hex。对于其他算法,粘贴私钥或按 生成密钥对。
- 按 复制令牌,或按 在解码器中打开 进行检查:解码并验证 标签页会打开,并自动填好令牌和验证密钥。
页面加载时的示例是一个用密钥 base64hub-demo-secret 签名的有效 HS256 令牌,你可以改动一个声明,看签名随之变化。
选择算法
| 算法 | 密钥类型 | 谁能验证 | 令牌中的签名 |
|---|---|---|---|
| HS256 | 共享密钥(HMAC-SHA256) | 持有同一密钥的任何人 | 43 个字符 |
| RS256、PS256 | RSA 私钥,2048 位或更长 | 持有公钥的任何人 | 342 个字符(RSA-2048) |
| ES256 | P-256 曲线上的 EC 私钥 | 持有公钥的任何人 | 86 个字符 |
| EdDSA | Ed25519 私钥 | 持有公钥的任何人 | 86 个字符 |
当同一个服务负责签名和验证时(例如由同一后端校验的会话令牌),HS256 是最简单的选择。一旦需要第二方来验证,与对方共享 HMAC 密钥也就让对方能够签发令牌,这时应改用非对称算法,只公开公钥。ES256 和 EdDSA 生成的令牌比 RSA 短得多,这对 Cookie 和请求头很重要。PS256 是使用 PSS 填充方案的 RSA;只有在所有验证方都支持时才选它。
列表中的 none 用于测试解析器。alg: none 的令牌签名为空,任何接受它的服务器都可能被任何人欺骗,所以工具会对此发出警告。
本编码器接受和生成的密钥
签名时,它接受 PEM PKCS#8 密钥(BEGIN PRIVATE KEY)、PKCS#1 RSA PRIVATE KEY、SEC1 EC PRIVATE KEY 或私钥 JWK。如果不用生成器,而是用 OpenSSL 制作测试密钥:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa.pem
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.pem
openssl genpkey -algorithm ed25519 -out ed.pem
openssl pkey -in ec.pem -pubout -out ec.pub
生成密钥对 会根据算法,在浏览器中用 WebCrypto 生成 RSA-2048、P-256、P-384、P-521 或 Ed25519 密钥。你会得到 PKCS#8 PEM 格式的私钥、SPKI PEM 格式的公钥,以及一个 JWKS,其 kid 是该密钥的 RFC 7638 指纹。这个 JWKS 就是你应该通过 jwks_uri 提供给验证方下载的内容。
对于 HS256,密钥按字节使用。如果你的服务器从环境变量读取密钥并先做 Base64 解码,这里也要把 密钥格式 设为 Base64,否则签名对不上。Base64 密钥生成器 的 JWT 密钥预设生成的值是 Base64URL,解码后为 32 字节。
载荷中应该放哪些声明
一个实用的访问令牌通常包含 iss(谁签发的)、sub(关于谁)、aud(给哪个 API 用)、iat 和一个较短的 exp。如果需要吊销单个令牌,再加上 jti。时间是自 1970 年起的整秒数;在 JavaScript 中生成时请用 Math.floor(Date.now() / 1000),因为 Date.now() 返回的是毫秒。
载荷只是签名,从不加密。任何看到令牌的人都能用 JWT 解码器 解码,所以不要放入密码、API 密钥,以及你不会打印到日志中的个人数据。
在代码中签名 JWT
这种格式简单到可以手工构造,调试库的时候很有帮助。下面这个 Node.js 函数,对同样的输入生成的令牌与 PyJWT 的 jwt.encode(payload, secret, algorithm="HS256") 完全一致:
const crypto = require("crypto");
const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
function signHS256(payload, secret) {
const input = b64url({ alg: "HS256", typ: "JWT" }) + "." + b64url(payload);
const sig = crypto.createHmac("sha256", secret).update(input).digest("base64url");
return input + "." + sig;
}
console.log(signHS256({ sub: "user-42", iat: 1767225600, exp: 1767229200 },
"replace-me-with-32-random-bytes!"));
它输出 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTQyIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9.F3xnXnXW8nUQy-Ay37DF626Lb5lHPKI3IFvqoEf3mTw。在本页输入同样的头部、载荷和密钥,会得到同样的字符串。工具在签名前会压缩 JSON,所以缩进无关紧要,但键的顺序有关,因为签名覆盖的是确切的字节。生产环境请使用有人维护的库;编码相关内容见 Node.js 中的 Base64。
常见问题
HS256 的密钥应该多长?
至少 256 位,也就是 32 个随机字节。RFC 7518 要求密钥长度不短于哈希输出。类似密码的短密钥可以仅凭一个令牌离线暴力破解,所以请用 Base64 密钥生成器 生成。
应该用 HS256 还是 RS256?
如果签发和验证令牌的是同一个服务,用 HS256,因为双方共享同一个密钥。如果其他方也必须验证,用 RS256、ES256 或 EdDSA,因为他们只拿到公钥,无法自己签发令牌。
为什么我的私钥报错?
受密码保护的密钥(BEGIN ENCRYPTED PRIVATE KEY)会被拒绝。用 openssl pkey -in key.pem -out plain.pem 去掉密码,再粘贴结果。密钥类型也必须与算法匹配:EC 密钥不能签名 RS256。
怎样给令牌添加 kid?
按 生成密钥对 后,从 JWKS 输出中复制 kid,添加到头部 JSON 中,例如 "kid": "…"。验证方会用它从自己的密钥集中挑选正确的密钥。