在 Go 语言里开发实时通信功能时,WebSocket 是很常用的选择。不过很多项目在接入鉴权这一步容易埋下隐患,尤其是把用户身份校验和连接逻辑混在一起,或者错误地依赖 URL 参数来传递令牌。本文从握手阶段入手,说明一套清晰且可落地的认证方案。

为什么不能用查询参数传递 Token
不少初学者在建立 WebSocket 时习惯写成 ws://host/ws?token=xxx 这种形式,因为前端写起来简单。但这类地址会出现在浏览器历史、服务端访问日志、反向代理记录里,令牌一旦被打印到日志就等同于明文泄露。即便使用 wss 加密传输,URL 本身在部分中间层仍可能被记录。
更合理的做法是把凭证放在 HTTP 请求头中,例如 Authorization: Bearer <token>。WebSocket 握手本质上首先是一次 HTTP 请求,服务端可以在响应升级之前读取头部并完成校验。这样日志中不会携带具体令牌,也符合常规 REST 接口的鉴权习惯。
基于 gorilla/websocket 的握手鉴权
Go 生态中 gorilla/websocket 是主流库。我们可以在 Upgrader 的 CheckOrigin 之外,在路由处理函数里手动读取 Header 并解析 JWT。只有校验通过才调用 Upgrade 方法,否则直接返回 401。
下面是一段最小可用示例,展示如何在升级连接前完成认证:
package main
import (
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/golang-jwt/jwt/v5"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 生产环境应按域名白名单严格限制
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
tokenStr := strings.TrimPrefix(auth, "Bearer ")
// 解析并校验 JWT
_, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return []byte("your_secret_key"), nil
})
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 校验通过再升级协议
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
// 后续读写逻辑
}
这段代码把鉴权失败拦截在协议升级之前,匿名用户根本拿不到 WebSocket 连接对象。相比先升级再关闭,这样更节省服务端资源,也不会让前端收到一个已打开又被立刻掐断的连接。
需要注意,CheckOrigin 不能替代业务鉴权。它只解决跨站连接问题,而 JWT 校验解决的是用户身份问题。两者必须同时使用,缺一不可。
连接建立后的安全加固
即便握手时认证成功,长连接也可能被复用或劫持。建议为每个连接绑定一个带超时的 context,并定时发送 ping/pong 心跳。若客户端在规定时间内无响应,服务端主动关闭连接。
示例中心跳检测可以这样写:
func readLoop(conn *websocket.Conn) {
for {
// 设置读取截止时间,避免空闲连接长期占用
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
_, msg, err := conn.ReadMessage()
if err != nil {
// 超时或出错则退出,由外层关闭连接
return
}
// 处理业务消息
_ = msg
}
}
此外,服务端应当维护连接与用户 ID 的映射关系,在用户主动退出登录或改密时,主动推送关闭指令或直接在服务端中断其所有连接,防止令牌作废后旧连接仍然可用。
常见误区与总结
一个典型误区是认为 wss 已经加密就不需要管 token 放哪。加密只保护传输内容,不保护日志与代理层的记录行为。另一个误区是把用户角色权限校验推迟到消息处理逻辑中,这会导致未授权连接占据资源。
安全的 Go WebSocket 认证核心只有三点:握手阶段用请求头校验身份、升级前拒绝非法请求、连接后用心跳与上下文控制生命周期。把这些动作前置并集中处理,系统既干净又不容易出漏子。