当前位置:首页 > 文章列表 > Golang > Go问答 > 跨源保护与 CORS 同时配置时各自负责什么

跨源保护与 CORS 同时配置时各自负责什么

来源:17golang原创 2026-10-09 08:28:28 0浏览 收藏

跨源保护与 CORS 同时配置时,职责并不重复:Go 的 http.CrossOriginProtection 决定一个非安全的跨源浏览器请求能不能进入业务处理器,主要用来降低 CSRF 风险;CORS 决定浏览器是否允许某个 Origin 发起受控跨源访问并读取响应。前者是服务端请求准入,后者是浏览器响应共享协议,两者不能互相替代。

Go 官方文档:https://pkg.go.dev/net/http#CrossOriginProtection

Fetch 标准:https://fetch.spec.whatwg.org/

如果接口依赖 Cookie 等浏览器自动携带的凭据,就让 CrossOriginProtection 守住“请求是否进入业务”的边界;如果前端与 API 不同源,就让 CORS 明确“哪些 Origin 可以读取响应”。最省心的做法是让两者读取同一份精确 Origin 清单,但仍分别配置。

问题现场:CORS 放行了,POST 为什么还是 403

我第一次把这两层一起接到服务里时,浏览器预检已经返回了正确的 Access-Control-Allow-Origin,可真正的 POST 仍然收到 403。反过来,删掉 CrossOriginProtection 后,接口确实执行了,浏览器控制台却又报 CORS 错误。看起来像两个中间件互相打架,实际是两个不同判断都在起作用。

可以先从症状分辨:

现象更可能对应的边界首先检查
服务端直接返回 403,业务 handler 没执行CrossOriginProtectionSec-Fetch-Site、Origin、可信 Origin 清单
OPTIONS 预检失败CORS允许的方法、请求头、Origin 和预检处理
接口可能已执行,但前端读不到响应CORS响应中的 Allow-Origin、凭据设置和 Vary: Origin
同源请求正常,跨源写请求失败两层都可能先看状态码和服务端访问日志,再看浏览器 CORS 信息

初步判断:两层保护的对象不同

CrossOriginProtection 在 Go 1.25 加入 net/http。它会拒绝非安全的跨源浏览器请求,并在调用被包裹的 handler 之前完成检查。现代浏览器主要通过 Sec-Fetch-Site 判断来源;缺少这个头时,再比较 Origin 与请求 Host。GET、HEAD、OPTIONS 被视为安全方法并始终允许,因此应用本身不能让这些方法产生状态变更。

CORS 则来自浏览器的 Fetch 模型。服务端通过 Access-Control-Allow-Origin 等响应头说明哪些跨源前端可以读取响应;遇到非简单方法、非简单请求头或特定内容类型时,浏览器通常先发送 OPTIONS 预检。预检未通过时,浏览器不会继续发送实际请求。

这也解释了为什么“配置了 CORS”不等于“已经防住 CSRF”。简单跨源请求可能不需要预检,浏览器即使不把响应交给攻击页面,服务端仍可能已经执行了请求。CSRF 关注的是副作用有没有发生,不只是 JavaScript 能不能读到结果。

Go CrossOriginProtection 请求准入边界与 CORS 浏览器响应共享边界的静态关系说明图
图1:CrossOriginProtection 与 CORS 各自负责的静态边界说明图,不是运行截图。

动手配置:一份 Origin 清单,两套明确策略

两个策略通常应该共享业务认可的 Origin 清单,避免一边允许、一边拒绝。但共享数据不等于共享行为:CrossOriginProtection 仍负责请求准入,CORS 中间件仍负责预检和响应头。

package web

import (
	"fmt"
	"net/http"
)

var trustedOrigins = []string{
	"https://console.example.com",
	"https://admin.example.com:8443",
}

// BuildHandler 用同一份精确 Origin 清单配置 CORS 与跨源请求保护。
func BuildHandler(mux http.Handler) (http.Handler, error) {
	protection := http.NewCrossOriginProtection()
	allowed := make(map[string]struct{}, len(trustedOrigins))

	for _, origin := range trustedOrigins {
		// AddTrustedOrigin 要求 scheme://host[:port],不能包含路径。
		if err := protection.AddTrustedOrigin(origin); err != nil {
			return nil, fmt.Errorf("add trusted origin %q: %w", origin, err)
		}
		allowed[origin] = struct{}{}
	}

	// CORS 放在外层,先处理预检并为允许来源写入响应头。
	return corsMiddleware(allowed, protection.Handler(mux)), nil
}

func corsMiddleware(allowed map[string]struct{}, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		origin := r.Header.Get("Origin")
		_, ok := allowed[origin]
		if ok {
			// 回显已核对的精确来源,并告知缓存按 Origin 区分响应。
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Add("Vary", "Origin")
			w.Header().Set("Access-Control-Allow-Credentials", "true")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		}

		if r.Method == http.MethodOptions {
			// 预检来源不在白名单时立即拒绝,不进入业务处理器。
			if !ok {
				http.Error(w, "cors origin denied", http.StatusForbidden)
				return
			}
			w.WriteHeader(http.StatusNoContent)
			return
		}

		next.ServeHTTP(w, r)
	})
}

这个示例故意使用精确 Origin,而不是把字符串后缀当成域名授权。协议、主机或端口不同,就应视为不同来源。若允许携带凭据,也不要把 Access-Control-Allow-Origin 写成通配符 *;应在白名单命中后回显具体 Origin。

定位原因:四种不一致会产生不同结果

把两份配置想成两个集合后,问题会直观很多:

CORSCrossOriginProtection可能结果
允许 Origin A信任 Origin A预检可通过,非安全请求可进入业务,浏览器可读取响应
允许 Origin A不信任 Origin A预检可能通过,但实际非安全请求被 403 拒绝
不允许 Origin A信任 Origin A非简单请求可能止于预检;简单请求可能到达服务端,但响应不会暴露给前端
两边都不允许两边都不信任预检或跨源保护会拒绝,请求不应进入业务逻辑

最容易误判的是第三种:看到浏览器报 CORS 错误,就断言“后端没有执行”。这个结论不总成立。要结合请求是否触发预检、服务端访问日志和业务副作用一起判断。

CORS 允许来源集合与 CrossOriginProtection 可信来源集合的交集和不一致状态静态说明图
图2:CORS 允许集合与跨源保护信任集合的静态关系图,不是浏览器或服务端运行证据。

修复方案:中间件顺序先保证预检,再保护实际写请求

我更倾向让 CORS 在外层,CrossOriginProtection 在内层。这样 CORS 可以直接回答 OPTIONS 预检,并为允许的响应统一设置头;真正的 POST、PUT、PATCH、DELETE 进入业务 mux 前,再由 CrossOriginProtection 做跨源检查。由于 OPTIONS 本来就是 CrossOriginProtection 始终允许的安全方法,把顺序反过来也可能工作,但最终行为取决于 CORS 中间件是否短路预检、是否给拒绝响应补头,外层 CORS 通常更容易解释。

还有三个边界不能省略:

  • 认证仍是认证。CORS 和 CrossOriginProtection 都不验证用户身份,Cookie、会话、Token 和权限检查仍要单独完成。
  • GET 不能产生副作用。CrossOriginProtection 会始终允许 GET、HEAD、OPTIONS,删除、转账、发布等状态变更不能藏在这些方法里。
  • 非浏览器客户端要有自己的凭据。当前实现会允许同时缺少 Sec-Fetch-Site 和 Origin 的请求,因为它们被视为同源或非浏览器请求;API 密钥、签名或 OAuth 仍然必要。

验证结果:用请求矩阵而不是只看控制台

测试时至少覆盖允许来源、拒绝来源、同源请求和无浏览器来源头的客户端。下面的测试专门验证“是否进入业务”和“是否返回 CORS 头”这两个结果:

func TestCrossOriginPolicies(t *testing.T) {
	called := false
	app := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true // 记录业务处理器是否真的执行
		w.WriteHeader(http.StatusNoContent)
	})
	h, err := BuildHandler(app)
	if err != nil {
		t.Fatal(err)
	}

	// 允许来源:跨源 POST 应进入业务,并返回精确 CORS 来源。
	req := httptest.NewRequest(http.MethodPost, "https://api.example.com/items", nil)
	req.Header.Set("Origin", "https://console.example.com")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	rr := httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusNoContent || !called {
		t.Fatalf("allowed request: code=%d called=%v", rr.Code, called)
	}
	if got := rr.Header().Get("Access-Control-Allow-Origin"); got != "https://console.example.com" {
		t.Fatalf("allow origin=%q", got)
	}

	// 拒绝来源:业务处理器不能再次执行,响应应为 403。
	called = false
	req = httptest.NewRequest(http.MethodPost, "https://api.example.com/items", nil)
	req.Header.Set("Origin", "https://attacker.example")
	req.Header.Set("Sec-Fetch-Site", "cross-site")
	rr = httptest.NewRecorder()
	h.ServeHTTP(rr, req)
	if rr.Code != http.StatusForbidden || called {
		t.Fatalf("denied request: code=%d called=%v", rr.Code, called)
	}
}

如果第一组测试预检成功但实际 POST 失败,检查 CrossOriginProtection 的可信来源;如果业务已执行但浏览器仍提示跨域,检查 CORS 响应头;如果两者都失败,先把 Origin 字符串、协议、端口和代理后的 Host 对齐,再看中间件顺序。

常见问题

配置 CORS 后还需要 CrossOriginProtection 吗?

依赖 Cookie 等浏览器自动凭据、并且存在状态变更接口时,仍然值得使用。CORS 主要控制跨源读取,不是通用 CSRF 防护。

两边的 Origin 清单必须完全一样吗?

大多数后台前端与 API 场景保持一致最容易维护。若确有只允许读取、不允许写入,或只服务非浏览器客户端的特殊边界,可以分开,但要用请求矩阵记录差异,不能靠偶然顺序实现。

OPTIONS 会被 CrossOriginProtection 拒绝吗?

不会。Go 文档明确把 OPTIONS 与 GET、HEAD 视为安全方法并始终允许;预检是否成功由 CORS 策略决定。

没有 Origin 的请求为什么能通过?

当 Sec-Fetch-Site 与 Origin 都缺失时,当前实现把请求视为同源或非浏览器请求并允许。它只说明跨源检查没有拒绝,不代表请求已经认证或授权。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
汽车维修门店接收新能源车辆前要划分哪些作业区域汽车维修门店接收新能源车辆前要划分哪些作业区域
上一篇
汽车维修门店接收新能源车辆前要划分哪些作业区域
雨夜霓虹电车与湿润街道手机壁纸提示词
下一篇
雨夜霓虹电车与湿润街道手机壁纸提示词
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    386次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    468次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    475次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    415次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    241次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码