148 lines
4.2 KiB
Markdown
148 lines
4.2 KiB
Markdown
---
|
||
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 预检请求机制
|