Echo v5 中 echo-jwt/v4 不支持自动 Token 刷新,因其仅负责校验 token 合法性,不管理生命周期、不提供刷新接口、不感知用户身份、不修改响应头、不区分 access/refresh token,所有刷新逻辑须手动实现。

Echo v5 中没有内置 Token 刷新逻辑,必须自己实现中间件 + 两个 endpoint(/login 和 /refresh),且不能依赖 echo-jwt/v4 自动处理刷新。
为什么 echo-jwt/v4 不支持自动刷新
echo-jwt/v4 的设计目标是「校验」而非「管理生命周期」:它只做一件事——解析并验证 Authorization 头里的 token 是否合法、未过期、签名正确。一旦 err != nil,它直接返回 401 或 400,不会触发任何续签行为。
常见误解是以为配置 SigningKey 后加个回调就能“自动刷新”,但实际:echojwt.Config 里根本没有 RefreshHandler、OnTokenExpired 这类字段;所有刷新动作必须由你手动路由和控制。
- 它不感知用户身份,无法查数据库或 Redis 获取 refresh token
- 它不修改响应头,不会自动写入新
Authorization或Set-Cookie - 它不区分 access token 和 refresh token,传进来什么就验什么
/refresh 接口必须独立实现,且需校验 refresh token
刷新不是“重签一个新 access token”那么简单,关键在安全边界:refresh token 必须比 access token 更长寿命、更严格存储、单次使用或带绑定(如 IP、UA、设备指纹)。
典型流程是:
- 前端在收到 401 且确认是过期(非非法格式)时,用当前 access token 的 payload 提取
user_id,再带上本地存的 refresh token 请求/api/v1/refresh - 后端验证 refresh token 是否存在、未被撤销、未过期、且与该
user_id匹配 - 验证通过后,签发新的 access token(建议短时效,如 15m),并可选地签发新的 refresh token(滚动刷新)
- 旧 refresh token 应立即失效(例如写入 Redis 黑名单,或更新 DB 中的
refresh_token_hash字段)
示例片段(不依赖任何框架封装):
e.POST("/api/v1/refresh", func(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;">// 1. 解析 refresh token(用独立密钥!别和 access token 共用 SigningKey)
token, err := jwt.Parse(r.RefreshToken, 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("refresh-secret-key"), nil
})
if err != nil || !token.Valid {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid refresh token")
}
// 2. 检查 payload 中的 user_id 和 jti(唯一标识),查 Redis/DB 确认未撤销
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !claims.VerifyExpiresAt(time.Now().Unix(), true) {
return echo.NewHTTPError(http.StatusUnauthorized, "refresh token expired")
}
userID := claims["user_id"].(string)
jti := claims["jti"].(string)
if !isRefreshTokenValid(userID, jti) { // 你自己的校验函数
return echo.NewHTTPError(http.StatusUnauthorized, "refresh token revoked")
}
// 3. 签发新 access token(HS256,15分钟)
newAccessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(15 * time.Minute).Unix(),
"iat": time.Now().Unix(),
})
signedToken, _ := newAccessToken.SignedString([]byte("access-secret-key"))
// 4. 可选:生成新 refresh token 并使旧的失效
newRefreshToken := generateRefreshToken(userID)
return c.JSON(http.StatusOK, map[string]string{
"access_token": signedToken,
"refresh_token": newRefreshToken,
})})
TokenLookup 和 ErrorHandler 要配合刷新逻辑用
默认 TokenLookup: "header:Authorization" 只会从请求头读,但 /refresh 接口通常需要从 body 或 query 读 refresh token,所以这个接口不能挂 echojwt.WithConfig(...) ——否则还没进你 handler 就被中间件拦下返回 401 了。
同时,ErrorHandler 别只返回 401 文字,要能区分错误类型,方便前端决策:
- 如果错误是
*jwt.TokenExpiredError,前端才该去调/refresh - 如果是
*jwt.ValidationError(比如签名错、算法不匹配),说明 token 被篡改,必须强制登出 - 如果是
context.Canceled或网络超时,不该触发刷新
所以推荐在全局中间件里这样写 ErrorHandler:
ErrorHandler: func(c echo.Context, err error) error {
var ve *jwt.ValidationError
if errors.As(err, &ve) {
if ve.Errors&jwt.ValidationErrorExpired != 0 {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "token_expired",
"action": "refresh",
})
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "token_invalid",
"action": "logout",
})
}
return c.JSON(http.StatusBadRequest, map[string]string{"error": "malformed_token"})
},
真正难的不是签两个 token,而是 refresh token 的存储、吊销、绑定和轮换策略——这些 echo-jwt/v4 一概不管,得你自己用 Redis 或数据库兜底。漏掉任意一环,刷新机制就变成安全漏洞放大器。











