长连接中JWT无状态认证必须放弃纯无状态设计,引入服务端状态管理;需用refresh_token绑定连接生命周期,建连时校验并生成短期session_id存Redis,后续消息仅校验session_id存在性。

长连接场景下,JWT 无状态认证不能靠“刷新”解决,必须放弃纯无状态设计,引入服务端状态管理。 WebSocket 或 gRPC streaming 等长连接通道无法像 HTTP 那样自然拦截 401 并重发请求,前端没法静默换 access_token,后端也不能在连接中途“中断重连”。所谓“无感刷新”在长连接里根本不存在——你得主动设计心跳、重鉴权、连接迁移机制。
为什么 WebSocket 里不能复用 HTTP 的 /refresh 接口
HTTP 刷新依赖响应头(如 X-Token-Expired: true)和前端拦截器 Promise 缓存,但长连接没有“响应头”概念,也没有统一的请求/响应生命周期。你发一个 {"type":"ping"} 消息,服务端没法塞个新 token 进响应头里。
- WebSocket 消息是双向异步的,
/auth/refresh这类 REST 接口无法被客户端自动触发,除非你额外实现一套消息协议来协商刷新 - 如果强行在长连接中嵌套 HTTP 请求(比如 conn.WriteJSON({type:"refresh_request"})),等于把状态逻辑又拉回 HTTP 层,违背了长连接初衷
- 更严重的是:长连接一旦建立,
access_token就绑定在连接上下文里;token 过期后,你无法“局部更新”它——整个连接的身份凭证已失效
长连接必须用 Refresh Token 绑定连接生命周期
别再把 refresh_token 当成 HTTP Cookie 里可轮换的字符串。在长连接中,它得是连接的“身份锚点”,且必须和服务端状态强绑定。
- 建连时,客户端必须携带
refresh_token(走初始 HTTP Upgrade 请求的Authorization头或 query 参数),服务端解析出jti后立即查 Redis:refresh:{user_id}:{jti}是否存在且未被标记为used - 验证通过后,服务端生成一个短期有效的
session_id(非 JWT),存入 Redis:ws_session:{session_id},TTL 设为 15–30 分钟,并关联该refresh_token的jti - 后续所有 WebSocket 消息都带这个
session_id,服务端只校验 Redis 中该 key 是否存在,不解析 JWT —— 避免每次 decode 开销,也绕过时钟偏移问题 - 心跳包(如每 30 秒
{"type":"heartbeat","ts":1747948820})由服务端检查ws_sessionTTL 剩余时间,若 {"type":"reauth_required"} 消息
并发重连和重复鉴权怎么防
用户切后台再回来、网络抖动、多端登录,都会导致多个长连接并存,而你的 refresh_token 是单次有效的——不能让新连接成功就把旧连接踢掉,那体验太差;也不能放任旧连接继续用,那有安全风险。
立即学习“go语言免费学习笔记(深入)”;
- Redis Key 必须含设备指纹:
refresh:{user_id}:{fingerprint_hash},其中fingerprint_hash由sha256(UserAgent + RealIP + DeviceID)生成,避免同一账号在不同设备上互踢 - 每次新连接成功,先
DEL refresh:{user_id}:{fingerprint_hash},再SETEX新值;若 DEL 返回 0,说明该设备已被登出或换绑,直接拒绝建连 - 旧连接收到
reauth_required后,应主动关闭自己,并触发前端重新走登录流程(不是静默刷新),因为长连接无法安全续期——这是设计取舍,不是缺陷 - 不要在 WebSocket handler 里调用
jwt.ParseWithClaims解析原始refresh_token,只信任建连时已校验过的jti和 Redis 中的状态;否则并发建连可能因查库顺序错乱导致双签发
真正难的不是怎么生成新 token,而是怎么让长连接在身份过期时“体面退场”:它不报错,不卡死,不丢数据,也不让用户意识到自己被登出了。这要求你在协议层就定义好重连语义,而不是指望 JWT 自己扛住所有边界。


















