Go 服务怎么用 Sec-Fetch-Site 拦住跨站请求:Fetch Metadata 的最小中间件
来源:17golang原创
时间:2026-07-17 12:33:14 349浏览 收藏
有一次给后台的 POST /api/billing/plan 加 Cookie 登录后,接口测试和站内页面都正常,直到安全演练用第三方页面提交了一次表单。业务代码没有报错,浏览器却把带着登录态的请求送到了服务端。CSRF token 当然还要保留,但这里还可以多看一眼浏览器已经附带的 Sec-Fetch-Site:它能告诉服务端,这次请求来自同源、同站,还是另一个站点。
要点速览
Sec-Fetch-Site: cross-site适合触发敏感写操作的额外拦截,不代表用户未登录,也不能替代权限判断。- 中间件应先放行
same-origin、same-site和明确允许的公开端点,再对跨站的状态改变请求返回 403。 - 缺少 Fetch Metadata 的浏览器、脚本客户端和健康检查不能一刀切拒绝;这条分支应继续走 CSRF token、Origin 或鉴权校验。
- 按请求头做出不同决定时,响应要包含
Vary: Sec-Fetch-Site,避免缓存把一个用户的结果交给另一个请求上下文。
先看清 Sec-Fetch-Site 解决的是哪一段风险
很多服务只在写接口里校验 Cookie 和用户角色。它们能回答“谁在调用”,却不一定能回答“浏览器是从哪里发起的”。Fetch Metadata 是浏览器在请求层提供的一组上下文信号,Sec-Fetch-Site 其中最适合先落地:same-origin 表示协议、主机和端口相同;same-site 表示仍在同一站点范围;cross-site 则来自不同站点;地址栏直接打开或书签触发的导航可能是 none。
这不是身份凭据,也不是 CORS 替身。前端脚本不能自己伪造 Sec- 前缀的请求头,但命令行客户端、老浏览器、网关回放和自动化探针可能根本不携带它。因此更合适的做法是把它当作一层资源隔离闸门:带头且为 cross-site 的敏感写请求优先拦下;没有头时,不凭空放行,而是回到原有的 CSRF 和鉴权检查。
| 请求特征 | 中间件动作 | 后续仍要检查 |
|---|---|---|
same-origin 或 same-site | 交给业务处理 | 会话、角色、CSRF token |
cross-site + POST/PUT/PATCH/DELETE | 默认 403,除非端点被明确列为跨站公开 | 接口白名单是否过宽 |
none + GET/HEAD | 可作为直接导航继续处理 | 页面本身的鉴权与跳转 |
| 请求头缺失 | 不靠该字段拒绝,进入兼容路径 | CSRF、Origin、认证和限流 |

把跨站写请求拦在业务处理前
当时最容易犯的错是直接对所有 cross-site 返回 403。这样确实省事,却会把支付回调、公开嵌入资源或专门设计给第三方的接口一并打断。先把“状态改变”与“允许跨站”的端点拆开,策略会稳很多。下面的示例默认只拦截带有 cross-site 的非安全方法;/hooks/ 是演示中的明确例外,实际项目应换成自己的端点与签名校验。
package gate
import (
"net/http"
"strings"
)
func FetchSiteGate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
site := r.Header.Get("Sec-Fetch-Site")
addVary(w, "Sec-Fetch-Site")
if site == "cross-site" && changesState(r.Method) && !strings.HasPrefix(r.URL.Path, "/hooks/") {
http.Error(w, "cross-site request blocked", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func changesState(method string) bool {
return method != http.MethodGet && method != http.MethodHead && method != http.MethodOptions
}
func addVary(w http.ResponseWriter, value string) {
old := w.Header().Get("Vary")
if old == "" {
w.Header().Set("Vary", value)
return
}
w.Header().Set("Vary", old+", "+value)
}
代码刻意没有看到 site == "" 就拒绝。这个判断只能说明浏览器没有发送该提示,无法说明请求一定恶意。先让请求进入后续的 CSRF、Origin 和认证链,既能兼容 API 客户端,也不会把安全责任悄悄移到一个并非所有用户代理都支持的字段上。
为什么 CORS 通过了,cross-site 仍可能要拒绝
CORS 解决的是“浏览器是否允许脚本读取响应”的协商问题;Fetch Metadata 更像服务端对资源用途做的一次早期判断。一个接口即便已经允许特定前端域名跨源读取,也不等于所有跨站的 Cookie 写请求都应该进入业务层。这里别急着把 CORS 白名单塞进通用中间件,先列出真正需要跨站使用的 API,再为它们单独配置来源、方法、凭据和签名校验。
还有一个常被漏掉的细节是缓存。若同一个 URL 会因 Sec-Fetch-Site 得到不同结果,响应应携带 Vary: Sec-Fetch-Site。示例里的 addVary 没有覆盖已有字段,避免把 Accept-Encoding 等缓存维度抹掉。反向代理或 CDN 有自定义缓存规则时,也要确认它们会尊重这一维度;否则本来只给同源请求的内容,可能被错误复用。
请求头缺失时,回到 CSRF 与 Origin 的组合校验
上线前最需要验证的是“缺失请求头”这条路径。移动端、服务间调用、测试脚本以及部分浏览器都可能不带 Sec-Fetch-Site。如果中间件只靠它做准入,攻击者换一个普通 HTTP 客户端就绕开了策略;如果一律拒绝,又会误伤自己的 webhook、探针和自动化任务。
我更建议把它放在已有防线之前:有 cross-site 且属于敏感写操作,就尽早返回 403;请求头缺失或允许继续的请求,再验证会话、CSRF token,以及适合该端点的 Origin/Referer 约束。对机器回调接口,优先用专用凭据、时间戳和签名,而不是依赖浏览器头。

拿四组请求做一次回归,比只看 403 更可靠
在预发布环境里,可以用浏览器网络面板观察真实字段,再用 Go 的 httptest 覆盖中间件。以下四组足够发现多数策略写反的问题:
- 站内页面对
POST /api/billing/plan发请求,携带same-origin,应继续到业务层。 - 外部站点的表单或脚本发同一写请求,携带
cross-site,应收到 403,且业务审计日志不应出现状态修改。 - 浏览器直接访问一个需要展示的 GET 页面,若字段为
none,不应被写接口策略误伤。 - 模拟不带该字段的服务端调用,确认它没有因为空头直接失败,同时仍必须通过接口自己的认证或签名校验。
如果团队有跨域 SDK、嵌入式页面或第三方 webhook,别把它们藏在全局例外里。把端点、允许来源、凭据方式和负责人写成一张短清单。过几个月回头看,通常会发现“临时放行”的接口才是最需要收紧的地方。
相关问题
Sec-Fetch-Site 能完全代替 CSRF token 吗?
不能。它适合在浏览器携带字段时提供额外的请求上下文,但并非所有客户端都有该字段。Cookie 会话的写操作仍应保留合适的 CSRF 防护与权限校验。
same-site 就一定安全可靠吗?
也不能这样理解。same-site 只是站点关系信号,子域、跳转链和业务授权仍要分别评估。敏感接口不能只凭一个头决定用户能做什么。
API 给移动端和服务端调用时怎么处理?
不要强制要求 Fetch Metadata。为机器调用使用 token、mTLS、签名或其他明确的服务端身份机制,并把该路径和浏览器 Cookie 会话区分开。
为什么响应里要有 Vary: Sec-Fetch-Site?
因为同一资源会按这个请求头得到不同结果。Vary 让缓存知道不能把一种上下文生成的响应直接复用给另一种请求上下文。
把它当作早期闸门,而不是唯一防线
这层 Go 中间件最有价值的地方,是在业务处理之前把明显的跨站写请求挡住。它不替代 CSRF token,也不替代 Origin、签名和权限控制;它把原本分散在日志里的“请求从哪来”变成了一条可观察的服务端决策。先从一两个 Cookie 写接口试运行,记录被拒绝的路径,再逐步收紧例外范围,通常比一次性全站拦截更稳。
字段语义、缓存提示和兼容建议可继续参考 MDN 的 Fetch metadata、Sec-Fetch-Site,以及 web.dev 的 资源隔离实践。
-
480 收藏
-
177 收藏
-
485 收藏
-
348 收藏
-
250 收藏
-
392 收藏
-
Golang · Go问答 | 13小时前 | 云原生 · golang · 性能优化 · kubernetes · HPA · Go HPA不扩容 Kubernetes HPA CPU resources requests HPA并发指标 Go服务扩缩容111 收藏
-
Golang · Go问答 | 14小时前 | golang · 消息队列 · 性能优化 · websocket · 高并发 · Go WebSocket广播 Go慢客户端 WebSocket发送队列 Go Hub分片 WebSocket背压146 收藏
-
Golang · Go问答 | 14小时前 | 性能排查 · net/http · HTTP客户端 · Go问答 · 连接复用 · 连接复用 keep-alive http.Transport Response.Body Go HTTP Client241 收藏
-
115 收藏
-
Golang · Go问答 | 15小时前 | time.Parse · 时间处理 · Go问答 · Go基础 · 日期解析 · Go 日期解析 time.Parse YYYY-MM-DD 2006-01-02443 收藏
-
Golang · Go问答 | 15小时前 | HTTP · ResponseWriter · net/http · Go问答 · 后端排错 · Go WriteHeader ResponseWriter HTTP 状态码 superfluous response.WriteHeader258 收藏
-
Golang · Go问答 | 16小时前 | append · Slice · 内存模型 · Go问答 · Go基础 · CAP 底层数组 Go slice len Go切片传参 append返回值226 收藏
-
251 收藏
-
Golang · Go问答 | 1天前 | 并发 · golang · go · 循环变量 · Go 1.22 · 模块升级 · Goroutine 闭包 循环变量 go.mod Go 1.22 loopvar 升级迁移277 收藏
-
Golang · Go问答 | 1天前 | 依赖管理 · golang · 私有仓库 · 工程实践 · Go Modules · GOPROXY GOPRIVATE GONOPROXY GONOSUMDB Go私有模块 go mod download288 收藏
-
Golang · Go问答 | 1天前 | interface · 测试 · 架构设计 · Go问答 · 代码评审 · Go 接口设计 接口定义位置 Go mock accept interfaces return structs Go 架构300 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习