不能只用 jwt.Parse 验证 Token,因其默认不校验 exp、nbf 时间且无法识别已撤销 Token;必须显式传入 jwt.WithValidTime() 并检查 token.Valid,同时动态选密钥、安全断言 Claims 类型。

为什么不能只用 jwt.Parse 验证 Token?
因为 jwt.Parse 默认不校验 exp 字段是否过期,也不检查 nbf(not before)时间,更不会自动拒绝已撤销的 Token。实际接口中,你看到的 token is invalid 错误,大概率是没调用 token.Valid 或漏了 jwt.WithValidTime 选项。
正确做法是显式启用时间验证,并结合自定义校验逻辑:
token, err := jwt.Parse(tokenString, keyFunc, jwt.WithValidTime())
if err != nil || !token.Valid {
return errors.New("invalid or expired token")
}
- 必须传入
jwt.WithValidTime(),否则token.Valid永远为true(即使已过期) -
keyFunc应根据token.Header["kid"]动态选择密钥,避免硬编码导致轮换失败 - 别在中间件里直接用
token.Claims断言为jwt.MapClaims—— 应先断言类型再取值,否则 panic
刷新 Token 时为何要双 Token(Access + Refresh)?
单 Token 方案无法安全实现“无感续期”:如果 Access Token 过期后还允许刷新,攻击者拿到旧 Token 就能无限续;如果禁止刷新,用户就得重新登录。双 Token 是平衡安全与体验的最小可行设计。
关键约束必须落地到代码里:
- Refresh Token 必须存储在服务端(如 Redis),带过期时间(比如 7 天),且每次使用后立即失效并生成新 Refresh Token
- Access Token 设为短时效(15–30 分钟),不存服务端,仅用于快速鉴权
- 刷新接口必须校验 Refresh Token 的
jid(JWT ID)是否匹配 Redis 中记录的值,防止重放 - 不要把 Refresh Token 放在 Cookie 里自动发送 —— 它应由前端显式携带在请求体中,避免 CSRF 泄露
http.HandlerFunc 中如何安全提取并验证 Bearer Token?
常见错误是直接从 r.Header.Get("Authorization") 取值后截取字符串,结果遇到大小写混用(bearer xxx)、多余空格、或缺失前缀时静默失败。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
建议封装一个健壮的提取函数:
func parseBearerToken(r *http.Request) (string, error) {
auth := r.Header.Get("Authorization")
if auth == "" {
return "", errors.New("missing Authorization header")
}
parts := strings.Fields(auth)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return "", errors.New("invalid Authorization header format")
}
return parts[1], nil
}
- 用
strings.Fields而不是strings.Split(auth, " "),自动处理多个空格 - 对前缀做
strings.ToLower比较,兼容各种大小写写法 - 别在提取后立刻解析 JWT —— 先校验格式,再进鉴权逻辑,避免无效字符串触发解析开销
Redis 存储 Refresh Token 时哪些字段不能省?
只存 Token 值本身(如 SET refresh:abc123 "uid:123|iat:1718234567")看似简单,但会导致无法做设备绑定、无法主动踢出、也无法区分 Token 来源。
至少应序列化为 JSON 并设置 TTL:
refreshData := map[string]interface{}{
"uid": 123,
"device": "ios-iphone14",
"ip": r.RemoteAddr,
"iat": time.Now().Unix(),
"jti": uuid.New().String(), // 唯一标识,用于防重放
}
data, _ := json.Marshal(refreshData)
redisClient.Set(ctx, "refresh:"+jti, data, 7*24*time.Hour)
-
jti(JWT ID)必须来自 Refresh Token 的 payload,不能由服务端生成后覆盖 —— 否则无法关联原始签发行为 - 不要用 UID 作为 Redis key(如
refresh:123)—— 一个用户多设备登录时会互相覆盖 - TTL 必须显式设置,且略长于 Refresh Token 的声明过期时间(比如 Token 设 7 天,Redis 设 7 天 1 小时),避免因时钟偏差导致提前失效
双 Token 流程真正难的不是签发和解析,而是 Refresh Token 的生命周期管理 —— 每次刷新都要删旧建新、记录设备指纹、预留踢出入口。少一步,就可能让“自动续期”变成“永久凭据”。










