2、JWT原理与结构:JWT头部、载荷、签名三部分详解,JWT生成与验证流程。
JWT(JSON Web Token)是目前分布式鉴权体系中最核心的令牌格式。它通过自包含的声明结构,将用户身份与权限信息编码为紧凑的字符串,无需服务端维护会话状态即可完成身份验证。理解JWT的三段式结构及其生成验证流程,是构建Ent权限验证体系的基础。
2.1 JWT三段式结构总览
一个标准的JWT由三个Base64URL编码的部分组成,以点号(.)分隔:Header.Payload.Signature。以下表格展示了每一部分的职责与内容特征:
| 组成部分 | 英文名称 | 编码方式 | 是否加密 | 核心作用 |
|---|---|---|---|---|
| 头部 | Header | Base64URL | 明文(仅编码) | 声明令牌类型与签名算法 |
| 载荷 | Payload | Base64URL | 明文(仅编码) | 携带用户身份、权限及自定义声明 |
| 签名 | Signature | 算法输出 | 防篡改 | 验证令牌完整性与真实性 |
关键认知:JWT的Payload是Base64URL编码而非加密。任何持有令牌的人都可以解码查看内容,因此绝对禁止在Payload中存放密码、密钥等敏感信息。签名仅保证内容未被篡改,不提供内容保密性。
2.2 头部(Header)详解
Header是一个JSON对象,通常包含两个字段:令牌类型(typ)和签名算法(alg)。
{
"alg": "HS256",
"typ": "JWT"
}
- typ(Type):固定为
"JWT",表示这是一个JWT令牌。 - alg(Algorithm):签名算法标识,常见值包括:
HS256:HMAC-SHA256(对称签名,需共享密钥)RS256:RSA-SHA256(非对称签名,私钥签名、公钥验证)ES256:ECDSA-SHA256(椭圆曲线签名)
安全警告:服务端必须严格校验
alg字段。攻击者可能将RS256篡改为none(无签名算法)或HS256(使用公钥作为HMAC密钥),绕过签名验证。务必在代码中白名单允许的算法列表。
2.3 载荷(Payload)详解
Payload是JWT的信息载体,包含三类声明(Claims):注册声明、公共声明、私有声明。以下表格列出最常用的注册声明:
| 声明名 | 全称 | 含义 | 是否必选 |
|---|---|---|---|
iss |
Issuer | 签发者,标识令牌的签发方 | 推荐 |
sub |
Subject | 主题,通常为用户ID或唯一标识 | 推荐 |
aud |
Audience | 接收方,标识令牌的目标服务 | 可选 |
exp |
Expiration Time | 过期时间(Unix时间戳) | 强烈推荐 |
nbf |
Not Before | 生效时间(Unix时间戳) | 可选 |
iat |
Issued At | 签发时间(Unix时间戳) | 推荐 |
jti |
JWT ID | 令牌唯一标识,用于防重放 | 可选 |
在Ent权限验证场景中,Payload通常包含用户角色与权限信息:
{
"sub": "user_abc123",
"iss": "auth.ent.example.com",
"iat": 1718000000,
"exp": 1718003600,
"roles": ["admin", "editor"],
"permissions": ["article:write", "article:publish", "user:read"]
}
2.4 签名(Signature)详解
签名是JWT防篡改的核心保障。其生成公式为:
Signature = HMAC-SHA256(
base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
secret
)
签名过程将头部和载荷的Base64URL编码拼接后,使用指定算法与密钥计算摘要。验证时,接收方使用相同的密钥和算法重新计算签名,并与令牌携带的签名比对。若不一致,则令牌无效。
对称 vs 非对称签名选择:
- HS256(对称):适合单一服务内部使用,签发与验证使用同一密钥,密钥不得泄露。
- RS256(非对称):适合微服务架构,认证服务持有私钥签发,其他服务使用公钥验证,公钥可安全分发。
2.5 JWT生成与验证流程
以下SVG图展示了JWT从生成到验证的完整生命周期,包含客户端、认证服务与业务服务三个角色:
2.6 生成与验证步骤详解
生成流程(认证服务侧)
- 构建Header:创建JSON对象,设置
alg和typ字段。 - 构建Payload:填充注册声明(sub、exp、iat等)与自定义权限声明。
- Base64URL编码:分别对Header和Payload进行Base64URL编码(去除填充符
=,替换+为-,/为_)。 - 计算签名:将编码后的Header与Payload用点号拼接,使用指定算法和密钥计算签名。
- 组装令牌:输出格式为
base64Header.base64Payload.signature。
验证流程(业务服务侧)
- 拆分令牌:按点号分割为三部分,检查是否恰好三段。
- 解码Header:Base64URL解码头部,读取
alg字段,确认算法在白名单内。 - 解码Payload:Base64URL解码载荷,提取
exp、nbf等时间声明,检查令牌是否在有效期内。 - 重新计算签名:使用Header中声明的算法和本地密钥,对
base64Header.base64Payload重新计算签名。 - 比对签名:使用恒定时间比较函数(如Go的
crypto/subtle.ConstantTimeCompare)比对计算出的签名与令牌携带的签名是否一致。 - 提取声明:签名验证通过后,从Payload中提取用户身份与权限信息,供后续Ent权限验证使用。
2.7 Go语言实现示例
以下代码展示了使用标准库crypto/hmac和encoding/base64实现JWT生成与验证的核心逻辑:
package jwt
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// Header 定义JWT头部
type Header struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
// Payload 定义JWT载荷
type Payload struct {
Sub string `json:"sub"`
Iss string `json:"iss"`
Iat int64 `json:"iat"`
Exp int64 `json:"exp"`
Roles []string `json:"roles"`
}
// base64UrlEncode 进行Base64URL编码(无填充)
func base64UrlEncode(data []byte) string {
return strings.TrimRight(
base64.URLEncoding.EncodeToString(data), "=",
)
}
// base64UrlDecode 进行Base64URL解码
func base64UrlDecode(s string) ([]byte, error) {
// 补全填充符
if l := len(s) % 4; l > 0 {
s += strings.Repeat("=", 4-l)
}
return base64.URLEncoding.DecodeString(s)
}
// Generate 生成JWT令牌
func Generate(secret []byte, sub, iss string, roles []string, ttl int64) (string, error) {
header := Header{Alg: "HS256", Typ: "JWT"}
headerJSON, _ := json.Marshal(header)
headerEncoded := base64UrlEncode(headerJSON)
now := time.Now().Unix()
payload := Payload{
Sub: sub,
Iss: iss,
Iat: now,
Exp: now + ttl,
Roles: roles,
}
payloadJSON, _ := json.Marshal(payload)
payloadEncoded := base64UrlEncode(payloadJSON)
// 计算签名
signingInput := headerEncoded + "." + payloadEncoded
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(signingInput))
signature := base64UrlEncode(mac.Sum(nil))
return signingInput + "." + signature, nil
}
// Verify 验证JWT令牌并返回Payload
func Verify(token string, secret []byte) (*Payload, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// 重新计算签名
signingInput := parts[0] + "." + parts[1]
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(signingInput))
expectedSig := base64UrlEncode(mac.Sum(nil))
// 恒定时间比较
if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
return nil, fmt.Errorf("signature mismatch")
}
// 解码Payload
payloadJSON, err := base64UrlDecode(parts[1])
if err != nil {
return nil, fmt.Errorf("payload decode failed: %w", err)
}
var payload Payload
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return nil, fmt.Errorf("payload unmarshal failed: %w", err)
}
// 检查过期时间
if time.Now().Unix() > payload.Exp {
return nil, fmt.Errorf("token expired")
}
return &payload, nil
}
生产环境建议:
- 优先使用成熟的JWT库(如
github.com/golang-jwt/jwt/v5),避免手写签名逻辑引入安全漏洞。 - 密钥定期轮换,HS256密钥长度至少256位(32字节)。
- 令牌过期时间(exp)建议控制在15分钟至2小时,配合刷新令牌(Refresh Token)实现长期会话。
- 在微服务架构中,优先使用RS256非对称签名,认证服务持有私钥,其他服务仅需公钥即可验证。
2.8 常见安全风险与防范
| 风险类型 | 攻击方式 | 防范措施 |
|---|---|---|
| 算法混淆 | 将RS256改为HS256,使用公钥作为HMAC密钥 | 服务端白名单允许的算法,拒绝none算法 |
| 令牌泄露 | 中间人截获JWT,直接重放请求 | 使用HTTPS传输,设置短过期时间,绑定客户端指纹 |
| 签名绕过 | 修改Payload后不更新签名,或使用空签名 | 严格校验签名,拒绝签名缺失的令牌 |
| 敏感信息泄露 | 在Payload中存放密码、密钥等明文 | Payload仅存放非敏感标识,敏感数据使用加密JWE |