Logo
Y

第三章 浏览器跨域请求问题


📅 | 📝 359 字
#go #cors

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.

这个现象就是所谓的跨域。从上面信息可以知道两点:

  1. http://localhost:3000http://localhost:28080/users/signup 发起的请求被阻塞了。
  2. preflight预检请求的响应未通过访问控制检查,preflight并没有传递access control check,导致上面问题的原因是因为请求的资源上没有设置 “Access-Control-Allow-Origin” 标头,在哪里设置呢?是在preflight request中设置。

正常来说,如果我们不做跨域处理,发起的请求就会出现类似这样的错误。

3 详解跨域

一个网站请求另一个网站,两个网站存在协议(如http、https)、域名(如a.com, b.com)、端口(如a.com:8080, a.com:8081)任意一个不同,即认为是跨域请求。

浏览器如何判断是否是两个网站的呢?

  1. 协议
  2. 端口
  3. 域名

比如请求是从 localhost:3000 发送到后端 localhost:28080 的,在请求时,浏览器认为 localhost:3000 是一个网站,localhost:28080 是一个网站。现在是从一个网站发送请求到另一个网站,浏览器就不答应,因为攻击者可以伪造自己的请求发送到后端服务器,浏览器是不允许这样的行为存在的。

主要是浏览器不允许这种行为,而后端无所谓,所以跨域请求其实就是浏览器阻止的。

想一想 浏览器为什么这么设计呢?主要是为了防止黑客伪造一些乱七八糟的东西不断地发送请求。

4 如何解决

4.1 思路

  1. 跨域的问题是浏览器不准请求从一个网站发往另一个网站。要解决这个问题,那就让浏览器允许将请求从一个网站发往另一个网站。
  2. 是什么让浏览器不允许发送请求呢?就是浏览器觉得网站携带的内容可能存在危险。
  3. 怎么才能让浏览器认为携带的内容没有危险呢?就是让浏览器告诉背后的服务器,请求都带了什么,背后的服务器也明确出给这个什么东西不危险。此时浏览器才放心请求允许通过。

比如问浏览器,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,在其他语言中有叫 pluginhandlerfilterinterceptor 等等。

从图中可看到 middleware 是在处理请求发出之后,在业务处理之前锁执行的操作,所以它通常适合用来解决所有业务都关心的问题,比如跨域、日志、鉴权等。

这个解决方案就是 AOP (Aspect-Oriented Programming) 解决方案。

6 小结

解决跨域配置什么也是看请求的标头携带的什么,cors跨域就配置什么?

  • 跨域问题是因为发送请求的协议+域名+端口和接受请求的协议+域名+端口对应不上,比如上面的localhost:3000发送到localhost:28080上。
  • 解决跨域问题的关键是在preflight请求里面告诉浏览器自己愿意接受请求。
  • 一般框架都有跨域解决方案。

7 更新日志

  • 2026-01-27: 更新截图、修正错别字、补充“详解配置”。