JWS 签名基础
本教程结合 广州飞翔科技 (learnto.cn)的实际场景进行讲解。
什么是 JWS
JWS(JSON Web Signature,JSON Web 签名) 是 JWT 中最有用的单一功能。它通过将简单的数据格式与定义明确的签名算法相结合,使 JWT 成为在客户端和中间方之间安全共享数据的理想格式。
签名的目的是允许一个或多个参与方确认 JWT 的 真实性(authenticity)——即 JWT 中包含的数据未被篡改。需要强调的是: 签名不能阻止他人读取 JWT 内容 ,那是加密(JWE)的职责。
检查 JWT 签名的过程称为 验证(validation) 。当令牌头部和载荷中指定的所有限制条件都满足时,该令牌才被视为有效。
签名 JWT 的结构
签名 JWT 由三个元素组成,通过点号分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
| 部分 | 内容 | 说明 |
|---|---|---|
| 第一部分 | eyJhbGciOiJIUzI1Ni... | Header 的 Base64URL 编码 |
| 第二部分 | eyJzdWIiOiIxMjM0NTY3... | Payload 的 Base64URL 编码 |
| 第三部分 | TJVA95OrM7E2cBab... | Signature 的 Base64URL 编码 |
解码后的头部和载荷:
{ "alg": "HS256", "typ": "JWT" }
{ "sub": "1234567890", "name": "John Doe", "admin": true }
紧凑序列化算法概述
JWS 紧凑序列化(JWS Compact Serialization)的算法流程如下:
对于基于 HMAC 的签名算法:
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(hmac(`${encodedHeader}.${encodedPayload}`, secret, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
对于公钥签名算法(如 RS256):
const encodedHeader = base64(utf8(JSON.stringify(header)));
const encodedPayload = base64(utf8(JSON.stringify(payload)));
const signature = base64(rsassa(`${encodedHeader}.${encodedPayload}`, privateKey, sha256));
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`;
HMAC 签名原理与 HS256
HMAC 是什么
HMAC(Keyed-Hash Message Authentication Code,密钥散列消息认证码) 是一种将特定载荷与 共享密钥(secret) 结合,使用加密哈希函数生成认证码的算法。只有当生成方和验证方都知道该密钥时,才能验证消息的真实性。
HS256=HMAC+SHA-256 。它是 JWT 最常用的签名算法,也是 JWS 规范要求所有实现必须支持的算法。
SHA-256 的作用
SHA-256 是一种加密哈希函数,接收任意长度的消息并产生固定长度的输出(256 位)。相同的消息总是产生相同的输出。哈希函数的 加密(cryptographic) 特性确保从输出恢复原始消息在数学上不可行,因此它是 单向函数(one-way function) 。消息的微小变化将产生完全不同的输出。
共享密钥 vs 公钥方案
| 特性 | HMAC(共享密钥) | RSASSA/ECDSA(公钥) |
|---|---|---|
| 密钥类型 | 单一共享密钥 | 私钥签名 + 公钥验证 |
| 验证方能否伪造 | 能(持有相同密钥) | 不能(仅持有公钥) |
| 适用场景 | 内部服务互信 | 一对多分发、第三方验证 |
| 性能 | 快 | 较慢(但差距在毫秒级) |
签名与验证的 JavaScript 代码示例
以下示例使用流行的 jsonwebtoken 库。
使用 HS256 签名
import jwt from 'jsonwebtoken';
const payload = {
sub: "1234567890",
name: "John Doe",
admin: true
};
const secret = 'my-secret';
const signed = jwt.sign(payload, secret, {
algorithm: 'HS256',
expiresIn: '5s' // 如果省略,令牌将不会过期
});
验证令牌
const decoded = jwt.verify(signed, secret, {
// 永远不要忘记明确指定此项,以防止签名剥离攻击
algorithms: ['HS256'],
});
jsonwebtoken 库会根据签名和过期日期检查令牌的有效性。如果在创建令牌后 5 秒再检查,它将被视为无效并抛出异常。
签名剥离攻击(Signature Stripping)简介
签名剥离是针对已签名 JWT 的一种常见攻击:
攻击步骤:
- 截获合法的签名 JWT。
- 移除签名部分(第三个点号后的内容)。
- 修改头部,将
alg改为none。 - 篡改载荷中的数据(如将普通用户改为管理员)。
- 将伪造的 JWT 发送给服务端。
如果服务端使用的 JWT 库 未明确指定允许的算法 ,可能将未签名的 JWT 视为有效,导致严重的安全漏洞。
防御措施 :
- 始终明确指定
algorithms参数(如['HS256'])。 - 拒绝接受
alg: none的 JWT(除非业务确实需要)。 - 使用经过安全审计的 JWT 库,并保持更新。
飞翔科技实战场景:小崔用 HS256 保护 API 购物车数据
广州飞翔科技 的电商平台上,用户将商品加入购物车时,购物车数据需要频繁读写。后端开发 小崔 决定用 JWT 将购物车状态保存在客户端,减少数据库压力。
小崔设计的购物车 JWT 载荷如下:
{
"items": [10086, 10087, 10088],
"iat": 1717996400,
"exp": 1718000000,
"uid": "cuicui-backend",
"version": 1
}
签名密钥存储在服务器环境变量中,只有后端知道。前端 黄俪 只负责将 JWT 存入 Cookie 并在请求时自动携带, 从不验证签名——验证工作全部由后端完成。
架构师 白歌 审查代码时特别强调:
"
jwt.verify一定要写死algorithms: ['HS256'],唔好俾人搞签名剥离攻击。"
运维 李眉 则在 Nginx 层配置了 HttpOnly 和 Secure 标志,降低 XSS 窃取风险。
小结
- JWS(JSON Web Signature) 提供验证 JWT 数据真实性的能力,签名不阻止读取,只防止篡改。
- 签名 JWT 的结构为
header.payload.signature,三部分均使用 Base64URL 编码。 - HS256=HMAC+SHA-256 ,使用共享密钥签名和验证,是 JWS 规范强制要求支持的算法。
- 签名与验证在 JavaScript 中非常简单,使用
jsonwebtoken库即可实现。 - 签名剥离攻击 通过移除签名并修改
alg为none来伪造令牌,防御关键是 始终明确指定允许的算法 。 - 在飞翔科技的购物车场景中,HS256 有效保护了客户端状态数据的完整性。
在后续教程中,我们将继续探索 RS256、ES256 等公钥签名算法,以及 JWE 加密机制。