vault backup: 2026-04-29 11:02:09

This commit is contained in:
2026-04-29 11:02:09 +08:00
parent 3135616bb2
commit ea00c2fbe4
4 changed files with 626 additions and 1 deletions
@@ -0,0 +1,147 @@
---
tags:
- 跨域
- localhost
- 同源策略
- 浏览器安全
create time: 2026-04-29 15:30
---
# localhost vs 127.0.0.1 —— 开发环境最常见的"伪同源"坑
## 概述
记录一个高频踩坑场景:前端访问 `localhost`,后端监听 `127.0.0.1`,浏览器却报了 CORS 错误。它们明明指向同一台机器,为什么不算同源?
## 正文
### 现象
```
页面: http://localhost:3000
请求: http://127.0.0.1:8080/api/user
结果: ❌ CORS blocked — "No 'Access-Control-Allow-Origin' header"
```
开发者百思不得其解:`localhost` 不就等于 `127.0.0.1` 吗?
### 真相:浏览器不认"等价",只认"相同"
浏览器的**同源策略(Same-Origin Policy)**判定标准是严格的字符串比较:
```
同源 = 协议完全相同 && 域名完全相同 && 端口完全相同
```
这里的"域名"是一个**身份标识字符串**,不做任何语义换算。`localhost` 和 `127.0.0.1` 是两个不同的字符串,所以浏览器认为这是**两个不同的源**。
> **思考**:如果浏览器把 `localhost:3000` 和 `127.0.0.1:3000` 视为同源,会有什么问题?
### 为什么浏览器要这样设计?
这不是 bug,而是有意为之的安全决策:
```mermaid
flowchart LR
A["域名 = 身份标识"] --> B["a.example.com ≠ b.example.com<br/>即使部署在同一服务器"]
A --> C["localhost ≠ 127.0.0.1<br/>即使解析到同一个网卡"]
B --> D["最小权限原则:<br/>网络可达 ≠ 安全可信任"]
C --> D
```
| 原因 | 说明 |
|------|------|
| **子域名隔离** | 同机运行多个子域名站点,不应互相读取对方数据 |
| **Host Header 攻击面** | 如果有人伪造 Host 头写成 `localhost`,浏览器不能假设"IP 一样就是安全的" |
| **一致性保证** | 如果浏览器开始做"智能换算"(IP↔域名等价),规则会变得极其复杂且容易出漏洞 |
### 常见触发路径
```
前端 dev server (Vite/Webpack) → 默认 host = localhost:3000
↓
Node.js / Go / Python 后端 → 默认 bind = 127.0.0.1:8080(更安全)
↓
浏览器报 CORS 错误
```
| 框架 | 默认绑定地址 |
|------|------------|
| Vite dev server | `localhost` |
| Webpack devServer | `localhost` |
| Node.js `listen()` | `0.0.0.0`(全接口,但也可能绑定 `127.0.0.1`) |
| Python Flask debug | `127.0.0.1` |
| Gin (`r.Run(":8080")`) | `0.0.0.0`(但显式写 `r.Run("127.0.0.1:8080")` 时就是 `127.0.0.1`) |
### 解决方法
#### 方法一:统一域名(最推荐)
让前后端用同一个名称:
```bash
# 方式 A:全部用 localhost
前端: http://localhost:3000
后端: http://localhost:8080
# 方式 B:全部用 127.0.0.1
前端: http://127.0.0.1:3000
后端: http://127.0.0.1:8080
```
修改后端监听地址:
```go
// Go — 显式绑定 localhost
r.Run("localhost:8080")
// Node.js / Express
app.listen(8080, "127.0.0.1") // 或 "localhost"
```
#### 方法二:配置 CORS(正确姿势)
如果必须混用,在后端正确设置响应头:
```go
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
}
c.Header("Access-Control-Allow-Credentials", "true")
```
详见 `[[GIN/3-middleware/cors-registration-scope]]`。
#### 方法三:开发代理(最省事)
用 Vite proxy 转发请求,让它变成真正的同源:
```js
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
}
}
```
请求流程变为:
```
浏览器 → localhost:3000/api/user (同源)
↓ Vite devServer 转发
localhost:8080/api/user
```
> **生活化类比**:你家公司大门写着"大厦A座3层"(localhost),另一扇消防通道门贴着"3层回廊"(127.0.0.1)。虽然走到的是同一个办公室,但门禁系统只认牌匾上的名字——牌子不一样,就不给你刷卡。
## 关联笔记
- [[跨域问题调试]] — 完整的跨域排查指南
- [[GIN/3-middleware/cors-registration-scope]] — CORS 中间件注册方案
- [[GIN/3-middleware/cors-preflight]] — OPTIONS 预检请求机制
@@ -0,0 +1,204 @@
---
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]]