3、JWT安全实践:JWT签名算法选择、密钥管理、过期时间、刷新令牌机制

JWT(JSON Web Token)作为分布式鉴权的核心载体,其安全性直接决定了整个权限体系的可靠性。本章从签名算法、密钥管理、过期策略与刷新机制四个维度,系统讲解生产级JWT安全实践。

3.1 JWT签名算法选择

JWT支持多种签名算法,按类型可分为对称算法(HMAC)与非对称算法(RSA/ECDSA)。算法选择不当会导致令牌伪造或性能瓶颈。

算法 类型 密钥 性能 安全等级 推荐场景
HS256 对称 单一密钥 高 中 内部服务间通信
RS256 非对称 公私钥对 中 高 跨服务/跨组织鉴权
ES256 非对称 公私钥对 高 高 移动端/物联网设备
PS256 非对称 公私钥对 中 极高 高安全合规场景
安全警示:禁止使用 alg: "none" 或 alg: "HS256" 配合公钥验证,这是经典的 JWT 算法混淆攻击向量。服务端必须白名单校验 alg 字段。
JWT 签名算法选择决策流程 开始选择算法 是否跨服务/跨组织? HS256(对称) RS256 / ES256 对称算法密钥分发困难,非对称算法支持数字签名分离验证 ES256 性能优于 RS256,推荐新项目优先选用

3.2 密钥管理最佳实践

密钥是JWT安全的核心资产。密钥泄露意味着整个鉴权体系失效。以下是生产级密钥管理要点:

  • 密钥长度:HS256至少256位(32字节),RS256至少2048位,ES256至少256位。
  • 密钥轮换:定期更换密钥,保留旧密钥窗口期用于验证未过期令牌。
  • 密钥存储:使用密钥管理服务(KMS/Vault),禁止硬编码或明文存储。
  • 密钥隔离:不同环境(开发/测试/生产)使用独立密钥。
推荐方案:使用 kid(Key ID)字段标识密钥版本,服务端根据 kid 动态获取对应公钥,实现无缝密钥轮换。
// Go 语言 JWT 密钥轮换示例
type KeyManager struct {
    keys map[string]*rsa.PublicKey
    currentKid string
}

func (km *KeyManager) Verify(tokenString string) (*jwt.Token, error) {
    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, errors.New("missing kid")
        }
        key, exists := km.keys[kid]
        if !exists {
            return nil, errors.New("unknown kid")
        }
        return key, nil
    })
    return token, err
}

3.3 过期时间设计策略

JWT的 exp(过期时间)声明是防止令牌长期有效被滥用的关键防线。过期时间需要平衡安全性与用户体验。

令牌类型 推荐过期时间 安全考量
Access Token 15分钟 - 1小时 短生命周期,减少泄露影响
Refresh Token 7天 - 30天 长生命周期,需配合存储验证
一次性令牌 5分钟 密码重置、邮箱验证等场景
常见陷阱:仅依赖客户端检查 exp 是不够的。服务端必须在每次请求时验证 exp、nbf(不早于)和 iat(签发时间)三个时间声明,防止篡改。

3.4 刷新令牌机制

刷新令牌(Refresh Token)机制解决了Access Token过期后用户需要重新登录的问题,同时保持了短生命周期令牌的安全性。

刷新令牌完整交互流程 客户端 认证服务 资源服务 1. 登录获取令牌 2. 携带Access Token 3. 返回401过期 4. 客户端收到401 5. 发送Refresh Token 验证Refresh Token 7. 返回新Access Token 8. 使用新令牌重新请求 Refresh Token 必须存储在安全位置(HttpOnly Cookie / 安全存储),并支持撤销机制

3.5 刷新令牌安全实现

刷新令牌的实现需要额外关注以下安全要点:

  • 存储验证:服务端必须存储Refresh Token的哈希值,每次刷新时比对,防止令牌泄露后无限使用。
  • 轮换策略:每次刷新时签发新的Refresh Token并废弃旧令牌,防止重放攻击。
  • 撤销机制:维护黑名单或版本号,支持用户主动登出或管理员强制撤销。
  • 设备绑定:将Refresh Token与设备指纹、IP地址绑定,异常时要求重新认证。
// Go 语言刷新令牌轮换实现
func (s *AuthService) RefreshToken(oldRefreshToken string) (*TokenPair, error) {
    // 1. 验证旧Refresh Token
    claims, err := s.validateRefreshToken(oldRefreshToken)
    if err != nil {
        return nil, ErrInvalidToken
    }
    
    // 2. 检查是否已被撤销
    if s.isRevoked(claims.JTI) {
        // 检测到重放攻击,撤销该用户所有令牌
        s.revokeAllUserTokens(claims.UserID)
        return nil, ErrTokenReused
    }
    
    // 3. 撤销旧令牌
    s.revokeToken(claims.JTI)
    
    // 4. 签发新令牌对
    accessToken, err := s.generateAccessToken(claims.UserID)
    if err != nil {
        return nil, err
    }
    refreshToken, err := s.generateRefreshToken(claims.UserID)
    if err != nil {
        return nil, err
    }
    
    return &TokenPair{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
    }, nil
}
生产建议:Access Token 使用 JWT 自包含格式,无需服务端存储;Refresh Token 使用不透明字符串(随机生成),服务端存储其哈希值。这种混合架构兼顾了性能与安全性。