1 背景
有个前后端分离的项目,前端向后端发起了一个注册的请求,从network中看到现象如下:

点击第一个请求,看到标头请求的URL是 http://localhost:28080/users/signup:

再点击第二个请求,标头请求的URL也是 http://localhost:28080/users/signup:

再到console控制台会看到如下错误:

再去看下服务器接收到的请求输出:
[GIN] 2024/09/27 - 14:33:17 | 404 | 1.25µs | ::1 | OPTIONS "/users/signup"
想一想:我明明发送点击了一次,为什么会有两次请求呢?
2 原因分析
导致上面现象的问题其实就是跨域要解决的问题,可以从控制台看到输出如下:
Access to XMLHttpRequest at 'http://localhost:28080/users/signup' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这个现象就是所谓的跨域。从上面信息可以知道两点:
http://localhost:3000向http://localhost:28080/users/signup发起的请求被阻塞了。- 对
preflight预检请求的响应未通过访问控制检查,preflight并没有传递access control check,导致上面问题的原因是因为请求的资源上没有设置 “Access-Control-Allow-Origin” 标头,在哪里设置呢?是在preflight request中设置。
正常来说,如果我们不做跨域处理,发起的请求就会出现类似这样的错误。
3 详解跨域
一个网站请求另一个网站,两个网站存在协议(如http、https)、域名(如a.com, b.com)、端口(如a.com:8080, a.com:8081)任意一个不同,即认为是跨域请求。
浏览器如何判断是否是两个网站的呢?
- 协议
- 端口
- 域名
比如请求是从 localhost:3000 发送到后端 localhost:28080 的,在请求时,浏览器认为 localhost:3000 是一个网站,localhost:28080 是一个网站。现在是从一个网站发送请求到另一个网站,浏览器就不答应,因为攻击者可以伪造自己的请求发送到后端服务器,浏览器是不允许这样的行为存在的。

主要是浏览器不允许这种行为,而后端无所谓,所以跨域请求其实就是浏览器阻止的。
想一想 浏览器为什么这么设计呢?主要是为了防止黑客伪造一些乱七八糟的东西不断地发送请求。
4 如何解决
4.1 思路
- 跨域的问题是浏览器不准请求从一个网站发往另一个网站。要解决这个问题,那就让浏览器允许将请求从一个网站发往另一个网站。
- 是什么让浏览器不允许发送请求呢?就是浏览器觉得网站携带的内容可能存在危险。
- 怎么才能让浏览器认为携带的内容没有危险呢?就是让浏览器告诉背后的服务器,请求都带了什么,背后的服务器也明确出给这个什么东西不危险。此时浏览器才放心请求允许通过。
比如问浏览器,localhost:28080 是否可以接收从 localhost:3000 过来的请求。浏览器因为跨域不让发送请求过来,要解决这个问题,比如让localhost:28080主动告诉浏览器让浏览器允许请求从localhost:3000发送过来,他可以接受。
关键是怎么告诉浏览器呢?谁告诉谁?比如:localhost:28080 怎么告诉 localhost:3000 允许将请求发送过来呢?
这需要浏览器的一个机制,需要通过一个叫 preflight 请求的机制。需要在 preflight 请求里面告诉浏览器,让其允许接收 localhost:3000 发过来的请求。
这就是为什么上面【想一想】提到的为什么会有两个请求,第一个请求是先问下localhost:28080能不能接收localhost:3000的请求,也就是下面这个请求,这就是 preflight:

看到 preflight 可以理解为是提前问一下后端允不允许 localhost:3000 的请求过来,然后 localhost:28080 回复浏览器说要,接着浏览器就会将 localhost:3000 的请求发过来,也就是这个请求:

下面就是如何告诉localhost:28080它呢?这也是解决跨域问题的关键,要在 preflight 请求中配置一些参数,就需要在 preflight 请求的响应里面配置,也就是在过来询问是否允许的时候告诉它。
4.2 preflight请求流程
- preflight 请求会发送到同一个地址上,使用 Options 方法,没有请求参数。
比如正常请求注册接口 /users/signup ,就会有一个 preflight 的请求也发送到 /users/signup,但是它的method是 Options ,携带哪些内容,比如携带了“authorization”,请求方式是“POST”:

后端在收到 preflight 请求之后就要在响应中告诉它【准许】接收从localhost:3000发过来的请求(Allow-Origins: http://localhost:3000),也可以接收它在Header里面携带Content-Type(Allow-Headers),还能接受的方法包括(Allow-Methods: 全部方法)。

然后紧接着请求方就会把真正的请求发送过去,如何看出来的呢?可以看请求标头,如下图所示:

这个过程也就是localhost:3000告诉localhost:28080,我接下来有个请求要发送给你,发送的请求要带两个头部,一个authorization,一个是content-type,我发送的请求是post请求。
也有点像亲戚给你介绍对象,先告诉你,她那里有个姑娘170、100斤,很漂亮,你要不要(这就是 preflighst 请求),这时你告诉她要,然后她就会把人介绍给你,介绍给你这就是正式的业务请求。

4.3 后端如何应答preflight请求?
这就是后端的跨域解决方案的视线,大多数的web框架都提供的跨域的解决方案。
比如在Go中的解决方案就是跨域的middleware。配置之后查看相应头,这些就是后端和浏览器preflight沟通后允许的请求的内容:
// internal/web/middleware/cors.go
package middleware
...
func CorsHdl() gin.HandlerFunc {
return cors.New(cors.Config{
// AllowOrigins -> Origin
// 不建议写 * ,另外如果前端设置了 strict policy,就会无法使用 *
//AllowOrigins: []string{"http://localhost:3000"},
// AllowMethods -> 对应 Access-Control-Request-Method
// AllowMethods 也可以指定具体方法:比如POST
//AllowMethods: []string{}, // 不写表示都支持
AllowMethods: []string{"POST", "GET", "PUT", "PATCH", "DELETE"},
// 大小写无所谓,一般浏览器展示什么就写什么
// AllowHeaders ->对应 Access-Control-Request-Headers
AllowHeaders: []string{"Content-Type", "Authorization"},
// 允许请求里携带 x-jwt-token,不加这个前端读取不到 x-jwt-token
// 我给前端的前端才能拿,没给的你就拿不到
ExposeHeaders: []string{"x-jwt-token", "x-refresh-token"},
AllowCredentials: true, // 是否允许携带cookie之类的东西
// AllowOrigins 可以开发、测试环境都写上,但些人的允许的请求来源很复杂,就可以不写AllowOrigins ,
// 而是借助AllowOriginFunc
AllowOriginFunc: func(origin string) bool {
// 有些人请求的来源很复杂,所以 AllowOrigins 也可以不写,可以写成下面的方式
//return origin == "https://github.com"
fmt.Println(origin)
if strings.HasPrefix(origin, "http://localhost") {
fmt.Println("开发环境")
return true
}
return strings.Contains(origin, "your company domain")
},
// preflight 请求有效期为12小时,但正常不需要12个小时,正常发送完preflight后就紧接着发送正常请求了,所以这个可以调为一个很小的值
MaxAge: 12 * time.Hour,
})
}
// main.go
package main
...
func main() {
server := gin.Default()
server.Use(middleware.CorsHdl())
uh := web.NewUserHandler()
uh.RegisterRoutes(server)
server.Run(":28080")
}
解决跨域问题后的请求:

下面就是如何告诉preflight如何准许将请求发送过来:

4.4 详解配置

| Request Headers | Response Headers | Gin Middleware | 描述 |
|---|---|---|---|
| origin: xxx | access-control-allow-origin: xxx | AllowOrigins: []string{“http://localhost:3000”} | 请求从何处来 |
| access-control-request-headers | access-control-allow-headers | AllowHeaders: []string{“Content-Type”, “Authorization”} | 头部携带信息 |
| access-control-request-method | access-control-allow-methods | AllowMethods: []string{“POST”, “GET”, “PUT”, “PATCH”, “DELETE”} | 请求方法 |
| … | access-control-allow-credentials | AllowCredentials: true | 证书,如是否允许携带cookie等 |
| … | … | ExposeHeaders: []string{“x-jwt-token”, “x-refresh-token”} | 允许前端获取参数 |
| … | … | … | … |
- 大小写:无所谓,一般浏览器展示什么就写什么;
- AllowOrigins:值不建议写
*,另外如果前端设置了strict policy,就会无法使用*,但该选项可以不做设置,而是使用AllowOriginFunc设置; - ExposeHeaders:允许正式业务请求的header里携带x-jwt-token,不加这个前端读取不到 x-jwt-token,即后端给前端的前端才能拿,没给的前端就拿不到;
5 AOP
在理解AOP之前,先提个东西,它在Go中叫中间件,也就是常说的 middleware,在其他语言中有叫 plugin、handler、filter、interceptor 等等。

从图中可看到 middleware 是在处理请求发出之后,在业务处理之前锁执行的操作,所以它通常适合用来解决所有业务都关心的问题,比如跨域、日志、鉴权等。
这个解决方案就是 AOP (Aspect-Oriented Programming) 解决方案。
6 小结
解决跨域配置什么也是看请求的标头携带的什么,cors跨域就配置什么?
- 跨域问题是因为发送请求的协议+域名+端口和接受请求的协议+域名+端口对应不上,比如上面的
localhost:3000发送到localhost:28080上。 - 解决跨域问题的关键是在
preflight请求里面告诉浏览器自己愿意接受请求。 - 一般框架都有跨域解决方案。
7 更新日志
- 2026-01-27: 更新截图、修正错别字、补充“详解配置”。