Echo中JWT过期默认返回500而非401,因jwt.ParseWithClaims抛出*jwt.TokenExpiredError未被中间件捕获;需用errors.As显式判断并返回401,同时配合Refresh Token实现无感续期。

为什么Echo里JWT过期会直接panic或返回500?
因为默认的 jwt.Parse(或 jwt.ParseWithClaims)在遇到过期token时,会抛出 ExpiredJwtException 类型错误(Java生态常见表述),但在Go的 github.com/golang-jwt/jwt/v5 中实际是 *jwt.TokenExpiredError。Echo中间件若没显式捕获该错误,就会被框架默认错误处理器转为500,而不是预期的401。这不是Echo的问题,而是JWT库行为与中间件逻辑没对齐。
用 jwt.ParseWithClaims + func(*jwt.Token) (interface{}, error) 显式校验exp
别依赖中间件自动解析——它通常只做基础签名验证,不主动检查 exp。你需要自己写校验逻辑:
func JWTMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "missing auth header")
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil {
// 关键:区分过期和其他错误
var e *jwt.TokenExpiredError
if errors.As(err, &e) {
return echo.NewHTTPError(http.StatusUnauthorized, "token expired")
}
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
}
if !token.Valid {
return echo.NewHTTPError(http.StatusUnauthorized, "token invalid")
}
c.Set("user", token.Claims)
return next(c)
}
}
}
- 必须用
errors.As捕获*jwt.TokenExpiredError,不能用==或字符串匹配 -
jwt.MapClaims是最简payload类型;若你用自定义struct,需确保其字段有jsontag 且包含exp字段 - 别在
func(*jwt.Token)回调里做time.Now().Unix() > exp手动判断——jwt.ParseWithClaims已内置该逻辑,重复判断反而绕过库的时钟偏移(WithTimeFunc)支持
配合Refresh Token实现无感续期(双Token方案)
单靠延长 exp 不解决根本问题。Echo后端要暴露一个 /auth/refresh 接口,接收 refresh_token 并签发新 access_token:
func refreshHandler(c echo.Context) error {
type req struct {
RefreshToken string `json:"refresh_token"`
}
var r req
if err := c.Bind(&r); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
<pre class="brush:php;toolbar:false;">// 验证refresh_token(注意:它应存于Redis或DB,且带独立过期时间)
claims, err := validateRefreshToken(r.RefreshToken)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "refresh token invalid or expired")
}
// 生成新access_token(短时效,如15m)
newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": claims["sub"],
"exp": time.Now().Add(15 * time.Minute).Unix(),
})
signedToken, _ := newToken.SignedString([]byte(secretKey))
return c.JSON(http.StatusOK, map[string]string{
"access_token": signedToken,
})}
Token 消耗追踪工具。当用户需要监控 API token 使用情况、生成每日/每周消耗报告、分析使用趋势时使用此 Skill。功能包括:自动记录每次对话的 token 消耗、生成数据表格报告、计算使用趋势和预测、支持定时推送日报。
-
validateRefreshToken必须查存储(如Redis),不能仅靠JWT解析——否则攻击者可重放旧refresh_token - 刷新成功后,务必让旧
refresh_token失效(删Redis key / 标记DB为已使用) - 前端收到401且响应体含
"token expired"时,才触发调用此接口;其他401(如签名错)不应重试
容易被忽略的时钟偏移与测试陷阱
生产环境服务器时间不准、Docker容器未同步宿主机时间、本地开发机时区混乱,都会导致 exp 判断失准。JWT库默认允许0毫秒偏移,但真实场景需要容忍几秒误差:
- 用
jwt.WithTimeFunc(func() time.Time { return time.Now().UTC() })统一时钟源 - 在
ParseWithClaims前加jwt.WithExpirationRequired()强制校验exp(默认开启,但显式声明更安全) - 测试时别用固定时间戳硬编码token;用
time.Now().Add(-5 * time.Minute)动态构造过期token,避免CI环境时间漂移导致测试随机失败
真正难的不是写刷新逻辑,而是让每个环节都信任同一个“现在”——服务端、客户端、Redis、Nginx代理,甚至CDN缓存头里的 Date,都得对齐。










