--- tags: [CORS, 跨域, HTTP, 网络安全, 前端调试] create time: 2026-04-29 10:30 --- # 跨域 HTTP 请求示例 — 网易邮箱 Countly 分析请求 ## 概述 本文档记录一次真实的浏览器 **CORS(Cross-Origin Resource Sharing)** 请求样本:来自 `mail.163.com` 向同域名下的 `countly.mail.163.com` 发起的 POST 事件追踪请求。通过分析完整的 HTTP 请求头和响应头,理解浏览器的 CORS 机制、服务端如何正确配置跨域策略,以及安全相关的首部字段含义。 > [!question] 思考 > `countly.mail.163.com` 和 `mail.163.com` 算"跨域"吗?严格来说它们属于不同子域,浏览器会将其视为不同 origin,因此仍然触发 CORS 流程。但如果服务端未配置 CORS 头,请求会被浏览器拦截——尽管这次请求实际成功返回了 `200 OK`。 ## 正文 ### 请求概览 这是一个典型的**前端埋点上报**请求,使用 Countly 分析 SDK 在用户发送邮件等行为时收集事件数据。 | 项目 | 值 | |------|----| | URL | `https://countly.mail.163.com/stats/i` | | 方法 | POST | | 状态码 | 200 OK | | Content-Type | `application/x-www-form-urlencoded` | | 响应内容长度 | 20 bytes(通常为一个透明像素 GIF) | ### 关键请求头解析 #### Origin & Referer — 跨域判定依据 > [!info] 核心概念 > 浏览器判断是否跨域的依据是 **origin = scheme + host + port**。即使同源(如都是 `.163.com`),只要 subdomain 不同就会触发预检或直接带 Origin 的请求。 ``` origin: https://mail.163.com referer: https://mail.163.com/ ``` - `origin`:由浏览器自动附加,**不可被 JS 手动设置**。服务端通过此字段决定要不要允许跨域。 - `referer`:告诉服务端页面来源,常用于统计和鉴权。 #### Sec-Fetch-* 系列 — 请求意图标识 > [!tip] W3C Fetch Metadata > Chrome 82+ 默认发送 `Sec-Fetch-*` 首部,帮助服务器理解请求性质,可以用来防御 CSRF 等攻击。 ``` sec-fetch-mode: cors ← 表明这是一个跨域请求 sec-fetch-site: same-site ← 同站(同一一级域名 .163.com) sec-fetch-dest: empty ← 无特定资源类型(非图片/脚本等) ``` **Sec-Fetch-Site 取值速查:** | 值 | 含义 | |----|------| | `same-origin` | 同源请求 | | `same-site` | 同站(共享 eTLD+1) | | `cross-site` | 真正的跨站 | | `none` | 无 referer 的特殊情况 | > [!info] `none` 的常见触发场景 > `sec-fetch-site: none` 等价于浏览器无法判断请求来源站点,通常出现在以下情况: > - `` 打开的新标签页中发起的请求 > - 书签页面(bookmarks:)作为来源时 > - 页面设置了 `Referrer-Policy: no-referrer` 且请求跨域 > - Service Worker / Background Sync 等后台任务发起的请求 > - HTTP 页面跳转到 HTTPS 资源(部分浏览器出于安全考虑丢弃 referer) > > 这些头部的派生链路:`Sec-Fetch-*` ← `document.referrer` ← 用户导航行为。referer 缺失则整组头部失效。 #### 安全相关头部 > [!warning] 引用站点策略 > `Referrer-Policy: strict-origin-when-cross-origin` 意味着: > - 同源请求:发送完整 referer > - 跨域请求:只发送 origin(不包含路径和参数) > - HTTPS → HTTP:不发送 referer(防止泄露) ### CORS 响应头解析 服务端的响应中包含了关键的 CORS 控制头: ```http access-control-allow-credentials: true access-control-allow-methods: GET,POST access-control-allow-origin: https://mail.163.com ``` | 响应头 | 作用 | |--------|------| | `Access-Control-Allow-Origin` | 指定允许访问的源,此处精确匹配 `https://mail.163.com`,而非通配符 `*` | | `Access-Control-Allow-Methods` | 允许的 HTTP 方法 | | `Access-Control-Allow-Credentials` | 允许携带 Cookie/认证凭据 | > [!important] Credentials + 通配符陷阱 > 当 `Access-Control-Allow-Credentials: true` 时,**不能**将 `Access-Control-Allow-Origin` 设置为 `*`。这是 CORS 规范中的硬性约束,违反会导致浏览器直接报错。此处的写法是正确的:精确指定允许的 origin。 ### CORS 请求全流程图解 #### 场景一:简单请求(无需预检)— 当前记录的情况 `Content-Type` 为 `application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain` 之一时,**只发 1 次 POST**: ```mermaid sequenceDiagram participant B as 浏览器 participant S as countly.mail.163.com participant M as mail.163.com(当前页面) Note over B,M: JS 发起事件追踪 B->>S: POST /stats/i
Origin: https://mail.163.com
Content-Type: application/x-www-form-urlencoded S-->>B: 200 OK
Access-Control-Allow-Origin: https://mail.163.com
Access-Control-Allow-Credentials: true Note over B: ✅ 校验通过,响应交由 JS 处理 ``` #### 场景二:需要预检的请求 — 常见情况 当 `Content-Type` 为 `application/json`、`application/octet-stream` 等非简单类型时,浏览器先发 **OPTIONS 预检**,通过后才发实际请求:**共 2 次**。 ```mermaid sequenceDiagram participant B as 浏览器 participant S as countly.mail.163.com participant M as mail.163.com(当前页面) Note over B,M: JS 发起带自定义 header 的跨域 POST B->>S: OPTIONS /stats/i
Origin: https://mail.163.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type activate S S-->>B: 204 No Content
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 86400 deactivate S Note over B: ⏱ 预检通过,按 Max-Age 缓存此结果 B->>S: POST /stats/i
Origin: https://mail.163.com
Content-Type: application/octet-stream activate S S-->>B: 200 OK
Access-Control-Allow-Origin: https://mail.163.com
Access-Control-Allow-Credentials: true deactivate S Note over B: ✅ 校验通过,响应交由 JS 处理 ``` > [!warning] 预检失败的处理 > 如果 OPTIONS 返回非 2xx 或响应中缺少必需的 CORS 头(如 `Allow-Methods`、`Allow-Headers`),浏览器会**直接放弃发送实际请求**并抛出 CORS 错误。这意味着客户端甚至不会看到服务端的原始响应。 ### 代码示例 以下是用原生 `fetch` API 复现此类埋点请求的代码: ```typescript // 发送跨域埋点事件(与 Countly SDK 行为类似) async function trackEvent(payload: Record): Promise { // 构造表单数据(服务端期望 application/x-www-form-urlencoded) const body = new URLSearchParams(payload).toString(); await fetch('https://countly.mail.163.com/stats/i', { method: 'POST', credentials: 'include', // 携带 Cookie(对应 Allow-Credentials: true) headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, // keepalive: true 用于 pagehide 时保证请求发出(可选优化) }); } // 调用示例 trackEvent({ event_key: 'email_sent', count: '1', duration: '120', }); ``` > [!note] 解释 > 上述代码中 `credentials: 'include'` 是关键:它让浏览器在跨域请求中携带 Cookie,这要求服务端必须明确返回 `Access-Control-Allow-Credentials: true` 且 `Access-Control-Allow-Origin` 必须是具体域名(不能是 `*`)。 ### 其他请求头一览 其余头部为常规 HTTP 标准字段,供参考: ``` :authority countly.mail.163.com # H2 pseudo-header,目标主机 :method POST # HTTP/2 :path /stats/i # 请求路径 :scheme https # 协议 accept */* # 接受任意内容类型 accept-encoding gzip, deflate, br, zstd # 支持的压缩算法 accept-language zh-CN,zh;q=0.9,... # 语言偏好 content-length 2184 # 请求体大小 priority u=1, i # HTTP/3 优先级 user-agent Mozilla/5.0 ... Edg/147 # 浏览器标识 sec-ch-ua "Microsoft Edge";v="147" # UA 品牌信息 sec-ch-ua-mobile ?0 # 非移动端 sec-ch-ua-platform "Windows" # 操作系统 ``` > [!info] 关于 HTTP/2 > 此请求使用了 HTTP/2(存在 `:authority`、`:method` 等伪头),HTTP/2 通过多路复用避免了传统 HTTP/1.1 的队头阻塞问题,对同时加载多个资源的现代 Web 应用性能提升明显。 ## 关联笔记 - [[localhost-vs-127.0.0.1]]