浏览器会标出请求来自本站还是外站,服务端怎么据此挡住跨站请求
浏览器会标出请求来自本站还是外站,服务端据此挡住跨站请求伪造,靠的是几个前端改不了的来源标记请求头。规范给这组头留了四个位置,取值由浏览器在发送请求之前自动填好。网页脚本既不能新造同名字段,也不能改动已有内容,服务端因此能把它当作相对可信的信号,用来判断请求是从自己页面发出的,还是被别的站点借走登录态带过来的。
这组请求头是谁加上的?
这组头在规范里叫 fetch metadata 请求头,也就是拉取元数据请求头,浏览器在真正把请求发出去之前替你写好。四个成员分别是 Sec-Fetch-Site(站点关系)、Sec-Fetch-Mode(发起模式)、Sec-Fetch-User(是否用户发起)、Sec-Fetch-Dest(目标类型)。规范把它们归进禁止请求头那一类,意思是网页不能自定义同名头,脚本伪造一份来源标记这条路被堵住了。
还有个容易被忽略的边界是可信地址:浏览器只在判定为可信的请求里附带这组头,跑在明文地址上的站点收不到它们。所以服务端手上的信号其实有两种状态,一种是明确写着跨站,另一种是压根没有这组头,两种要分开处理,不能把缺失直接当成放行理由。
四个取值分别说明什么?
Sec-Fetch-Site 给出的是发起方与接收方之间的关系,分成同源、同站、跨站、无发起方四种,含义与处置见下表。同源要求协议、主机、端口三者完全一致,同站则是主机名相同或互为子域。
| 取值 | 表示的关系 | 典型场景 | 服务端常见处置 |
|---|---|---|---|
| same-origin(同源) | 协议、主机、端口完全一致 | 本站页面里发起的接口调用 | 按正常业务逻辑放行 |
| same-site(同站) | 主机名相同或互为子域 | 主站页面调用接口子域 | 读写分开,按接口性质放宽 |
| cross-site(跨站) | 双方没有站点关系 | 外部站点带着登录态发来的请求 | 写操作直接拒绝 |
| none(无发起方) | 看不出来源页面 | 地址栏直接访问、书签打开 | 只放浏览器导航类请求 |
其余三个头回答的是别的侧面。Sec-Fetch-Mode 说明请求以什么模式发出,是页面导航、嵌入资源还是接口调用;Sec-Fetch-Dest 说明这次请求想落到什么类型的目标上,是文档、脚本还是取数据;Sec-Fetch-User 只在少数由用户动作直接触发的请求里出现,用来说明这是人主动点出来的一次跳转。这几项组合起来,才分得清本站的接口调用和外部页面的自动提交。
服务端怎么写按上下文的放行策略?
可行的写法是给每一类接口声明它接受哪些上下文。带登录态的写接口通常只接受同源,同站的读请求按业务需要再放宽,看到跨站标记的写请求直接拒绝,不必再绕去校验令牌,因为来源关系本身已经不成立。头缺失要按接口性质单独定档:能在浏览器里直接打开的页面可以接受缺失,纯接口路径宁可拒绝,免得把没有信号读成有利信号。
这组头还有个容易漏掉的影响,就是缓存。放行既然按上下文判定,同一个地址在不同上下文下就可能得到不同结果,缓存也必须区分这些上下文,否则被拒绝的响应可能被复用到正常请求上,反过来也一样。稳妥的做法是让按上下文放行的接口把来源信息带进缓存键,或者干脆把这类接口排除在共享缓存之外。
它和写操作确认是互补不是替代?
这套标记回答的是谁发起了这次请求,不回答发起者有没有权限做这件事。它能挡住别人借登录态悄悄发一次写操作,挡不住合法用户在自己页面上点了不该点的按钮,所以它是补一道面,不能替代写之前的确认。
站内的 AI 接口侧(MCP)对写操作有回合级审批,会把一批改动合并成一次确认,人在这一轮点头之后改动才落地。两层的对象并不重叠:来源标记管的是请求是谁发起的,回合级审批管的是这次改动要不要人点头。前者在服务端边界上把外部来源筛掉,后者在智能体这一侧把批量变更收拢到同一个确认点,谁也替不了谁。
常见问题
这组头能挡住跨站请求伪造的全部情形吗? 不能。它只处理能被浏览器判定出关系的那部分,明文地址上压根收不到这组头,同站子域之间要靠别的约束。权限判断和写操作前的确认,仍然要一起在业务层里做完。
请求里没有这组头该怎么判? 先看服务地址是不是可信的,明文地址下浏览器不会发送。由浏览器之外的客户端直连时,这组头天然缺失,只能退回原有的鉴权与确认机制,把缺失当成信号不可用,而不是当成通过。
带跨站标记的请求就一定有问题吗? 不一定。第三方嵌入、合作方页面跳过来、外部链接引起的正常导航都带跨站标记。稳妥的用法是只把它用在写操作的拒绝上,读请求和导航按模式与目标类型再判。