鉴权必须在Upgrader.Upgrade()调用前完成:先从Header或URL Query提取JWT,同步校验签名与有效期,失败立即返回401;禁用CheckOrigin做鉴权;绑定用户身份应通过AuthConn结构体,避免闭包捕获request。

鉴权必须在 Upgrader.Upgrade() 调用前完成
Go 的 gorilla/websocket 或标准库升级流程不提供“拦截器”概念,Upgrade() 一旦成功,HTTP 连接就切换为 WebSocket 帧通道,此时无法再返回 401 或 403。常见错误是把 ParseWithClaims() 放在 conn.ReadMessage() 循环里——非法用户早已连上、占着 fd、能发心跳、甚至触发广播逻辑。
正确做法是在 handler 函数开头就提取并校验凭据:
- 优先从
r.Header.Get("Authorization")读取,格式应为"Bearer xxx"(注意空格) - fallback 到
r.URL.Query().Get("token"),前端需用encodeURIComponent()编码 JWT - 校验必须同步:调
jwt.ParseWithClaims()后,**同时检查err == nil和token.Valid**,否则过期但签名有效的 token 会被放过 - 失败立即
http.Error(w, "Unauthorized", http.StatusUnauthorized)并return,确保响应头未被写入
别在 CheckOrigin 里做鉴权
Upgrader.CheckOrigin 只负责 Origin 白名单,不是鉴权钩子。把它当鉴权入口会埋下两个坑:
- 返回
false时,gorilla/websocket固定返回"origin not allowed"错误,前端无法区分是跨域还是未授权 - 若你硬塞鉴权逻辑进去,一旦数据库查慢或 JWT 解析卡住,握手超时,连接直接断开,日志里只看到
timeout,问题难定位
Origin 校验应独立且轻量:用 url.Parse() 提取 scheme://host,查 map[string]bool 白名单,O(1) 完成。生产环境禁用通配符,也别只比 Host 字段。
立即学习“go语言免费学习笔记(深入)”;
用户身份如何安全绑定到连接生命周期
鉴权通过后,*http.Request 对象在 Upgrade() 后就不可靠了——底层 TCP 连接已被接管,它的 Context()、Header、URL 都可能失效。别用闭包捕获 r,更别存全局 map[*websocket.Conn]UserID(并发读写要锁,且易泄漏)。
稳妥做法是定义一个包装结构体:
type AuthConn struct {
*websocket.Conn
UserID string
Role string
}
在 Upgrade() 成功后立刻构造:
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
authConn := &AuthConn{
Conn: conn,
UserID: userID,
Role: role,
}
后续所有读写 goroutine 都用这个 AuthConn,用户信息随连接生命周期自然释放。
浏览器客户端传 Token 的实际限制
原生 new WebSocket(url) 不支持自定义 header,所以 Authorization 头方案在浏览器里基本不可用(fetch + Response.upgrade() 是 Chromium 专属,Firefox 不支持)。最通用的路径只有 query 参数。
- 前端拼 URL 时必须
encodeURIComponent(token),JWT 中的.和+会被误解析 - 后端用
r.URL.Query().Get("token")读取,别用r.FormValue()(它会尝试解析 body,WebSocket 握手请求无 body) - 如果非要用 header,得靠反向代理中转:Nginx 配置
proxy_set_header Authorization "Bearer $arg_token";,后端再从r.Header.Get("Authorization")读
真正容易被忽略的是:token 必须短期有效(建议 ≤15 分钟),且服务端要查 Redis 黑名单确认未吊销——否则一次泄露,永久有效。


















