CORS 跨域中间件无法执行
程序
package main
import (
"net/http"
"github.com/xgfone/ship/v5"
"github.com/xgfone/ship/v5/middleware"
)
func main() {
cors := middleware.CORS(&middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"},
})
r := ship.Default()
r.Use(cors)
r.Route("/ping").GET(func(c *ship.Context) error {
return c.Text(http.StatusOK, "pong")
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
srv.ListenAndServe()
}
复现
$ curl -i -XOPTIONS http://127.0.0.1:8080/balabala -H "Origin: http://example.com" -H "Access-Control-Request-Method: GET"
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Date: Wed, 23 Jul 2025 10:36:51 GMT
Content-Length: 9
Not Found
在 简单请求 或 浏览器预检之后实际发起请求 的情况下,如果 req 的 Origin 不被允许,此时 CORS 中间件应该怎么做呢?
-
只添加 CORS 响应头,不阻止非法跨域请求继续传入业务层。
-
403 阻止非法跨域请求传入到业务层。
中间件执行的时机有两个点:
-
在路由前: 使用
Pre方法注册中间件 -
在路由后: 使用
Use方法注册中间件
上面之所以返回 404 的原因,是使用了 Use 方法注册中间件,从而会先执行路由查找,只有当找到路由后,才使用执行 CORS 中间件。
所以,只需要把 Use 换成 Pre 即可,如下:
r.Pre(cors)
如果不希望因为路由而返回404,而是需要在路由前先进行预处理,都需要使用 Pre 方法。
如果不关心是否返回 404,可一律使用 Use。
还有一个跨域的问题讨论一下:
在 简单请求 或 浏览器预检之后实际发起请求 的情况下,如果 req 的 Origin 不被允许,此时 CORS 中间件应该怎么做呢? Ship 的策略:只添加 CORS 响应头,不阻止非法跨域请求继续传入业务层。 Gin 的策略: 403 阻止非法跨域请求传入到业务层。
对与第一种只添加 Header 不阻止的宽松策略,并非 Ship 一个框架这么做,但我个人更倾向于 Gin CORS 的严格做法, 跨域本身就是为了阻止非法网站的请求。
宽松策略 CORS 没有匹配到 AllowOrigins,所以响应 Header Access-Control-Allow-Origin: 的 value 为空,在浏览器看来该请求被阻止跨域了,但是后端程序 cors 并未阻止该请求,实际业务程序已经被执行了。
我分别用 Gin 和 Ship 写一个等效的程序,用于演示差异:
- https://a.example.com 后端允许跨域的网站 a
- https://b.example.com 模拟跨域的网站 b
Gin CORS
程序
package main
import (
"fmt"
"net/http"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://a.example.com"},
}))
r.GET("/ping", func(c *gin.Context) {
fmt.Println("executed")
c.String(http.StatusOK, "pong")
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
srv.ListenAndServe()
}
cURL 模拟跨域请求
$ curl -i -XGET http://localhost:8080/ping -H "Origin: https://b.example.com"
HTTP/1.1 403 Forbidden
Date: Fri, 25 Jul 2025 01:03:10 GMT
Content-Length: 0
# 返回 403,/ping 路由未执行。
Ship CORS
程序
package main
import (
"fmt"
"net/http"
"github.com/xgfone/ship/v5"
"github.com/xgfone/ship/v5/middleware"
)
func main() {
cors := middleware.CORS(&middleware.CORSConfig{
AllowOrigins: []string{"https://a.example.com"},
})
r := ship.Default()
r.Pre(cors)
r.Route("/ping").GET(func(c *ship.Context) error {
fmt.Println("executed")
return c.Text(http.StatusOK, "pong")
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
srv.ListenAndServe()
}
cURL 模拟跨域请求
$ curl -i -XGET http://localhost:8080/ping -H "Origin: https://b.example.com"
HTTP/1.1 200 OK
Access-Control-Allow-Origin:
Content-Type: text/plain; charset=UTF-8
Vary: Origin
Date: Fri, 25 Jul 2025 01:10:12 GMT
Content-Length: 4
pong
# 返回 200,/ping 路由执行。
宽松 或 严格,这两者策略在实践中都是可以的,具体取决于实际需求。或者,在实现 CORS 中间件时,两者都支持,但默认返回 204,如果需要返回 403 之类的,可以主动设置;注:这个前提是,CORS 中间件要支持。
另外,从 HTTP 标准来看,不应当返回除 204 之外的其它状态码;按 HTTP 标准,应当返回 204 状态码,同时不返回 Access-Control-Allow-Origin 等应答头。至于返回 Access-Control-Allow-Origin 等应答头、但其值为空,对于这种情况,是未定义的,一般浏览器会按未返回处理。
从这来看,Gin 的处理方式是不符合HTTP标准的。