3、Token基础:JWT结构解析(Header/Payload/Signature)、HS256与RS256算法对比
JSON Web Token(JWT)是当前 Web 授权中最常用的自包含令牌格式。理解 JWT 的三段式结构以及签名算法的差异,是进行 Token 自省与撤销端点测试的前提。本节将深入解析 JWT 的 Header、Payload、Signature 三个组成部分,并对比对称算法 HS256 与非对称算法 RS256 在安全模型、性能与适用场景上的核心区别。
3.1 JWT 三段式结构
一个标准的 JWT 由三个 Base64URL 编码的片段组成,以点号(.)分隔:Header.Payload.Signature。解码后,每一部分都是一个 JSON 对象。
| 部分 | 名称 | 内容说明 | 是否加密 |
|---|---|---|---|
| 第一部分 | Header(头部) | 令牌类型(typ)和签名算法(alg) | 仅 Base64 编码,明文可读 |
| 第二部分 | Payload(载荷) | 声明(Claims),如用户 ID、角色、过期时间 | 仅 Base64 编码,明文可读 |
| 第三部分 | Signature(签名) | 对前两部分的签名,用于防篡改 | 二进制签名数据,不可逆 |
安全提示:Payload 中的敏感信息(如密码、信用卡号)不应以明文存放。JWT 的 Payload 仅经过 Base64 编码,并非加密。如需保护载荷内容,应使用 JWE(JSON Web Encryption)。
3.2 Header 与 Payload 详解
Header(头部)
Header 通常包含两个字段:alg(签名算法)和 typ(令牌类型)。示例:
{
"alg": "RS256",
"typ": "JWT"
}
- alg:指定签名算法,常见值有
HS256、RS256、ES256。 - typ:固定为
JWT,表示这是一个 JWT 令牌。
Payload(载荷)
Payload 包含声明(Claims),分为三类:
- 注册声明(Registered Claims):预定义的标准字段,如
iss(签发者)、sub(主题)、aud(受众)、exp(过期时间)、iat(签发时间)。 - 公共声明(Public Claims):由使用方自定义,需在 IANA JSON Web Token Claims 注册表中定义,或使用命名空间避免冲突。
- 私有声明(Private Claims):双方协商的字段,如
user_id、role。
示例 Payload:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
3.3 Signature 生成原理
Signature 是对 Header 和 Payload 的完整性保护。生成过程如下:
- 将 Header 和 Payload 分别进行 Base64URL 编码。
- 将编码后的字符串用点号连接:
base64UrlEncode(header).base64UrlEncode(payload)。 - 使用指定的签名算法(HS256 或 RS256)和密钥对上述字符串进行签名。
- 将签名结果进行 Base64URL 编码,作为 JWT 的第三部分。
验证要点:接收方使用相同的算法和密钥重新计算签名,并与 JWT 中的签名比对。若不一致,则令牌无效。
3.4 HS256 与 RS256 算法对比
HS256(HMAC with SHA-256)和 RS256(RSA Signature with SHA-256)是 JWT 最常用的两种签名算法。它们在密钥管理、安全模型和性能上存在本质差异。
| 对比维度 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥类型 | 单一共享密钥(对称密钥) | 私钥签名 + 公钥验证(密钥对) |
| 密钥分发 | 需安全共享给所有验证方 | 公钥可公开分发,私钥仅签发方持有 |
| 安全性风险 | 密钥泄露后,攻击者可伪造任意令牌 | 私钥泄露风险低,公钥泄露不影响安全 |
| 性能 | 签名和验证速度极快(微秒级) | 签名较慢(毫秒级),验证较快 |
| 适用场景 | 单一服务内部通信、微服务间信任 | 多服务、跨组织、第三方验证 |
| 密钥轮换 | 轮换时需同步所有服务 | 仅需更新公钥,支持动态轮换 |
| 常见攻击 | 密钥暴力破解、算法混淆攻击 | 公钥注入、弱密钥攻击 |
安全警告:在 Go 语言实现中,使用 HS256 时务必确保密钥长度 ≥ 256 位(32 字节),且使用
crypto/rand 生成。使用 RS256 时,私钥必须妥善保管,建议使用硬件安全模块(HSM)或密钥管理服务(KMS)。
3.5 算法选择决策树
在实际的 Go Web 授权测试中,选择哪种算法取决于你的架构和安全需求。以下决策树可帮助快速判断:
- 所有验证方都在同一信任域内? → 选择 HS256(性能优先)
- 需要第三方或外部服务验证令牌? → 选择 RS256(安全优先)
- 需要支持密钥动态轮换? → 选择 RS256
- 令牌签发频率极高,对延迟敏感? → 选择 HS256
- 合规要求私钥不可泄露? → 选择 RS256
3.6 Go 语言中的 JWT 实现要点
在 Go 中,github.com/golang-jwt/jwt/v5 是最主流的 JWT 库。以下是使用 HS256 和 RS256 的核心代码片段:
HS256 签名与验证
import "github.com/golang-jwt/jwt/v5"
// 签名
claims := jwt.MapClaims{
"sub": "1234567890",
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte("your-256-bit-secret"))
// 验证
parsedToken, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte("your-256-bit-secret"), nil
})
RS256 签名与验证
import "github.com/golang-jwt/jwt/v5"
// 加载私钥
privateKeyBytes, _ := os.ReadFile("private.pem")
privateKey, _ := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes)
// 签名
claims := jwt.MapClaims{
"sub": "1234567890",
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
signedToken, err := token.SignedString(privateKey)
// 加载公钥
publicKeyBytes, _ := os.ReadFile("public.pem")
publicKey, _ := jwt.ParseRSAPublicKeyFromPEM(publicKeyBytes)
// 验证
parsedToken, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return publicKey, nil
})
测试要点:在授权测试中,应重点验证以下边界情况:
1. 修改 Header 中的 alg 为 "none" 或 "HS256"(当服务端使用 RS256 时)。
2. 篡改 Payload 中的 sub、exp、role 等关键声明。
3. 使用空密钥或弱密钥进行签名尝试。
4. 验证过期令牌是否被正确拒绝。
1. 修改 Header 中的 alg 为 "none" 或 "HS256"(当服务端使用 RS256 时)。
2. 篡改 Payload 中的 sub、exp、role 等关键声明。
3. 使用空密钥或弱密钥进行签名尝试。
4. 验证过期令牌是否被正确拒绝。
3.7 常见攻击与防御
| 攻击类型 | 描述 | 防御措施 |
|---|---|---|
| 算法混淆攻击 | 将 RS256 令牌的 alg 改为 HS256,使用公钥作为 HMAC 密钥验证 | 在验证时强制检查算法类型,拒绝非预期算法 |
| none 算法攻击 | 将 alg 改为 "none",绕过签名验证 | 禁用 "none" 算法,验证时要求签名非空 |
| 密钥暴力破解 | 对 HS256 令牌进行离线字典攻击 | 使用强随机密钥(≥256 位),定期轮换 |
| 公钥注入 | 攻击者替换 RS256 公钥为自签名公钥 | 公钥通过 HTTPS 安全分发,使用固定公钥指纹 |
| 重放攻击 | 捕获有效令牌后重复使用 | 使用 jti(JWT ID)声明 + 短过期时间 + 令牌撤销列表 |