前后端分离架构已经成为目前Web开发的主流模式,前端页面部署在一个域名下,后端API服务部署在另一个域名甚至另一台服务器上,浏览器默认会拦截这种跨域请求,导致接口调用失败。要让Golang后端正常响应跨域请求,就必须正确实现CORS(Cross-Origin Resource Sharing,跨域资源共享)策略。本文将从CORS的原理讲起,再分别用原生net/http和主流框架两种方式给出可落地的配置方案。

一、CORS跨域请求的工作原理
首先要理解,跨域限制是浏览器的同源策略造成的,而不是服务端拒绝访问。所谓同源,指的是协议、域名、端口三者完全一致。只要有一项不同,浏览器就会认为这是跨域请求。当浏览器发起跨域请求时,会自动在请求头中附加Origin字段,标明请求来自哪个源,服务端如果允许该源访问,就在响应头中返回Access-Control-Allow-Origin等字段,浏览器校验通过后才把响应内容交给前端JavaScript。
CORS请求分为两类:简单请求和预检请求。简单请求需要同时满足方法为GET、POST、HEAD之一,且不包含自定义请求头,Content-Type仅限于text/plain、application/x-www-form-urlencoded、multipart/form-data这几种。不满足条件的请求(比如带自定义header的请求、PUT或DELETE请求、JSON格式的POST请求)会先触发一次预检请求。预检请求是一个OPTIONS方法的请求,浏览器通过它先询问服务端是否允许真正的请求,服务端需要在响应中告知允许的方法、允许的请求头以及预检结果的有效期。很多开发者配置了CORS却依然报错,最常见的原因就是没有正确处理这个OPTIONS预检请求,导致预检阶段就被拦截了。
理解这一点非常重要,因为Golang的net/http默认路由对OPTIONS请求的处理方式和普通请求一样,如果你的处理函数内部对请求体或方法做了校验,预检请求就会失败,浏览器控制台会报出经典的错误信息。所以处理CORS的核心,就是在中间件层拦截OPTIONS请求并直接返回正确的响应头。
二、使用net/http中间件手写CORS处理
对于不依赖第三方框架的项目,自己写一个CORS中间件是最直接的方式。中间件的思路是:在请求进入业务逻辑之前,先判断是否为预检请求,如果是就直接写响应头并返回;如果是普通请求,则写上CORS响应头后继续传递给下一个处理器。下面是完整可运行的示例:
package main
import (
"net/http"
)
// corsMiddleware 为所有请求添加跨域响应头
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 允许的源,生产环境建议从配置中读取白名单
w.Header().Set("Access-Control-Allow-Origin", "https://www.ipipp.com")
// 允许携带Cookie等凭证信息
w.Header().Set("Access-Control-Allow-Credentials", "true")
// 允许的HTTP方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 允许前端传递的自定义请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Token")
// 预检结果缓存时间,单位秒,减少重复预检
w.Header().Set("Access-Control-Max-Age", "86400")
// 预检请求直接返回,不进入业务逻辑
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.JSON, _ := json.Marshal(map[string]string{"message": "hello"})
w.Write(w.JSON)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/hello", helloHandler)
http.ListenAndServe(":8080", corsMiddleware(mux))
}这段代码有几个细节值得注意。第一,Access-Control-Allow-Origin的值如果设置为星号*,在与Access-Control-Allow-Credentials: true同时使用时浏览器会直接报错,因为允许携带凭证的情况下必须明确指定具体的源,不能使用通配符。第二,Access-Control-Allow-Headers必须包含前端实际会发送的所有自定义头,比如常见的Authorization、X-Token,漏掉任何一个都会导致预检失败。第三,Access-Control-Max-Age设置得长一些可以显著减少预检请求次数,提升接口性能。
如果需要支持多个域名,可以把允许的源做成白名单,在中间件里动态判断请求的Origin是否在名单中,命中才回写该源。相比硬编码单一域名,这种方式更适合同时服务多个前端站点的场景。下面是白名单版本的判断逻辑:
var allowedOrigins = map[string]bool{
"https://www.ipipp.com": true,
"https://admin.ipipp.com": true,
}
func getAllowedOrigin(r *http.Request) string {
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
return origin
}
return ""
}三、使用第三方库和框架快速配置CORS
如果项目已经使用了Gin、Echo等框架,直接使用官方配套的CORS中间件会更加省心。以Gin框架为例,社区提供的gin-contrib/cors包功能完善,支持多域名、通配符域名匹配、Cookie凭证、请求头白名单等配置项:
package main
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
corsConfig := cors.Config{
AllowOrigins: []string{"https://www.ipipp.com", "https://admin.ipipp.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}
r.Use(cors.New(corsConfig))
r.GET("/api/data", func(c *gin.Context) {
c.JSON(200, gin.H{"code": 0, "msg": "ok"})
})
r.Run(":8080")
}对于不使用框架但又不想自己维护中间件的项目,gorilla/handlers库也是不错的选择。它提供了handlers.CORS函数,一行代码就能为整个路由加上跨域支持,并且可以搭配handlers.AllowedOrigins、handlers.AllowedMethods、handlers.AllowedHeaders等选项灵活控制策略,底层实现同样会自动处理OPTIONS预检请求。
无论选择哪种方式,上线前都建议做几项检查:确认Nginx等反向代理层没有重复添加CORS响应头,否则浏览器会因为收到多个Access-Control-Allow-Origin值而报错;确认生产环境的允许源列表不包含测试域名;如果前后端部署在同一个域名下(例如前端静态文件由Go服务直接托管),则根本不需要CORS配置。合理利用同源部署规避跨域问题,配合中间件处理真正的跨域场景,才能让接口访问控制既安全又高效。
Golang CORS跨域请求访问控制策略修改时间:2026-09-05 03:40:29