Files
cs-note/hzh/DEV/跨域问题调试/跨域 HTTP 请求示例.md
T
2026-05-24 11:42:38 +08:00

205 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` 等价于浏览器无法判断请求来源站点,通常出现在以下情况:
> - `<a target="_blank">` 打开的新标签页中发起的请求
> - 书签页面(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<br/>Origin: https://mail.163.com<br/>Content-Type: application/x-www-form-urlencoded
S-->>B: 200 OK<br/>Access-Control-Allow-Origin: https://mail.163.com<br/>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<br/>Origin: https://mail.163.com<br/>Access-Control-Request-Method: POST<br/>Access-Control-Request-Headers: content-type
activate S
S-->>B: 204 No Content<br/>Access-Control-Allow-Methods: POST, OPTIONS<br/>Access-Control-Allow-Headers: content-type<br/>Access-Control-Max-Age: 86400
deactivate S
Note over B: ⏱ 预检通过,按 Max-Age 缓存此结果
B->>S: POST /stats/i<br/>Origin: https://mail.163.com<br/>Content-Type: application/octet-stream
activate S
S-->>B: 200 OK<br/>Access-Control-Allow-Origin: https://mail.163.com<br/>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<string, string>): Promise<void> {
// 构造表单数据(服务端期望 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]]