必须改用服务端可主动失效的混合方案,因JWT默认不支持踢人,需Redis存储用户唯一token并强制覆盖,登录时删旧存新,中间件校验一致性,登出时同步清理。

直接限制单账号多端登录,必须放弃纯无状态 JWT 的默认模式,改用“服务端可主动失效”的混合方案。JWT 本身不支持踢人,所有“限制多端”逻辑都得靠额外存储 + 中间件拦截来补。
为什么不能只靠 JWT 默认行为
echo-jwt/v4 默认只校验签名和 exp/nbf 时间字段,它根本不知道这个 token 是不是已被用户在别处登出。只要没过期、签名正确,就放行——这和“单设备登录”需求天然冲突。
常见错误是试图用 jwt.MapClaims 里的 user_id 去查 Redis 当前登录数,但:
- 查到多个 token 不代表都能用(可能已过期或被手动撤销)
- 查不到也不代表安全(刚签发、还没写入 Redis)
- 并发时写入延迟会导致漏判
用 Redis 存 user_id → token 映射并强制覆盖
核心思路:每个用户只允许一个有效 token,新登录时删旧的,后续请求用旧 token 就直接 401。
关键点:
- 登录成功后,先用
redis.Del("user_token:" + userID)清掉旧 token - 再用
redis.Set("user_token:" + userID, newToken, expireTime)写入新 token - 中间件校验时,从 context 取出解析后的
token,提取claims["sub"]或claims["user_id"],再redis.Get("user_token:" + userID)对比是否一致 - 不一致就返回
echo.NewHTTPError(http.StatusUnauthorized, "login conflict")
注意:userID 必须是业务唯一标识(如数据库主键),不能是昵称或邮箱(可能重复或含特殊字符);Redis key 建议加统一前缀,如 user_token:123。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
在 echo-jwt 后链式插入校验中间件
别动 echojwt.WithConfig(),就在它后面注册一个自定义中间件:
e.Use(echojwt.WithConfig(jwtConfig))
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user")
if user == nil {
return next(c)
}
token, ok := user.(*jwt.Token)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token type")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid claims")
}
userID := fmt.Sprintf("%v", claims["sub"]) // 或 "user_id"
storedToken, _ := redisClient.Get(c.Request().Context(), "user_token:"+userID).Result()
if storedToken != token.Raw {
return echo.NewHTTPError(http.StatusUnauthorized, "login conflict")
}
return next(c)
}
})
这里 token.Raw 是原始字符串,和 Redis 里存的完全一致,避免 JSON 序列化差异导致误判。
客户端登出时必须同步清理 Redis
登出接口不能只清前端 localStorage,后端必须调 redis.Del("user_token:" + userID)。否则下次用旧 token 请求,仍会通过校验。
容易忽略的细节:
- 登出接口本身也要受 JWT 中间件保护,否则恶意用户能批量删别人 token
- 如果用
jti字段做黑名单,它只管单个 token 失效,不管“当前谁在线”,不适合本场景 - Redis 操作失败(如网络抖动)应记录日志但不阻断请求,避免雪崩;可用
SET user_token:123 "xxx" EX 3600 NX带原子性保障
真正难的不是代码怎么写,而是想清楚“冲突发生时,该以哪次登录为准”。比如用户 A 在手机登出、B 在网页登录,B 登录成功后立刻触发 A 的 token 失效——这个时间差内,A 的请求仍可能被处理。业务上要接受这种短暂窗口,或加双检查机制。

















