
本文旨在解决 go 语言使用 `go.net/websocket` 包时常见的 403 Forbidden 错误,该错误通常源于默认的 `Origin` 头部校验机制。我们将深入探讨 `websocket.Handler` 与 `websocket.Server` 的区别,并提供使用 `websocket.Server` 来绕过或自定义 `Origin` 校验的解决方案,确保 WebSocket 连接的顺利建立,同时强调相关安全考量。
1. Go 语言WebSocket 基础服务构建与常见问题
在 Go 语言中,使用 go.net/websocket 包可以方便地构建 WebSocket 服务。一个常见的 WebSocket echo服务器示例如下,它旨在接收客户端消息并打印:
package main import ("fmt" "golang.org/x/net/websocket" // 注意:原先的 code.google.com/p/go.net/websocket 已迁移至此 "net/http" ) // webHandler 处理 WebSocket 连接,接收消息并打印 func webHandler(ws *websocket.Conn) {var s string // 从 WebSocket 连接中读取字符串 if _, err := fmt.Fscan(ws, &s); err != nil {fmt.Printf("Error reading from websocket: %vn", err) return } fmt.Println("Received: ", s) // 可以选择将消息回显给客户端 // if _, err := fmt.Fprint(ws, "Echo: "+s); err != nil {// fmt.Printf("Error writing to websocket: %vn", err) // } } func main() { fmt.Println("Starting websocket server on :8080/echo") // 将 webHandler 包装成 websocket.Handler 并注册到 HTTP路由 http.Handle("/echo", websocket.Handler(webHandler)) // 启动 HTTP 服务器监听 8080 端口 err := http.ListenAndServe(":8080", nil) if err != nil {panic("ListenAndServe: " + err.Error()) } }
// 尝试连接到 WebSocket 服务 ws = new WebSocket("ws://localhost:8080/echo"); // 监听接收到的消息 ws.onmessage = function(e) {console.log("websock: " + e.data); }; // 连接成功时发送消息 ws.onopen = function() { ws.send("Hello from client!"); }; // 连接关闭时 ws.onclose = function() { console.log("WebSocket connection closed."); }; // 发生错误时 ws.onerror = function(err) {console.error("WebSocket error:", err); };
然而,当尝试使用上述客户端代码连接到服务器时,浏览器 控制台可能会报告 WebSocket connection to ‘ws://localhost:8080/echo’ failed: Unexpected response code: 403 错误。这表明服务器拒绝了连接请求。
立即学习“go 语言免费学习笔记(深入)”;
2. 深入理解 403 错误:Origin 校验机制
这个 403 错误通常与 go.net/websocket 包中 websocket.Handler 的默认行为有关。websocket.Handler 在处理 WebSocket 握手时,会默认检查 HTTP 请求头中的 Origin 字段。Origin 头是 浏览器 在发起 跨域 请求(包括 WebSocket 连接)时自动添加的,用于指示请求的来源域。
websocket.Handler 的默认行为是:
- 它期望 Origin 头部是一个有效的 URL。
- 它会尝试验证 Origin 是否与服务器的地址匹配,或者至少是一个可接受的来源。
- 如果 Origin 头部缺失、无效,或者不符合其内部的默认校验规则,websocket.Handler 就会拒绝连接,并返回 403 Forbidden 状态码。
这种机制主要是为了增强基于浏览器的 WebSocket 连接的安全性,防止跨站请求伪造(csrf)等攻击。然而,在某些场景下,例如:
- 非浏览器客户端(如服务器到服务器的 WebSocket 通信)可能不发送 Origin 头部。
- Origin 头部与服务器地址不完全匹配,但连接是合法的。
- 开发或测试环境中,需要临时禁用 Origin 校验。
在这些情况下,websocket.Handler 的默认 Origin 校验就会成为障碍。
3. 解决方案:使用 websocket.Server 绕过或自定义 Origin 校验
为了解决 Origin 校验导致的 403 错误,我们可以使用 websocket.Server结构体 来替代直接使用 websocket.Handler。websocket.Server 提供了更灵活的配置选项,尤其是在处理握手阶段。
websocket.Server 允许我们直接嵌入一个 websocket.Handler,但更重要的是,它提供了一个 Handshake 字段,可以自定义握手逻辑。当 Handshake 字段为 nil 时,websocket.Server 会采用一个默认的握手函数,这个默认函数 不会强制执行 Origin 校验。
下面是修改后的 Go 服务器代码,它使用 websocket.Server 来解决 403 问题:
package main import ("fmt" "golang.org/x/net/websocket" // 注意:请确保导入路径正确 "net/http") // webHandler 处理 WebSocket 连接,接收消息并打印 func webHandler(ws *websocket.Conn) {var s string // 从 WebSocket 连接中读取字符串 if _, err := fmt.Fscan(ws, &s); err != nil {// 忽略 EOF 错误,这通常表示客户端关闭了连接 if err.Error() != "EOF" {fmt.Printf("Error reading from websocket: %vn", err) } return } fmt.Println("Received: ", s) // 可以选择将消息回显给客户端 if _, err := fmt.Fprint(ws, "Echo: "+s); err != nil {fmt.Printf("Error writing to websocket: %vn", err) } } func main() { fmt.Println("Starting websocket server on :8080/echo") // 使用 http.HandleFunc 注册一个自定义处理函数 http.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) {// 创建 websocket.Server 实例 // Handshake 字段为 nil 时,默认的握手函数不会强制检查 Origin s := websocket.Server{Handler: websocket.Handler(webHandler)} // 调用 ServeHTTP 方法来处理 WebSocket 连接 s.ServeHTTP(w, req) }) // 启动 HTTP 服务器监听 8080 端口 err := http.ListenAndServe(":8080", nil) if err != nil {panic("ListenAndServe: " + err.Error()) } }
在这个修正后的代码中,我们不再直接将 websocket.Handler(webHandler)传递给 http.Handle。取而代之的是,我们创建了一个 websocket.Server 实例,并将 websocket.Handler(webHandler)赋值给它的 Handler 字段。然后,通过 s.ServeHTTP(w, req)方法来处理 HTTP 请求。由于 websocket.Server 的 Handshake 字段默认为 nil,它将使用一个不执行 Origin 校验的默认握手函数,从而允许连接成功建立。
4. 示例代码与注意事项
完整的服务器代码
package main import ("fmt" "golang.org/x/net/websocket" "net/http" "log" // 引入 log 包用于更专业的错误处理) // webHandler 处理 WebSocket 连接,接收消息并打印,并回显 func webHandler(ws *websocket.Conn) {defer ws.Close() // 确保连接关闭 var msg string for {// 从 WebSocket 连接中读取字符串 err := websocket.Message.Receive(ws, &msg) // 使用 websocket.Message 进行结构化读写 if err != nil {if err.Error() != "EOF" {// 忽略 EOF 错误,通常表示客户端关闭了连接 log.Printf("Error receiving message from websocket: %vn", err) } break // 发生错误或客户端关闭时退出循环 } log.Printf("Received from client %s: %sn", ws.RemoteAddr().String(), msg) // 将消息回显给客户端 err = websocket.Message.Send(ws, "Echo: "+msg) if err != nil {log.Printf("Error sending message to websocket: %vn", err) break } } log.Printf("Client %s disconnected.n", ws.RemoteAddr().String()) } func main() { log.Println("Starting websocket server on :8080/echo") http.HandleFunc("/echo", func(w http.ResponseWriter, req *http.Request) {// 可以选择在此处手动检查 Origin 头部,以增加安全性 // if req.Header.Get("Origin") != "http://localhost:8080" {// http.Error(w, "Forbidden", http.StatusForbidden) // return // } // 创建 websocket.Server 实例,Handler 字段指向我们的业务逻辑 // Handshake 字段为 nil 时,默认的握手函数不会强制检查 Origin s := websocket.Server{Handler: websocket.Handler(webHandler)} // 调用 ServeHTTP 方法来处理 WebSocket 连接 s.ServeHTTP(w, req) }) log.Fatal(http.ListenAndServe(":8080", nil)) // 使用 log.Fatal 来处理 ListenAndServe 错误 }
注意事项
- 安全性考虑: 禁用 Origin 校验会降低 WebSocket 连接的安全性,使其更容易受到 CSRF 攻击。在生产环境中,特别是面向浏览器的应用,强烈建议:
- go.net/websocket 的替代方案: go.net/websocket 是一个相对较老的包,且其 API 设计在某些方面可能不如现代的 WebSocket 库灵活。在新的项目中,更推荐使用像 gorilla/websocket 这样的第三方库,它提供了更丰富的功能、更好的性能和更清晰的 API。
- websocket.Conn.Config().Origin: 即使使用 websocket.Server 绕过了默认校验,你仍然可以在 webHandler 内部通过 ws.Config().Origin 获取到客户端发送的 Origin 头部,然后根据业务需求进行自定义验证。
- 错误处理: 在实际应用中,WebSocket 连接的错误处理(如客户端意外断开、网络问题)至关重要。示例中增加了 defer ws.Close()和更详细的错误日志。
- websocket.Message 与 fmt.Fscan: websocket.Message.Receive 和 websocket.Message.Send 是 go.net/websocket 包中推荐的用于发送和接收结构化消息的方式,它们比 fmt.Fscan 更健壮,尤其是在处理二进制数据或更复杂的文本格式时。
总结
当在 Go 语言中使用 go.net/websocket 包构建 WebSocket 服务时,遇到 403 Forbidden 错误通常是由于 websocket.Handler 默认的 Origin 头部校验机制所致。为了解决这个问题,我们可以通过实例化 websocket.Server 并将其 Handler 字段设置为我们的业务逻辑,利用其默认的握手行为(不强制 Origin 校验)来成功建立连接。然而,在禁用 Origin 校验时,务必充分考虑安全风险,并在生产环境中采取适当的安全措施,如自定义 Origin 验证或使用更现代、功能更完善的 WebSocket 库。