跨源保护与 CORS 同时配置时各自负责什么
跨源保护与 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 没执行 | CrossOriginProtection | Sec-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 能不能读到结果。

动手配置:一份 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。
定位原因:四种不一致会产生不同结果
把两份配置想成两个集合后,问题会直观很多:
| CORS | CrossOriginProtection | 可能结果 |
|---|---|---|
| 允许 Origin A | 信任 Origin A | 预检可通过,非安全请求可进入业务,浏览器可读取响应 |
| 允许 Origin A | 不信任 Origin A | 预检可能通过,但实际非安全请求被 403 拒绝 |
| 不允许 Origin A | 信任 Origin A | 非简单请求可能止于预检;简单请求可能到达服务端,但响应不会暴露给前端 |
| 两边都不允许 | 两边都不信任 | 预检或跨源保护会拒绝,请求不应进入业务逻辑 |
最容易误判的是第三种:看到浏览器报 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 都缺失时,当前实现把请求视为同源或非浏览器请求并允许。它只说明跨源检查没有拒绝,不代表请求已经认证或授权。
汽车维修门店接收新能源车辆前要划分哪些作业区域
- 上一篇
- 汽车维修门店接收新能源车辆前要划分哪些作业区域
- 下一篇
- 雨夜霓虹电车与湿润街道手机壁纸提示词
-
- Golang · Go问答 | 21分钟前 | 并发 · 基准测试 · go · RunParallel B.Loop Go并行基准测试 PB.Next testing.Benchmark
- 并行基准测试能否直接改用 B.Loop
- 142浏览 收藏
-
- Golang · Go问答 | 41分钟前 |
- testing.B.Loop 为什么不再需要手动读取 b.N
- 497浏览 收藏
-
- Golang · Go问答 | 1小时前 | go · testing · Go问答 · testing.B.Loop Go基准测试 循环外变量 benchmark状态
- B.Loop 中修改循环外变量为什么会影响基准结果
- 105浏览 收藏
-
- Golang · Go问答 | 1小时前 | 故障排查 · net/http · Go问答 · 反向代理 Sec-Fetch-Site Go CrossOriginProtection Origin Host 403误判
- 反向代理后 CrossOriginProtection 误判来源怎么办
- 201浏览 收藏
-
- Golang · Go问答 | 2小时前 | go · net/http ·
- CrossOriginProtection 为什么拒绝没有 Origin 的请求
- 186浏览 收藏
-
- Golang · Go问答 | 2小时前 | 错误处理 · go · 软链接 filepath.Clean filepath.IsLocal Go os.Root 路径拒绝
- 路径已经清理过为什么 os.Root 仍拒绝访问
- 329浏览 收藏
-
- Golang · Go问答 | 3小时前 | go · 文件系统 · 软链接 文件安全 Root.Open Go os.Root 路径边界
- os.Root 打开软链接为何仍可能返回边界错误
- 306浏览 收藏
-
- Golang · Go问答 | 3小时前 |
- 数据库中的 UUID 字节序与 Go 结果不一致怎么办
- 478浏览 收藏
-
- Golang · Go问答 | 4小时前 | uuid · Go问答 · Go标准库uuid Go uuid.Parse UUID小写格式化 uuid.String UUID规范化
- UUID 解析成功后为什么格式化结果变成小写
- 245浏览 收藏
-
- Golang · Go问答 | 4小时前 | 并发安全 · goroutine · Go问答 · Go结构化并发 runtime/secret并发读取 secret.Do goroutine Go密钥生命周期 WaitGroup敏感数据
- runtime/secret 在并发读取时应如何管理生命周期
- 107浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 386次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 468次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 475次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 415次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 241次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Golang实现HTTP编程请求和响应
- 2022-12-28 101浏览
-
- golangNewRequest/gorequest实现http请求的示例代码
- 2023-01-24 343浏览
-
- 一文详解Golang中net/http包的实现原理
- 2022-12-29 419浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览

