WebSocket鉴权必须在握手阶段完成,因连接建立后资源已占用且无法拦截恶意行为;推荐URL传短期专用Token或Header+HandshakeInterceptor方式,并需Redis联动吊销。

WebSocket 鉴权必须在握手阶段完成,不能等连接建立后再校验——否则未授权连接已存在,可能被用于广播攻击或资源耗尽。
为什么不能连接后再发登录消息?
很多人习惯让前端先 new WebSocket("wss://..."),再立刻 socket.send({ type: "login", token: "xxx" }),等服务端回 “ok” 才算鉴权成功。这看似合理,但实际已埋下风险:
- 连接已建立,TCP 和 WebSocket 层资源已被占用(内存、文件描述符、线程)
- 恶意客户端可绕过后续逻辑,直接发大量无效消息或伪造用户 ID
- 服务端若未做连接数限制,可能被批量空连接打满
- 无法利用网关/Nginx 层做统一拦截(它们只管握手请求)
URL 参数传 Token 是最简单可靠的方案
浏览器原生 WebSocket 构造函数只支持 URL,不支持自定义 header,所以把 Token 放 query string 是兼容性最好、落地最快的方式:
前端写法:new WebSocket("wss://api.example.com/ws?token=eyJhbGciOi...")
后端(如 Node.js + ws 库)需在 connection 事件里立即解析并校验:
- 从
req.url提取token查询参数,注意 URL decode - 用 JWT 库验证签名、
exp(必须检查!)、iss、aud - 额外查一次 Redis 或数据库,确认该 Token 未被主动吊销(如用户登出)
- 校验失败直接
socket.close(4001, "Unauthorized"),不要socket.send()
⚠️ 注意:必须走 HTTPS,否则 Token 会被中间人截获;也别把长期登录 Token 直接塞进去,应调用 /api/v1/ws-token 换一个 15 分钟有效期的专用 Token。
Spring Boot 中用 HandshakeInterceptor 做 header 鉴权
如果你用 Spring WebSocket,并且网关(如 Nginx)或前端代理能注入 header,推荐用 HandshakeInterceptor 拦截握手请求:
- 重写
beforeHandshake(),从request.getHeaders().getFirst("Authorization")或自定义头(如X-Auth-Token)取值 - 调用
StpUtil.checkToken()或手动解析 JWT,失败则返回false(Spring 会自动返回 403) - 校验通过后,把用户 ID 存进
attributes,后续@OnMessage方法可通过Session.getUserProperties()拿到 - 别在
@OnOpen里做鉴权——此时连接已建立,只是还没触发业务逻辑
这个方式比 URL 参数更安全(header 不进浏览器历史、不被 CDN 缓存),但要求你控制得了请求链路(比如用 Nginx 加 proxy_set_header X-Auth-Token $http_authorization;)。
Token 失效和吊销必须和服务端状态联动
JWT 自带 exp,但仅靠它不够。用户点击“退出登录”时,当前 WebSocket Token 必须立刻失效,否则连接还能继续用:
- 生成 WS Token 时,把它哈希后存入 Redis,设置和 Token 本身相同的过期时间(如
SET ws:sha256(token) 1 EX 900) - 握手校验时,除了 JWT 签名和
exp,再GET ws:sha256(token),返回 nil 就拒绝 - 用户登出接口要同步
DEL ws:sha256(token),或用布隆过滤器做轻量级黑名单 - 别依赖内存 Map 存在线 Token 列表——分布式部署下节点间不同步
真正容易被忽略的点是:Token 吊销不是“删掉数据库记录”就完事,它必须在握手那一刻生效;而 WebSocket 连接一旦通过,后续所有消息都不再校验 Token——所以握手是唯一可信的防线。

















