结论:Echo中JWT过期处理不能仅依赖jwt.Parse异常,必须显式检查errors.Is(err, jwt.ErrTokenExpired)并手动校验exp字段,否则会因ExpiredSignatureError导致500而非401,中断刷新流程;Parse默认校验exp并直接返回特定error,token为nil时Claims强转失败。

直接说结论:Echo 框架里处理 JWT 过期,不能只靠 jwt.Parse 抛异常来判断,必须提前检查 exp 字段并结合时钟偏移(clock skew),否则会因 ExpiredJwtException 类型错误中断中间件流程,导致 401 响应无法被前端统一捕获或刷新。
为什么 jwt.Parse 会 panic 而不是返回 error?
Go 的 github.com/golang-jwt/jwt/v5(当前主流版本)默认在解析时就校验 exp、nbf 等时间声明。一旦发现过期,直接返回 *jwt.ExpiredSignatureError(注意:不是普通 error,而是实现了 error 接口的特定类型),且该错误在 Parse 阶段就触发,不会进入后续 token.Valid 判断分支。
常见错误现象:
- 中间件里写
if err != nil { c.JSON(401, "invalid token") },但实际日志里看到 panic 或空响应 - 前端收到 500 而非 401,无法触发刷新逻辑
- 用
token.Claims.(jwt.MapClaims)强转失败,因为token本身是nil
正确做法是显式启用宽松解析 + 手动校验:
token, err := jwt.Parse(tokenString, keyFunc)
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return c.JSON(401, map[string]string{"error": "token expired"})
}
return c.JSON(401, map[string]string{"error": "invalid token"})
}
if !token.Valid {
return c.JSON(401, map[string]string{"error": "token invalid"})
}
如何在 Echo 中安全提取 exp 并做预判?
不要等 Parse 报错,先解码 Payload(无签名验证)拿到 exp 时间戳,再和当前时间比对。这样既能避免 panic,又能为自动续期留出判断窗口(比如剩余
使用场景:
- 需要在过期前 3 分钟静默刷新 access token
- 配合 Redis 存储 refresh token 时,需提前拦截已过期请求
- 调试阶段快速定位是过期还是签名错
示例代码(不依赖完整解析):
func getExpFromToken(tokenStr string) (int64, error) {
parts := strings.Split(tokenStr, ".")
if len(parts) != 3 {
return 0, errors.New("invalid token format")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return 0, err
}
var claims map[string]interface{}
if err := json.Unmarshal(payload, &claims); err != nil {
return 0, err
}
if exp, ok := claims["exp"].(float64); ok {
return int64(exp), nil
}
return 0, errors.New("exp not found")
}
Refresh Token 接口在 Echo 中怎么防重放+安全存储?
单纯返回新 access_token 不够,必须绑定设备指纹或限制单次使用。否则攻击者截获 refresh token 就能无限续期。
关键参数差异:
-
refresh_token必须存服务端(如 Redis),key 为refresh:{userId}:{fingerprint},设置 TTL 7 天 - 每次成功刷新后,立即
DEL旧 refresh token,并写入新 token(可选) - 客户端传来的
fingerprint应基于 User-Agent + IP 哈希(非明文),避免暴露真实 IP - 响应头加
Cache-Control: no-store,禁止浏览器缓存 refresh 响应
性能影响:
- 每次刷新都查 Redis + DEL + SET,RT 增加约 2–5ms(本地 Redis)
- 高并发下建议用 Lua 脚本原子执行校验+删除+写入
前端拿不到新 token?检查 Echo 的 CORS 和 Cookie 设置
如果前端用 fetch 带 credentials: 'include',但响应里没带 Set-Cookie,大概率是 Echo 的 CORS 中间件没配对。
容易踩的坑:
-
c.SetCookie()默认HttpOnly=false,但前端 JS 读不到;若设为true,又无法通过 JS 获取新 token - 推荐方案:refresh 接口返回 JSON,前端用
localStorage存新access_token,refresh_token存httpOnlyCookie(需后端 set) - CORS 配置里漏了
AllowCredentials: true,或AllowOrigins写成*(与 credentials 冲突)
复杂点在于:refresh token 的有效期、存储位置、是否单次有效,这三者必须同步设计。漏掉任一环,都会让“无感刷新”变成“偶发登出”。


















