WebSocket鉴权必须在upgrader.Upgrade()前完成,否则c.Request()/c.Response()失效;需用http.HandlerFunc+echo.WrapHandler绕过Echo中间件,鉴权失败须返回HTTP错误而非关闭连接。

WebSocket升级前必须完成鉴权,不能在Upgrade后做
WebSocket连接一旦调用 upgrader.Upgrade(),就脱离了 Echo 的请求生命周期,c.Request() 和 c.Response() 不再可用,后续任何对 c 的读写(比如 c.QueryParam("token") 或 c.Get("user_id"))都会 panic 或返回空值。所以所有鉴权逻辑——解析 token、校验签名、查 session、验证权限——必须在调用 Upgrade() 之前做完。
常见错误是把鉴权写在 upgrader.Upgrade() 调用之后,或者误以为能像普通 HTTP handler 那样在 WebSocket 连接建立后再“中间件式”拦截消息。实际上,WebSocket 没有“中间件链”的概念,只有一次性的握手阶段可干预。
- 从
c.Request().URL.Query()提取 query 参数(如?token=xxx) - 或从
c.Request().Header.Get("Authorization")提取 Bearer token - 或从
c.Request().Header.Get("X-Api-Key")校验 API key - 鉴权失败立即 return,不调用
upgrader.Upgrade() - 成功后才调用
upgrader.Upgrade(),此时不能再碰c
不能用 e.Use() 给 /ws 路由加中间件
Echo 的 e.Use() 注册的中间件只作用于标准 HTTP handler 生命周期,而 WebSocket 升级需要绕过整个中间件链和路由匹配逻辑。如果你把 upgrader.Upgrade() 放在普通 e.GET("/ws", handler) 里,会因响应头已被中间件写入、状态码已设为 200 等原因触发 "bad request" 或直接断连。
正确做法是:用 http.HandlerFunc 写纯原生 handler,再用 echo.WrapHandler() 包裹后挂到路由上,确保该路径完全跳过 Echo 中间件执行流。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 不要写
e.GET("/ws", func(c echo.Context) error { ... upgrader.Upgrade() ... }) - 要写
http.HandlerFunc,例如func(w http.ResponseWriter, r *http.Request) { ... } - 再用
e.GET("/ws", echo.WrapHandler(yourHandler)) - 路径必须严格一致:
new WebSocket("ws://host/ws")对应e.GET("/ws", ...),多一个/就 404
鉴权失败时必须返回 HTTP 错误,不能靠 conn.Close()
前端发起 WebSocket 连接时,如果服务端鉴权失败,你不能等 upgrader.Upgrade() 成功后再关连接——那已经晚了,连接已建立,客户端收不到明确拒绝信号。必须在 Upgrade 前就中断流程,返回标准 HTTP 错误响应(如 401、403),让浏览器明确感知连接被拒。
否则会出现“连接看似成功但立刻断开”“控制台报 WebSocket connection to '...' failed 却无具体原因”等问题,排查困难。
- 鉴权失败时调用
http.Error(w, "Unauthorized", http.StatusUnauthorized) - 或手动设置状态码+响应体:
w.WriteHeader(http.StatusForbidden); w.Write([]byte("forbidden")) - 绝对不要在
Upgrade()后调用conn.Close()来“模拟拒绝” - 前端可通过
onerror和onclose的event.code辅助判断,但服务端必须主动返回 HTTP 错误
JWT token 解析需注意时间与签名校验顺序
如果用 JWT 做 WebSocket 鉴权,别只校验 signature,还要检查 exp、nbf、iss 等字段。Echo 自带的 echo-jwt 中间件不适用于 WebSocket 场景(它依赖 echo.Context 生命周期),必须手写解析逻辑。
推荐用 github.com/golang-jwt/jwt/v5,并注意:解析时传入的 *http.Request 是原始请求对象,不是 echo.Context;且必须在 Upgrade() 前完成全部校验,包括从 header 或 query 中提取 token 字符串、解析、验证签名、检查过期时间。
- 用
token, _, err := new(jwt.Parser).ParseUnverified(rawToken, jwt.MapClaims{})先解出 claims,避免重复解析 - 手动校验
claims["exp"].(float64)是否过期(注意类型断言和 Unix 时间戳单位) - 用
token.Claims.(jwt.MapClaims).VerifyExpiresAt(time.Now().Unix(), true)更稳妥 - 签名密钥建议从环境变量或配置加载,不要硬编码
echo.WrapHandler 包裹后,该 handler 已完全脱离 Echo 上下文,所有依赖 echo.Context 的工具函数(如 c.Get()、c.Set()、c.Echo())都不可用。所有逻辑必须基于原始 *http.Request 和 http.ResponseWriter 展开。

















