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从生成到验证的完整生命周期,包含客户端、认证服务与业务服务三个角色:

JWT 生成与验证流程 客户端 认证服务 业务服务 1. 登录请求 生成 Header + Payload 计算 Signature 2. 返回 JWT 令牌 存储 JWT(LocalStorage) 3. 请求携带 JWT(Authorization: Bearer) 解析 Header + Payload 重新计算 Signature 比对签名 → 通过/拒绝 验证要点 • 检查 exp 是否过期 • 检查 iss / aud 是否匹配 • 签名比对必须恒定时间比较

2.6 生成与验证步骤详解

生成流程(认证服务侧)

  1. 构建Header:创建JSON对象,设置alg和typ字段。
  2. 构建Payload:填充注册声明(sub、exp、iat等)与自定义权限声明。
  3. Base64URL编码:分别对Header和Payload进行Base64URL编码(去除填充符=,替换+为-,/为_)。
  4. 计算签名:将编码后的Header与Payload用点号拼接,使用指定算法和密钥计算签名。
  5. 组装令牌:输出格式为 base64Header.base64Payload.signature。

验证流程(业务服务侧)

  1. 拆分令牌:按点号分割为三部分,检查是否恰好三段。
  2. 解码Header:Base64URL解码头部,读取alg字段,确认算法在白名单内。
  3. 解码Payload:Base64URL解码载荷,提取exp、nbf等时间声明,检查令牌是否在有效期内。
  4. 重新计算签名:使用Header中声明的算法和本地密钥,对base64Header.base64Payload重新计算签名。
  5. 比对签名:使用恒定时间比较函数(如Go的crypto/subtle.ConstantTimeCompare)比对计算出的签名与令牌携带的签名是否一致。
  6. 提取声明:签名验证通过后,从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