为什么用浏览器请求Golang接口会触发跨域错误,而Postman和curl却总能拿到数据?根本原因不在服务端是否拒绝了请求,而在于浏览器的同源策略对跨域响应的限制。要解决这个问题,Golang服务端需要正确返回一组CORS响应头,并且妥善处理浏览器的预检请求。本文先把同源策略和预检请求的工作机制讲清楚,然后分别演示在标准库net/http、rs/cors库以及Gin框架中的处理方式,最后梳理携带Cookie、自定义头部和缓存时间等常见配置陷阱。

同源策略与CORS的核心机制
同源策略(Same-Origin Policy)是浏览器最核心的安全机制之一。一个源由协议、域名和端口三个要素组成,只有三者完全一致时浏览器才认为两个URL同源。例如 http://ippipp.com:8080 与 https://ippipp.com 就不是同源。JavaScript发起的跨域HTTP请求虽然通常能到达服务器,但浏览器会拦截响应内容,除非响应中带有合法的CORS头。这就是Postman能拿到数据但浏览器报错的关键原因。
CORS根据请求特征把跨域请求分成两类:简单请求与预检请求。简单请求通常满足方法为GET、HEAD、POST,且只包含安全头,Content-Type仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded。如果使用PUT、DELETE,或添加自定义头如Authorization、X-Requested-With,或发送application/json,浏览器会先发送一个OPTIONS预检请求,询问服务器是否允许该跨域请求。
预检请求包含Access-Control-Request-Method和Access-Control-Request-Headers,服务器需要以2xx状态码响应,并通过Access-Control-Allow-Methods、Access-Control-Allow-Headers等响应头明确授权。预检响应也可以设置Access-Control-Max-Age来缓存授权结果,减少重复预检。服务端还应考虑设置Vary: Origin,避免缓存时把不同源的响应头串用。
用net/http手写CORS中间件
对于小型API或不希望引入第三方依赖的项目,可以直接在net/http中用一个中间件包装handler。最简单的响应头设置是在业务handler里写入Access-Control-Allow-Origin,但这样会在每个handler中重复代码,而且无法统一处理OPTIONS预检。更合理的做法是定义一个中间件函数,在所有路由之前统一完成CORS逻辑。
中间件在处理请求时,先设置允许的源、方法和头。如果请求方法是OPTIONS且携带Access-Control-Request-Method,说明这是一次预检请求,可以直接返回204,不再进入后续业务逻辑。对于实际请求,需要继续调用next.ServeHTTP。
package main
import (
"net/http"
)
func corsMiddleware(next 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", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Max-Age", "3600")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{\"message\":\"hello from go\"}"))
})
http.ListenAndServe(":8080", corsMiddleware(mux))
}
上述代码把源设置为星号,表示允许任意源。这种配置简单,但无法携带Cookie。如果需要支持带凭据的跨域请求,必须将Access-Control-Allow-Origin改为请求中的Origin值,同时设置Access-Control-Allow-Credentials为true。此时不能使用星号,否则浏览器会忽略Credentials声明。回显Origin时还要校验它是否在允许列表中,避免开放危险源。
使用rs/cors库快速集成
rs/cors 是Go社区中使用非常广泛的CORS处理库,内部已经处理了OPTIONS预检、Vary头、Origin校验和缓存等细节。它的配置结构清晰,适合中大型项目。安装方式为 go get github.com/rs/cors。
初始化一个cors.Cors对象,设置AllowedOrigins、AllowedMethods、AllowedHeaders和AllowCredentials等字段,然后通过Handler方法包装整个mux。这样可以避免手写中间件时遗漏边界情况。
package main
import (
"net/http"
"github.com/rs/cors"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("{\"message\":\"hello from go\"}"))
})
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://ipipp.com", "http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 3600,
})
handler := c.Handler(mux)
http.ListenAndServe(":8080", handler)
}
rs/cors还会根据是否配置AllowCredentials来决定如何响应Origin。当AllowCredentials为true时,库会回显请求Origin而不是星号,并自动设置Vary: Origin。如果请求的Origin不在AllowedOrigins列表中,库会直接不返回CORS头,浏览器自然会拦截。可以通过开启Debug选项查看实际响应过程。
Gin框架下实现CORS
Gin框架本质上仍然使用net/http,但它提供了中间件机制,可以非常方便地管理CORS逻辑。你可以先手写一个Gin中间件,在处理函数中调用c.Header设置响应头,并判断c.Request.Method是否为OPTIONS。预检请求应当尽早终止,避免进入后续业务处理。
package main
import (
"github.com/gin-gonic/gin"
)
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
c.Header("Access-Control-Allow-Credentials", "false")
c.Header("Access-Control-Max-Age", "3600")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func main() {
r := gin.Default()
r.Use(CORSMiddleware())
r.GET("/api/data", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello from gin"})
})
r.Run(":8080")
}
这种方式适合大多数场景,但需要自己维护允许的源。当需要精确控制Origin列表或携带凭据时,可以在Gin中间件内部读取c.GetHeader("Origin")并与白名单比较,然后将匹配的Origin写回Access-Control-Allow-Origin。如果直接返回星号,浏览器在携带Cookie时会拒绝响应。
如果项目已经使用rs/cors,也可以直接包装Gin引擎。例如在main函数中创建cors.Cors实例,然后使用r.Use(func(ctx *gin.Context) { c.HandlerFunc(ctx.Writer, ctx.Request); ctx.Next() })。需要注意执行顺序:CORS中间件应当在路由处理之前运行,并且预检请求要尽早终止,避免经过后续鉴权或业务逻辑。
常见CORS配置错误与排查技巧
第一个高频错误是携带Cookie时仍然使用Access-Control-Allow-Origin: *。浏览器会在控制台提示此时不能使用通配符。解决办法是把允许源设置为具体Origin,并增加Access-Control-Allow-Credentials: true。前端也必须开启withCredentials,例如fetch请求中设置credentials为include,或者XHR中设置withCredentials = true。
第二个问题是OPTIONS请求返回404或405。很多路由没有为OPTIONS方法匹配处理逻辑,如果中间件没有拦截预检请求,预检会落到普通路由处理函数上,可能导致失败。排查时可以用curl -i -X OPTIONS http://localhost:8080/api/data -H "Origin: http://localhost:3000" -H "Access-Control-Request-Method: POST" 模拟预检,观察是否有Access-Control-Allow-Methods等响应头。
第三个问题是浏览器提示某个自定义头没有被允许,例如Authorization头。服务端除了设置AllowedHeaders外,还要注意Header名称的大小写匹配。在HTTP/2中头名通常是小写形式,但Go标准库会自动做规范化。必要时可以把Access-Control-Allow-Headers设置为与请求头完全一致的列表。调试时还应检查反向代理是否过滤了CORS响应头,并确认Access-Control-Max-Age缓存没有让旧配置继续生效。
Golang CORS跨域资源共享HTTP中间件修改时间:2026-08-22 16:24:13