跨域解决方案——CORS学习了解

发布于:2025-09-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

前言

在前后端分离开发成为主流的今天,“跨域” 几乎是不可避免的情景。对于控制台弹出的 “Access to XMLHttpRequest at ‘xxx’ from origin ‘xxx’ has been blocked by CORS policy” 的错误时,大多都会去AI解决处理,却未必真正理解背后的逻辑。继上文对于同源策略的了解,本文将对"跨域解决方案"中其一的CORS做介绍。


一、为什么会有 CORS?

要理解 CORS,必须先明白它要解决的 问题,即同源策略(Same-Origin Policy)。(详情可参考上文,本文做一些简单讲解)

这是浏览器从诞生之初就有的安全规则,相当于一道 “防火墙”,目的是防止恶意网站窃取用户的敏感数据。

1. 同源策略:

所谓 “同源”,指的是请求的 “发起方”(比如前端页面)和 “资源方”(比如后端API服务器)必须满足 “三相同”,否则浏览器就会拦截请求。 具体是:

举个例子:如果前端页面部署在 http://localhost:8080,直接去请求 https://api.example.com(后端生产环境),这就属于 “不同源”,浏览器会出于安全考虑,拦截这次请求 —— 哪怕后端其实已经返回了数据,前端也拿不到。

2. 跨域场景:

同源策略虽然保护了用户安全,但在实际开发中,“跨域请求” 却是刚需。比如这些常见场景:

  • 开发环境下,前端用 localhost:8080,后端用 localhost:3000,请求接口时必然跨域
  • 生产环境中,前端静态页面放在 CDN(比如 cdn.abc.com),需要加载另一个域名(比如 img.xyz.com)的图片,或者请求后端 API(api.abc.com)(但出于静态文件是单向传递这一事实的考量,很多情况下这是同源策略的特许区)
  • 做第三方集成时,比如调用微信支付接口、地图 API,这些接口的域名肯定和你的前端域名不同。

这些场景下,同源策略的 “一刀切” 就成了 “过度保护”—— 加载 CDN 图片、调用合法 API 本应是安全的,但浏览器却拦截了。这时候,CORS 就登场了:它相当于浏览器和服务器之间的 “沟通桥梁”,既能允许合法的跨域请求通过,又能通过规则挡住恶意请求,完美解决了 “安全” 和 “功能” 的矛盾。

二、CORS 的核心原理:

CORS 并不需要复杂的代码——CORS 的核心是服务器通过 HTTP 响应头告诉浏览器 “我允许哪个域名的请求访问我”,整个协商过程由浏览器自动触发,前端几乎不用做额外操作(除非涉及 “预检请求”)。

根据请求的 “复杂程度”,CORS 会分为两种处理模式:简单请求预检请求(Preflight Request)。这两种模式的区别,本质上是浏览器为了平衡 “效率” 和 “安全”—— 简单请求风险低,直接发送;复杂请求风险高,先 “问一问” 服务器再发。

1. 简单请求:直接发送,一步到位

首先要明确:“简单请求” 不是指请求的内容简单,而是指请求的 “方式和头部” 符合浏览器定义的 “安全范围”,风险较低,所以浏览器允许直接发送,不用提前校验。

什么样的请求算 “简单请求”?

必须同时满足以下所有条件:

  • 请求方法仅限 3 种:GET(获取数据)、POST(提交数据)、HEAD(只获取响应头,不获取响应体);
  • 请求头仅限 “简单头”:要么是浏览器默认会携带的头(比如 Accept、Accept-Language,前端不用手动加),要么是前端手动加的 “简单头”(比如 Content-Language);
  • 如果是 POST 请求,Content-Type 只能是 3 种值:text/plain(纯文本)、multipart/form-data(表单文件上传常用)、application/x-www-form-urlencoded(传统表单提交常用)。

比如:用 GET 方法请求 https://api.example.com/data,或者用 POST 方法提交 form-data 格式的表单,都属于简单请求。

简单请求的完整流程
  1. 前端发起请求:比如前端用 Axios 发一个 GET 请求 axios.get('https://api.example.com/data'),此时浏览器会自动在请求头里加一个 Origin 字段,值是当前前端的域名(比如 http://localhost:8080),告诉服务器 “我是从这个域名来的”;
  2. 服务器响应并加 CORS 头:服务器收到请求后,会检查请求头里的 Origin,如果允许这个域名访问,就会在响应头里添加 Access-Control-Allow-Origin 字段,值就是前端的域名(比如 http://localhost:8080);
  3. 浏览器校验响应头:浏览器收到响应后,会先看有没有 Access-Control-Allow-Origin 这个头,并且值是否包含当前前端的域名。如果包含,就允许前端读取响应数据;如果没有这个头,或者值不匹配,就会拦截请求,在控制台报 “No ‘Access-Control-Allow-Origin’ header is present” 的错误。

过程就像:你(前端)去别人家(服务器)串门,门口保安(浏览器)先告诉主人(服务器)“这人是从 xx 小区来的”,主人看了觉得没问题,就跟保安说 “让他进来”,保安才会放行。

2. 预检请求:先 “询问”,再 “行动”

如果请求不满足 “简单请求” 的条件,比如用了 PUT/DELETE 方法(RESTful API 常用)、POST 请求的 Content-Type 是 application/json(前后端数据交互最常用),或者加了自定义的请求头(比如 Authorization 用来传 Token),这些都属于 “复杂请求”—— 浏览器认为这类请求风险更高,不会直接发送,而是先发送一次 “预检请求”,相当于 “先问一下服务器:我这个请求你允许吗?”,只有服务器明确同意,才会发送真正的业务请求。

预检请求的完整流程

我们以 “用 PUT 方法发送 JSON 数据” 为例,看看整个过程:

  1. 浏览器发送预检请求(OPTIONS 方法):在真正的 PUT 请求发送前,浏览器会先发送一个 OPTIONS 方法的请求(OPTIONS 是 HTTP 标准方法,专门用于 “查询服务器支持的方法”),这个请求的头里会包含 3 个关键信息:

    • Origin:当前前端域名(比如 http://localhost:8080),告诉服务器 “我来自哪”;
    • Access-Control-Request-Method:真正要使用的请求方法(比如 PUT),告诉服务器 “我接下来要用这个方法发请求”;
    • Access-Control-Request-Headers:真正请求中会带的自定义头(比如 Content-Type: application/json),告诉服务器 “我接下来要带这些头”。
  2. 服务器响应预检请求:服务器收到 OPTIONS 请求后,会根据这 3 个字段判断是否允许该请求,然后在响应头里返回允许的规则,核心是以下 4 个字段:

    响应头字段 作用说明
    Access-Control-Allow-Origin 允许的前端域名(比如 http://localhost:8080),必须和请求的 Origin 匹配
    Access-Control-Allow-Methods 允许的请求方法(比如 GET,POST,PUT,DELETE),必须包含请求中的方法
    Access-Control-Allow-Headers 允许的自定义头(比如 Content-Type,Authorization),必须包含请求中的头
    Access-Control-Max-Age 预检请求的缓存时间(比如 86400 秒,即 24 小时)—— 在这个时间内,相同的请求不用再发预检
  3. 浏览器判断是否发送业务请求:浏览器收到预检响应后,会检查这 4 个字段是否符合自己的请求需求。如果符合(比如允许的方法包含 PUT,允许的头包含 Content-Type),就会发送真正的 PUT 请求;如果不符合,就会拦截请求,控制台报错。

预检请求就像:你(前端)要去别人家(服务器)做一件比较复杂的事(比如装修),保安(浏览器)先打电话问主人(服务器)“这人要做 xx 事,你允许吗?”,主人说 “允许”,保安才会让你进去做这件事。

三、CORS 的关键配置:服务器端的 “4 个核心响应头”

前面我们反复提到,CORS 的核心是服务器配置响应头 —— 只要后端把这几个关键头配置对了,跨域问题就能解决。下面这 4 个是最常用、也最容易踩坑的头,必须掌握:

响应头字段 取值示例 关键注意事项
Access-Control-Allow-Origin http://localhost:8080* 1. * 表示允许所有域名访问,但不支持携带 Cookie(如果需要跨域带 Cookie,必须写具体域名); 2. 这个字段只能填 1 个域名(如果需要允许多个域名,后端要动态判断请求的 Origin,然后返回对应的域名)
Access-Control-Allow-Credentials truefalse 1. 设为 true 时,表示允许跨域请求携带 Cookie 或认证信息(比如 JWT Token 存在 Cookie 里); 2. 一旦设为 trueAccess-Control-Allow-Origin 就不能用 *,必须填具体域名
Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS 1. 要包含所有允许的请求方法,尤其是 OPTIONS 方法(预检请求要用); 2. 顺序不影响,但建议把常用方法都列出来,避免遗漏
Access-Control-Allow-Headers Content-Type,Authorization,X-Token 1. 要包含前端会用到的所有自定义头,比如传 Token 用的 Authorization、自定义的 X-Token; 2. 如果前端加了新的自定义头,这里必须同步添加,否则预检会失败

举个简单的逻辑示例:如果后端要允许前端 http://localhost:8080 跨域携带 Cookie 访问,就必须同时返回 Access-Control-Allow-Origin: http://localhost:8080Access-Control-Allow-Credentials: true,缺一不可。

四、CORS 常见问题及解决方案

即使理解了原理,实际开发中还是会遇到各种报错。下面是 3 个最常见的问题,以及对应的解决方案,帮你快速排错:

1. 报错:“No ‘Access-Control-Allow-Origin’ header is present on the requested resource”

这是最常见的错误,翻译过来就是 “请求的资源没有返回 Access-Control-Allow-Origin 头”。

原因分析

  • 后端根本没配置 CORS 头,或者配置了但 Access-Control-Allow-Origin 的值不对(比如前端是 localhost:8080,后端填成了 localhost:3000);
  • 后端用了框架自带的 CORS 中间件,但配置有误(比如某些框架默认不允许跨域)。

解决方案

  • 先检查后端代码,确认是否添加了 Access-Control-Allow-Origin 头,且值是当前前端的域名(开发环境是 localhost:8080,生产环境是你的前端域名,比如 https://www.abc.com);
  • 如果用了中间件(比如 Go 的自定义中间件、Java 的 @CrossOrigin),检查中间件的配置是否正确,确保允许的域名包含当前前端域名。

2. 问题:跨域请求无法携带 Cookie

很多时候,我们需要跨域携带 Cookie(比如用户登录后,Cookie 里存了 Session ID),但发现 Cookie 根本传不过去。

原因分析

  • 后端没配置 Access-Control-Allow-Credentials: true,浏览器不允许携带 Cookie;
  • 后端把 Access-Control-Allow-Origin 设为了 *(带 Cookie 时不允许用通配符);
  • 前端请求没开启 “携带 Cookie” 的配置(比如 Axios 默认不携带,需要手动开启)。

解决方案

  • 后端必须同时满足两个条件:Access-Control-Allow-Credentials: true + Access-Control-Allow-Origin 填具体域名(不能用 *);
  • 前端配置请求时开启携带 Cookie:比如 Axios 要设置 axios.defaults.withCredentials = true,Fetch API 要设置 credentials: 'include'

3. 报错:“OPTIONS 请求返回 404/500 错误”

这种情况通常是预检请求失败,浏览器发送的 OPTIONS 请求被后端拦截,返回了 404(找不到资源)或 500(服务器错误)。

原因分析

  • 后端没处理 OPTIONS 方法,比如路由只配置了 GET/POST,没配置 OPTIONS,导致请求被拦截;
  • 后端的权限校验中间件(比如 Token 校验)拦截了 OPTIONS 请求 —— 预检请求是没有业务数据的,自然没有 Token,所以会被当成 “非法请求” 拦截。

解决方案

  • 后端必须允许 OPTIONS 方法,并且在处理 OPTIONS 请求时,直接返回 200 状态码和完整的 CORS 头(不用走业务逻辑);
  • 让权限校验中间件跳过 OPTIONS 请求的校验 —— 比如在 Token 校验前加判断,如果是 OPTIONS 方法,直接放行。

五、CORS 与其他跨域方案的对比:

除了 CORS,开发中还会接触到 JSONP、代理服务器等传统跨域方案,它们各有优缺点,适用场景也不同。我们通过表格详细对比:

跨域方案 核心原理 优点 缺点 适用场景
CORS 基于浏览器与服务器的 HTTP 头协商,服务器通过响应头明确允许的跨域规则 1. 支持所有 HTTP 方法(GET/POST/PUT/DELETE 等),满足 RESTful API 需求; 2. 安全可控,可精准限制允许的域名、方法、头; 3. 前端几乎不用写额外代码 1. 依赖后端配置(前端无法单独解决); 2. IE10 及以下浏览器部分不支持(现在很少用旧 IE 了) 现代前后端分离项目(Vue/React/Angular 等框架开发),是当前主流且推荐的方案
JSONP 利用 <script> 标签不受同源策略限制的特性,通过回调函数获取数据 1. 兼容性极强,支持所有浏览器(包括 IE6/7 等老旧版本); 2. 前端可独立实现,无需后端修改复杂配置 1. 仅支持 GET 方法,无法满足 POST/PUT/DELETE 等请求需求; 2. 安全性低,易遭受 XSS 攻击; 3. 无法捕获请求错误(如 404/500) 需兼容极老旧浏览器(如政府 / 企业遗留系统),且仅需简单 GET 请求的场景(目前已极少使用)
代理服务器 前端先请求同源的代理服务器,再由代理转发到目标跨域服务器,最后返回结果 1. 无需后端修改 CORS 配置,前端可独立解决; 2. 避开浏览器同源限制,请求透明; 3. 可在代理层加额外逻辑(如缓存、拦截) 1. 增加代理层,消耗服务器资源,可能延迟; 2. 生产环境需维护代理服务,增加复杂度; 3. 配置不当易有安全风险 1. 开发环境(如 Webpack Dev Server、Vite 配置 proxy); 2. 后端无法修改 CORS 配置的场景(如调用第三方 API)

六、CORS 实战配置示例:Go 语言后端怎么配?

Go 语言中没有专门的 “CORS 框架”,但可通过自定义中间件统一处理 HTTP 请求头,灵活实现 CORS 配置。以下分 “基础配置”“允许多域名”“支持携带 Cookie” 三种高频场景,给出可直接复用的代码,确保贴合实际开发需求。

1. 基础配置:允许指定域名的简单跨域请求

适用于前端仅发起 GET/POST/HEAD 等简单请求,且无需携带 Cookie 的场景(如开发环境前端 http://localhost:8080 访问 Go 后端 http://localhost:3000)。

package main

import (
	"net/http"
)

// corsMiddleware 自定义 CORS 中间件:处理所有请求的跨域头
func corsMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 1. 核心 CORS 响应头配置
		// 允许的前端域名(开发环境填 localhost:8080,生产环境填实际前端域名,如 https://www.your-front.com)
		w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8080")
		// 允许的请求方法(覆盖简单请求+复杂请求常用方法,避免漏配)
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		// 允许的请求头(包含前端默认头+常用自定义头,如 Content-Type 用于传 JSON 数据)
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		// 预检请求缓存时间(24小时,单位秒):减少重复预检,提升性能
		w.Header().Set("Access-Control-Max-Age", "86400")

		// 2. 处理预检请求(OPTIONS 方法):无需走业务逻辑,直接返回 200 确认允许
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		// 3. 放行请求:进入后续业务接口处理
		next.ServeHTTP(w, r)
	})
}

// 业务接口示例:返回简单 JSON 数据
func dataHandler(w http.ResponseWriter, r *http.Request) {
	// 设置响应格式为 JSON
	w.Header().Set("Content-Type", "application/json")
	// 返回成功状态码和数据
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"code":200,"message":"跨域请求成功","data":{"name":"Go CORS Demo","type":"simple request"}}`))
}

func main() {
	// 1. 注册路由:所有接口先经过 CORS 中间件
	mux := http.NewServeMux()
	mux.HandleFunc("/api/data", dataHandler) // 业务接口

	// 2. 启动服务器:监听 3000 端口,用中间件包装路由
	server := &http.Server{
		Addr:    ":3000",
		Handler: corsMiddleware(mux), // 应用 CORS 中间件
	}

	// 3. 启动服务并监听错误
	println("Go 后端服务器运行在 http://localhost:3000(已配置 CORS)")
	if err := server.ListenAndServe(); err != nil {
		panic("服务器启动失败:" + err.Error())
	}
}

2. 进阶配置:允许多个前端域名访问

适用于需要同时支持 “开发环境 + 测试环境 + 生产环境” 多域名的场景(如 http://localhost:8080 开发、http://test-front.com 测试、https://prod-front.com 生产)。通过判断请求头中的 Origin 是否在 “允许列表”,动态返回合法域名,避免直接用 * 带来的安全风险。

package main

import (
	"net/http"
)

// 定义允许的前端域名列表:根据实际环境维护
var allowedOrigins = []string{
	"http://localhost:8080",   // 开发环境
	"http://test-front.com",   // 测试环境
	"https://prod-front.com",  // 生产环境
}

// isOriginAllowed 校验请求域名是否在允许列表中
func isOriginAllowed(origin string) bool {
	for _, allowed := range allowedOrigins {
		if origin == allowed {
			return true
		}
	}
	return false
}

func corsMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 1. 动态获取并校验请求域名(从请求头 Origin 中获取)
		requestOrigin := r.Header.Get("Origin")
		// 若请求域名在允许列表中,才返回 Access-Control-Allow-Origin(避免非法域名访问)
		if requestOrigin != "" && isOriginAllowed(requestOrigin) {
			w.Header().Set("Access-Control-Allow-Origin", requestOrigin)
		}

		// 2. 固定 CORS 头配置(同基础配置)
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		w.Header().Set("Access-Control-Max-Age", "86400")

		// 3. 处理预检请求
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		// 4. 放行请求
		next.ServeHTTP(w, r)
	})
}

// 业务接口示例:多域名场景下的用户信息接口
func userHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	// 返回当前请求的域名,方便调试
	w.Write([]byte(`{"code":200,"message":"多域名跨域请求成功","data":{"allowed_origin":"` + r.Header.Get("Origin") + `"}}`))
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/user/info", userHandler) // 多域名支持的接口

	server := &http.Server{
		Addr:    ":3000",
		Handler: corsMiddleware(mux),
	}

	println("Go 后端服务器运行在 http://localhost:3000(支持多域名 CORS)")
	if err := server.ListenAndServe(); err != nil {
		panic("服务器启动失败:" + err.Error())
	}
}

3. 高级配置:支持跨域携带 Cookie

适用于需要跨域传递 Cookie 的场景(如用户登录后,Cookie 中存储 Session ID 或认证 Token)。需同时满足 “后端开启允许 Cookie” 和 “前端开启携带 Cookie” 两个条件,且 Access-Control-Allow-Origin 不能用 \*(浏览器强制限制,确保安全)。

package main

import (
	"net/http"
)

// 允许的域名列表(必须指定具体域名,不能用 *)
var allowedOrigins = []string{
	"http://localhost:8080",  // 开发环境前端
}

func isOriginAllowed(origin string) bool {
	for _, allowed := range allowedOrigins {
		if origin == allowed {
			return true
		}
	}
	return false
}

func corsMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestOrigin := r.Header.Get("Origin")
		if requestOrigin != "" && isOriginAllowed(requestOrigin) {
			w.Header().Set("Access-Control-Allow-Origin", requestOrigin)
			// 关键配置:开启允许跨域携带 Cookie(必须配合具体 Origin,不能用 *)
			w.Header().Set("Access-Control-Allow-Credentials", "true")
		}

		// 其他固定 CORS 头(同前)
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		w.Header().Set("Access-Control-Max-Age", "86400")

		// 处理预检请求
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

// 业务接口示例:读取跨域携带的 Cookie(如用户登录后的 Session ID)
func authHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// 读取 Cookie 中的用户 ID(示例逻辑:实际项目需结合 Session 或认证逻辑)
	userIDCookie, err := r.Cookie("user_id")
	var userID string
	if err != nil {
		userID = "未获取到用户 Cookie(可能未登录或未携带 Cookie)"
	} else {
		userID = userIDCookie.Value
	}

	// 返回结果
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"code":200,"message":"跨域携带 Cookie 成功","data":{"user_id":"` + userID + `"}}`))
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/auth/info", authHandler) // 需携带 Cookie 的接口

	server := &http.Server{
		Addr:    ":3000",
		Handler: corsMiddleware(mux),
	}

	println("Go 后端服务器运行在 http://localhost:3000(支持跨域携带 Cookie)")
	// 前端配合说明:Axios 需设置 axios.defaults.withCredentials = true;Fetch 需设置 credentials: 'include'
	if err := server.ListenAndServe(); err != nil {
		panic("服务器启动失败:" + err.Error())
	}
}

七、总结:

  1. CORS 本质是 “规则约定”:核心是服务器通过 HTTP 头告诉浏览器 “允许谁访问”,所有逻辑围绕 “安全” 与 “功能” 平衡,前端几乎无需额外代码,重点在后端配置;
  2. Go 配置关键是 “中间件”:通过自定义中间件统一处理所有请求的 CORS 头,避免在每个接口重复写配置,同时必须单独处理 OPTIONS 预检请求,确保复杂请求能通过;
  3. 避坑三要素:
    • 携带 Cookie 时,Access-Control-Allow-Origin 必须填具体域名,不能用 *,且要配 Access-Control-Allow-Credentials: true
    • 允许多域名时,通过 “允许列表 + 动态判断 Origin” 实现,不建议直接开放所有域名;
    • 预检请求必须返回 200,且不能被权限校验中间件拦截(如 Token 校验要跳过 OPTIONS 方法);
  4. 方案选择优先 CORS:现代项目首选 CORS,兼容性和安全性最优;开发环境临时用代理,兼容旧浏览器才考虑 JSONP(目前极少用)。

网站公告

今日签到

点亮在社区的每一天
去签到