websocket鉴权必须在upgrader.upgrade()前完成,从r.url.query()或r.header.get("authorization")提取凭据并校验,失败立即返回401;upgrade后ctx失效,中间件无法拦截,严禁在升级后或checkorigin中做鉴权。

WebSocket 连接建立前必须完成授权校验,否则攻击者可绕过鉴权直连,导致未授权访问、连接泛滥甚至账号冒用。Gin 本身不拦截 WebSocket 升级请求的中间件链,ctx.Next() 在 Upgrade() 后完全失效——这是绝大多数人踩坑的起点。
为什么 Gin 中间件对 WebSocket 升级无效
Gin 的中间件只作用于 HTTP 请求生命周期,而 websocket.Upgrader.Upgrade() 会调用 http.Hijack(),接管底层 TCP 连接,强制终止 HTTP 响应流程。一旦执行 Upgrade(),后续所有 Gin 中间件(包括 AuthMiddleware)都不再运行,ctx.Abort() 和 ctx.JSON() 也会 panic 报错 http: response.WriteHeader on hijacked connection。
常见错误写法:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Query("token") // 或从 header 取
if !isValidToken(token) {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort() // ❌ 此处 Abort 无效!Upgrade 已 hijack 连接
return
}
c.Next() // ❌ Next 之后再 Upgrade 仍会失败
}
}
r.GET("/ws", AuthMiddleware(), handleWebSocket) // ❌ 错误:中间件无法约束 Upgrade 行为
正确校验时机:Upgrade 前完成全部鉴权
所有校验逻辑必须在调用 upgrader.Upgrade() 之前完成,且不能向 http.ResponseWriter 写入任何字节(包括空格、换行、c.String())。校验失败直接返回 HTTP 状态码并 return。
- 从
c.Request.URL.Query()或c.Request.Header.Get("Authorization")提取凭证 - 校验 token 有效性、过期时间、绑定设备/IP(如需)
- 检查用户状态(是否被禁用、是否已登录其他端)
- 校验通过才允许调用
upgrader.Upgrade(),否则用c.AbortWithStatusJSON(401, ...)或c.Status(401)
示例片段:
func handleWebSocket(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.AbortWithStatusJSON(400, gin.H{"error": "missing token"})
return
}
userID, err := parseAndValidateToken(token)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"})
return
}
// ✅ 此时才升级,且确保前面没写任何响应体
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("upgrade failed: %v", err)
return
}
defer conn.Close()
// 启动读写 goroutine,传入 userID 做后续业务关联
go readPump(conn, userID)
writePump(conn, userID)
}
如何安全传递用户上下文到 WebSocket goroutine
HTTP handler 结束后,gin.Context 不再可用,不能把 c 或其字段(如 c.Keys)直接传给长期运行的 goroutine。必须提取必要信息(如 userID、role、clientIP)作为参数传入。
注意点:
- 避免在 goroutine 中调用
c.GetString()或访问c.Request—— 它们可能已被回收 - 若需动态刷新权限(如后台踢人),应将用户 ID 注册进全局 map,并在
readPump中定期 check 状态,而非依赖初始值 - 敏感信息(如原始 token)不要保留,只存解码后的
userID和有效期
连接管理建议:
// 全局注册表,用 sync.Map 避免锁竞争
var clients = sync.Map{} // userID → *websocket.Conn
func readPump(conn *websocket.Conn, userID string) {
defer func() {
clients.Delete(userID)
conn.Close()
}()
clients.Store(userID, conn)
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
// 处理消息,可结合 userID 查询 DB、发通知等
}
}
生产环境必须关闭 CheckOrigin 的 true 返回
CheckOrigin: func(r *http.Request) bool { return true } 是开发快捷写法,但上线即成 XSS 和连接劫持入口。浏览器会携带 Origin 头,必须显式比对白名单域名:
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
// 支持多个域名,或用正则匹配子域
return origin == "https://your-app.com" ||
origin == "https://admin.your-app.com"
},
}
更严格的做法是结合后端 session 或 JWT 中的 iss 字段做二次校验,防止恶意构造 Origin 绕过。
真正难的不是写通 WebSocket,而是让每个连接都带着可信身份进来,并且这个身份在长达数小时的连接生命周期里始终可控。漏掉一次校验、多传一个指针、少删一个 map 条目,都可能变成线上事故的伏笔。











