Skip to content

常见问题 ​

HMAC、RSA 和 ECDSA 怎么选? ​

答: 按架构与性能需求选:单体应用或同信任域微服务用 HMAC(对称密钥,最快,密钥 ≥ 32 字节);跨服务验证(验证端不持有私钥)用 RSA / RSA-PSS / ECDSA(非对称)。其中 RSA-PSS 安全性优于 RSA PKCS#1 v1.5 且与 RSA 共用同一套密钥,新项目优先选它;对签名长度或密钥长度敏感(签名 ~64 字节)选 ECDSA。对比表与决策图见签名算法。

HMAC 密钥有什么要求? ​

答: 两项检查都在 jwt.New 初始化时执行,不合法返回 ErrInvalidSecretKey:长度 ≥ 32 字节;不能是低熵密钥——全部相同字符、重复短模式、连续递增序列、常见弱口令及其变体均被拒绝。生产环境用密码学安全随机源生成:

go
raw := make([]byte, 32)
_, _ = rand.Read(raw) // crypto/rand
secret := base64.StdEncoding.EncodeToString(raw)

详见密钥要求。

ES512 为什么初始化时报曲线错误? ​

答: ES512 对应的曲线是 P-521(不是 P-512)——Go 标准库不存在 P512,最高位曲线就是 elliptic.P521()。曲线与算法不匹配时返回 ErrInvalidSecretKey。对应关系:ES256 → P-256、ES384 → P-384、ES512 → P-521。详见曲线匹配。

服务器之间有时钟偏差怎么办? ​

答: 设置 ClockSkew(建议 30s–60s)为 exp 与 nbf 校验提供对称的宽容窗口:令牌在 exp + ClockSkew 之后才被视为过期、在 nbf - ClockSkew 之前就已被视为生效。偏移只放宽、不收紧;零值等价于 RFC 7519 严格语义,且不允许为负数(返回 ErrInvalidConfig)。遇到 ErrTokenNotValidYet 时优先检查这一项。详见时钟偏移。

如何拒绝没有 exp 声明的令牌? ​

答: 默认情况下没有 exp 的令牌不会过期(RFC 7519 中合法)。设置 RequireExpiration = true 后,验证时拒绝缺少 exp 的令牌(返回 ErrExpirationRequired)。本库签发的令牌总是携带 exp,因此该开关主要影响外部签发方或旧令牌——生产环境建议启用。详见强制过期。

Refresh 之后原刷新令牌还有效吗? ​

答: 默认有效——Refresh 不会自动吊销原刷新令牌(重用模式),它在过期或被显式 Revoke 前可反复使用,适合移动端、单设备登录。需要一次性使用语义时启用 Config.RotateRefreshTokens(v1.2.3+):旧令牌的 jti 在新令牌签发之前被拉黑(fail-closed),已接受的刷新令牌无法事后重放。详见轮换策略。

重用模式和一次性轮换怎么选? ​

答: 按安全要求:重用模式零额外代码、用户体验好(无需频繁重登),但泄露的刷新令牌可反复使用;一次性轮换只需一个配置开关(RotateRefreshTokens),每次刷新旧令牌立即失效,泄露检测也更直接——攻击者使用已吊销令牌时 Refresh 返回 ErrTokenRevoked,可据此强制重新认证。注意轮换收窄但不消除并发重放窗口,严格单次使用需自定义 BlacklistStore 实现原子 check-and-add。对比表见策略对比。

用访问令牌去刷新为什么被拒绝? ​

答: Refresh / RefreshInto 会拒绝 access 类型的令牌(返回 ErrTokenTypeMismatch),防止访问令牌被用于获取新令牌。反过来,刷新令牌被用作访问令牌时默认不报错(兼容旧版签发的令牌)——需要拒绝时启用 Config.RejectRefreshAsAccess(v1.2.3+),此后 Parse / ParseInto 对 refresh 类型令牌同样返回 ErrTokenTypeMismatch。详见类型安全。

内置黑名单和 Redis 等外部存储怎么选? ​

答: 单实例部署用内置内存存储(默认启用,零配置;MaxSize 100000、自动清理)。多实例部署必须改用共享存储:实现 BlacklistStore 接口(Add/Contains/Close)注入 cfg.Blacklist.Store,如 Redis(用 time.Until(expiresAt) 作 TTL,令牌过期后自动移除)。注意切换到自定义存储后,CleanupInterval/MaxSize/EnableAutoCleanup 三个字段被忽略,清理与容量由后端自行负责。详见自定义存储后端。

Revoke 为什么必须传完整的令牌字符串? ​

答: Revoke 并非直接写入传入字符串,而是先执行安全验证链:验签 → 校验 iss/aud → 提取 jti → 按 exp 计算 TTL 写入存储。若直接信任调用者传入的 jti,恶意调用者就能伪造 jti 把任意合法用户的令牌拉黑(拒绝服务攻击)。强制验签保证「只有持有真实令牌的人才能吊销它」。Revoke 与 IsRevoked 都不检查 exp/nbf——已过期的令牌仍可补吊销(事后审计)。详见工作原理。

黑名单条目会无限累积吗? ​

答: 不会。Revoke 写入时按令牌 exp 计算条目 TTL,令牌到期后条目随之失效并被清理;无 exp 的令牌默认 7 天;上限 30 天(即便 exp 是 100 年后)——这是 DoS 防护,防止超长 exp 的令牌批量吊销撑爆存储。条目数达到 MaxSize 时先清理过期条目、再驱逐最早过期的约 10%,极端情况下才拒绝写入(吊销失败)。详见黑名单条目 TTL。

多实例部署时黑名单和限流为什么不生效? ​

答: 内置黑名单与内置 RateLimiter 都是进程内的:多实例各自独立存储与计数,在某实例上吊销的令牌换个实例仍会放行。解决方案都是注入共享后端:黑名单实现 BlacklistStore 对接 Redis/数据库;限流实现 RateLimitProvider(Allow/Reset/Close)对接 Redis 分布式计数。详见生产环境建议与自定义限流器。

20 个哨兵错误怎么用 errors.Is 匹配? ​

答: 全部错误支持 errors.Is() 判断(能穿透包装),不要用 err == 或字符串匹配:

go
claims, err := processor.Parse(tokenString)
if err != nil {
    switch {
    case errors.Is(err, jwt.ErrTokenExpired):
        // 引导客户端刷新令牌
    case errors.Is(err, jwt.ErrTokenRevoked):
        // 已吊销,拒绝访问
    default:
        // 其他错误
    }
}

按阶段记忆:配置(3 个,New() 返回)、令牌操作(13 个)、限流与黑名单(2 个)、生命周期(2 个)。完整清单与 HTTP 状态码映射见错误分类。

Claims 具体哪个字段校验失败怎么看? ​

答: 字段级校验失败(长度超限、注入检测等)返回 *ValidationError,用 errors.As 提取 Field 与 Message:

go
token, err := processor.Create(claims)
if err != nil {
    var ve *jwt.ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
        // 字段:user_id, 原因:suspicious pattern detected
    }
}

先用 errors.Is(err, jwt.ErrInvalidClaims) 定位类别(Create/Parse 路径会包装它),再用 errors.As 提取细节。详见错误包装链。

更多资源 ​