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 从签发、传输、验证到刷新的完整生命周期,以及网关在其中的拦截位置。
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)来实现登出、密码修改、权限变更等场景的即时失效。
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。