4、JWT令牌与认证:JWT结构(Header/Payload/Signature)、Go生成与验证JWT、令牌刷新策略

4.1 JWT 结构:三部分拆解

JWT(JSON Web Token)是一种紧凑的、自包含的令牌格式,由三个 Base64URL 编码的 JSON 对象通过点号(.)连接而成。在权限审计网关中,JWT 是承载用户身份与权限声明的核心载体。

部分 名称 内容 是否加密
Header 头部 令牌类型(typ)与签名算法(alg),如 {"alg":"HS256","typ":"JWT"} Base64URL 编码,明文
Payload 载荷 声明(Claims):注册声明(iss、sub、exp、iat)、公共声明、私有声明 Base64URL 编码,明文
Signature 签名 对 Header + "." + Payload 使用指定算法和密钥生成的签名 防篡改
关键理解:Payload 是明文可读的,不要在 Payload 中存放密码、密钥等敏感信息。签名仅保证数据未被篡改,不提供加密保护。如需加密,应使用 JWE(JSON Web Encryption)。

4.2 JWT 生命周期与核心流程

下图展示了 JWT 从签发、传输、验证到刷新的完整生命周期,以及网关在其中的拦截位置。

JWT 生命周期与网关验证流程 客户端 认证服务 权限审计网关 业务服务 ① 登录请求 ② 签发JWT ③ 携带JWT请求业务 ④ 验证JWT ⑤ 转发请求 ⑥ 令牌刷新 ⑦ 刷新请求 ⑧ 签发新JWT ⑨ 新令牌

4.3 Go 生成与验证 JWT

使用 github.com/golang-jwt/jwt/v5 库实现 JWT 的签发与验证。以下代码展示了网关中核心的令牌管理逻辑。

4.3.1 生成 JWT(签发令牌)

package auth

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

// 自定义声明结构
type CustomClaims struct {
    UserID   uint     `json:"user_id"`
    Role     string   `json:"role"`
    Perms    []string `json:"permissions"`
    jwt.RegisteredClaims
}

// 签发 Access Token
func GenerateAccessToken(userID uint, role string, perms []string, secret []byte) (string, error) {
    claims := CustomClaims{
        UserID: userID,
        Role:   role,
        Perms:  perms,
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:    "audit-gateway",
            Subject:   "access-token",
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            ID:        generateJTI(), // 唯一令牌ID,用于吊销
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secret)
}

4.3.2 验证 JWT(网关拦截)

// 验证并解析 Access Token
func ValidateAccessToken(tokenString string, secret []byte) (*CustomClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
        // 校验签名算法
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, jwt.ErrSignatureInvalid
        }
        return secret, nil
    })
    if err != nil {
        return nil, err
    }
    if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
        // 可选:检查 JTI 是否已被吊销(黑名单校验)
        if isRevoked(claims.ID) {
            return nil, ErrTokenRevoked
        }
        return claims, nil
    }
    return nil, jwt.ErrTokenInvalidClaims
}
安全实践:验证时务必校验签名算法(alg),防止攻击者将 RS256 改为 HS256 使用公钥作为密钥进行伪造。推荐使用 jwt.WithValidMethods() 明确指定允许的算法列表。

4.4 令牌刷新策略

Access Token 有效期短(通常 15~30 分钟),Refresh Token 有效期长(通常 7~30 天)。刷新策略决定了系统的安全性与用户体验的平衡。

策略 实现方式 安全性 适用场景
自动刷新 网关检测 Access Token 过期,自动使用 Refresh Token 获取新令牌 中 移动端、SPA 单页应用
轮换刷新 每次刷新时同时签发新的 Access Token 和新的 Refresh Token,旧 Refresh Token 立即失效 高 金融、高安全要求系统
滑动过期 每次请求有效时,自动延长 Access Token 有效期,用户持续活跃则永不登出 低 内部管理系统、低敏感场景
强制刷新 Access Token 过期后必须重新登录,不提供 Refresh Token 最高 管理后台、敏感操作

4.4.1 轮换刷新策略实现

// 刷新令牌对(轮换策略)
func RefreshTokenPair(refreshTokenString string, refreshSecret []byte, accessSecret []byte) (string, string, error) {
    // 1. 验证 Refresh Token
    claims, err := ValidateRefreshToken(refreshTokenString, refreshSecret)
    if err != nil {
        return "", "", ErrInvalidRefreshToken
    }
    
    // 2. 检查 Refresh Token 是否已被使用(轮换检测)
    if isRefreshTokenUsed(claims.ID) {
        // 检测到重放攻击:吊销该用户所有令牌
        revokeAllUserTokens(claims.UserID)
        return "", "", ErrReplayAttackDetected
    }
    
    // 3. 标记当前 Refresh Token 为已使用
    markRefreshTokenUsed(claims.ID)
    
    // 4. 签发新的令牌对
    newAccessToken, _ := GenerateAccessToken(claims.UserID, claims.Role, claims.Perms, accessSecret)
    newRefreshToken, _ := GenerateRefreshToken(claims.UserID, claims.Role, claims.Perms, refreshSecret)
    
    return newAccessToken, newRefreshToken, nil
}
轮换策略关键点:必须维护已使用 Refresh Token 的 JTI 列表(Redis 或数据库),检测到重复使用时应立即吊销该用户所有令牌,防止 Refresh Token 泄露后的重放攻击。

4.5 令牌吊销与黑名单

JWT 本身无状态,无法主动失效。网关必须维护一个令牌黑名单(Token Blacklist)来实现登出、密码修改、权限变更等场景的即时失效。

令牌黑名单校验流程 HTTP 请求 解析 JWT 检查 JTI 黑名单 (Redis EXISTS) 放行 / 拒绝 Redis 黑名单 TTL = 令牌剩余有效期

4.5.1 黑名单操作示例

// 将 JTI 加入黑名单(登出/吊销时调用)
func RevokeToken(jti string, expiresAt time.Time, redisClient *redis.Client) error {
    ttl := time.Until(expiresAt)
    if ttl <= 0 {
        return nil // 已过期,无需加入黑名单
    }
    return redisClient.Set(ctx, "blacklist:"+jti, "revoked", ttl).Err()
}

// 检查 JTI 是否在黑名单中
func IsTokenRevoked(jti string, redisClient *redis.Client) bool {
    exists, _ := redisClient.Exists(ctx, "blacklist:"+jti).Result()
    return exists == 1
}
性能优化:黑名单的 TTL 设置为令牌的剩余有效期,过期后 Redis 自动清理,避免内存泄漏。对于高并发网关,可使用本地缓存(如 LRU)配合 Redis 减少网络开销。

4.6 安全审计要点

  • 签名算法白名单:验证时明确指定允许的算法,防止算法混淆攻击。
  • JTI 唯一性:每个令牌必须携带唯一 JTI,用于吊销和重放检测。
  • 短生命周期:Access Token 不超过 15 分钟,减少泄露影响窗口。
  • 轮换刷新:Refresh Token 使用一次即失效,检测重放立即吊销全部令牌。
  • 密钥管理:签名密钥定期轮换,使用 KMS 或 Vault 管理,禁止硬编码。
  • 审计日志:记录每次令牌签发、验证失败、刷新、吊销事件,包含 JTI、用户 ID、时间戳、来源 IP。