I’LL BE HERE

In solitude, where we are least alone.

在 Golang 中如何编写允许跨域访问的 Handlers

可以直接在原有的 Handler 里面加东西,也可以用 中间件 。 也可以再搞一个代理服务器。

1. 问题的背景

假如你有一个很简单的 Golang 服务器程序:

/* handler.go */

package main

import (
    "log"
    "net/http"
)

func serveHello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello, world"))
}

func main() {
    log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(serveHello)))
}

然后很自然地,你就可以运行它。

go run ./handler.go

# 开始监听 8080 端口,对所有的 http 请求,都会返回 hello, world

但是这个 服务器不支持 跨域资源共享 (Cross-Origin Resource Sharing, CORS) 。意思就是说,你打开浏览器,如果浏览器地址栏里面的域名不是 127.0.0.1:8080 ,那么就没有办法用 XMLHttpRequest 对服务器发起请求。

如果硬要发起请求的话,就会出现 Cross-Origin Request Blocked 之类的 错误,如下图所示。

2. 解法:修改服务器的代码,使服务器允许 CORS

最简单的办法就是修改 serveHello 函数。在 serveHello 里面加上一行:

-/* handler.go */
+/* handler_allow_cors.go */

 package main

 import (
     "log"
     "net/http"
 )

 func serveHello(w http.ResponseWriter, r *http.Request) {
+	 w.Header().Set("Access-Control-Allow-Origin", "*")
     w.Write([]byte("hello, world"))
 }

 func main() {
     log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(serveHello)))
 }

重新编译、重启服务器,再试一次,就发现 OK 了。

但是,有时候,只加 Access-Control-Allow-Origin 是不够的。 如果是 POST 或者其他类型的请求,还得设置其他的 Response Header, 还要处理 CORS Preflight:

-/* handler_allow_cors.go */
+/* handler_allow_cors_more.go */

 package main

 import (
     "log"
     "net/http"
 )

 func serveHello(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Access-Control-Allow-Origin", "*")
+	w.Header().Set(
+		"Access-Control-Allow-Methods",
+		"POST, GET, OPTIONS, PUT, DELETE",
+	)
+	w.Header().Set(
+		"Access-Control-Allow-Headers",
+		"Accept, Content-Type, Content-Length",
+	)
+	if (*r).Method == "OPTIONS" {
+		return
+	}

	w.Write([]byte("hello, world"))
 }

 func main() {
     log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(serveHello)))
 }

3. 中间件封装

但是假如有很多个 handlers 的话,我们 不想复制代码,然后在每个 handler 里面粘贴很多遍。

这个时候就可以把允许 CORS 的代码封装成中间件:

-/* handler_allow_cors_more.go */
+/* handler_allow_cors_using_middleware.go */

 package main

 import (
     "log"
     "net/http"
 )

 func serveHello(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("Access-Control-Allow-Origin", "*")
-	w.Header().Set(
-		"Access-Control-Allow-Methods",
-		"POST, GET, OPTIONS, PUT, DELETE",
-	)
-	w.Header().Set(
-		"Access-Control-Allow-Headers",
-		"Accept, Content-Type, Content-Length",
-	)
-	if (*r).Method == "OPTIONS" {
-		return
-	}
-
+	// 现在 serveHello 变回了最初的样子:
	w.Write([]byte("hello, world"))
 }

+func UNSAFE_enableCORS(h http.Handler) http.Handler {
+	return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Access-Control-Allow-Origin", "*")
+		w.Header().Set(
+			"Access-Control-Allow-Methods",
+			"POST, GET, OPTIONS, PUT, DELETE",
+		)
+		w.Header().Set(
+			"Access-Control-Allow-Headers",
+			"Accept, Content-Type, Content-Length",
+		)
+		if (*r).Method == "OPTIONS" {
+			return
+		}
+
+		h.ServeHTTP(w, r)
+	})
+}

 func main() {
-	log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(serveHello)))
+	log.Fatal(
+		http.ListenAndServe(
+			":8080",
+			UNSAFE_enableCORS(http.HandlerFunc(serveHello)),
+		)
+	)
}

【旁注】什么叫“中间件”?

在这里,中间件(Middleware)指的就是接受一个 http.Handler,返回一个 http.Handler 的函数(见文章底部的“相关链接”)。比如上面代码中的 UNSAFE_enableCORS 就是一个中间件。

之后,如果想让某个 handler (比如说 serveContent) 支持跨域访问的话,就用 UNSAFE_enableCORS(serveContent) 即可。

4. 能不能不改服务器的代码?

封装了中间件以后,其实只要在 main 函数里面改代码就可以了, handler 里面的代码不用改。但是假如服务器不在你手上,你根本改不了的话, 那该怎么办呢?

这个时候就可以设置代理服务器。比如,可以在本机上架个 nginx,然后在配置文件 里面设置 proxy_pass,然后用 add_header 覆写原来的 Response Header。

假如你是前端程序员,想用别人的服务器做测试,也可以用 webpack 的 devServer 自己写一个代理服务器。

有点离题了。就到这里吧。

相关链接