跳转至

跨域(CORS)

浏览器的同源策略看的是'协议+域名+端口'三元组,不是'同一台机器'——只要有一个不一样就跨域


核心概念

  1. 同源策略 — 浏览器的安全基石,不同源的请求一律先拦住
  2. Origin 三元组 — 协议 + 域名 + 端口,三者必须完全一致才算同源
  3. CORS — 跨源资源共享,服务器通过响应头告诉浏览器"允许谁来访问"
  4. 预检请求 — 非简单请求浏览器先发 OPTIONS 探路,确认安全后才发真正请求

详解

同源的定义

浏览器判断跨域的标准不是"是不是同一台机器",而是:

源(Origin)= 协议 + 域名 + 端口

https://example.com:443
  ↑        ↑         ↑
协议      域名      端口

三个都一样 → 同源 → 没问题
任何一个不一样 → 跨域 → 浏览器拦截

常见的"同一台主机但跨域"情况

1. 协议不同

页面:http://localhost:3000    (HTTP)
请求:https://localhost:3000   (HTTPS)

http ≠ https → 跨域!

2. 端口不同

页面:http://localhost:3000    (前端)
请求:http://localhost:8080    (后端 API)

3000 ≠ 8080 → 跨域!

前后端分离开发时极其常见。

3. 域名写法不同

页面:http://example.com
请求:http://www.example.com

example.com ≠ www.example.com → 跨域!

4. 子域名不同

页面:https://app.example.com      (前端)
请求:https://api.example.com      (后端 API)

app.example.com ≠ api.example.com → 跨域!

图示

graph TD
    subgraph 同源 ✅
        A1["https://example.com:443"]
        A2["https://example.com:443"]
        A1 --- A2
    end
    subgraph 跨域 ❌
        B1["https://app.example.com:443"]
        B2["https://api.example.com:443"]
        B1 --- B2
    end
    subgraph 跨域 ❌
        C1["http://localhost:3000"]
        C2["http://localhost:8080"]
        C1 --- C2
    end
    subgraph 跨域 ❌
        D1["http://example.com:80"]
        D2["https://example.com:443"]
        D1 --- D2
    end

为什么要拦截?

sequenceDiagram
    participant You as 你
    participant Bank as bank.com
    participant Evil as evil.com
    Note over You,Bank: 你登录了银行网站,浏览器存了 cookie
    You->>Evil: 访问恶意网站
    Evil->>Bank: 请求 bank.com/api/transfer(冒充你转账)
    Note over Bank: 浏览器同源策略拦截!
    Note over Bank: evil.com 的请求被阻止 😅

同源策略 = 浏览器替你守门,不同源的请求一律先拦住。

解决方案

方案 1:后端设置 CORS 响应头(最常用)

# Python Flask
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # 允许所有来源

# 或精确控制
CORS(app, origins=["https://app.example.com"])
// Go 标准库
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "https://app.example.com")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}
# Nginx
add_header Access-Control-Allow-Origin "https://app.example.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";

方案 2:前端开发代理

// Vite 开发服务器配置
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      }
    }
  }
}

浏览器看到的请求始终在 localhost:3000,同源,不触发跨域。

方案 3:Nginx 反向代理(生产环境)

server {
    listen 80;
    server_name app.example.com;

    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://localhost:8080;
        # 对浏览器:请求的是 app.example.com/api → 同源 ✅
    }
}

简单请求 vs 预检请求

类型 条件 行为
简单请求 GET/POST/HEAD + 简单 Header 浏览器直接发,看响应头判断
预检请求 PUT/DELETE/自定义 Header 等 浏览器先发 OPTIONS 探路,确认后再发

预检请求流程:

sequenceDiagram
    participant C as 浏览器
    participant S as 服务器
    C->>S: OPTIONS /api/data(预检)
    S->>C: 204 No Content
    Note over S: Access-Control-Allow-Origin: https://app.example.com
    Note over S: Access-Control-Allow-Methods: PUT, POST, GET
    C->>C: 确认允许
    C->>S: PUT /api/data(真正的请求)
    S->>C: 200 OK

网络面板里一个请求出现两次——第一次是 OPTIONS 预检,不是 bug。


常见陷阱

Access-Control-Allow-Origin 不能设为 * 同时带 Cookie

如果请求携带 Cookie(withCredentials: true),Access-Control-Allow-Origin 不能是 *,必须指定具体来源。否则浏览器会拒绝。

忘记处理 OPTIONS 预检请求

很多后端框架默认不处理 OPTIONS 方法,导致预检请求返回 405 Method Not Allowed。需要显式添加 OPTIONS 处理逻辑。


练习题

为什么 WebSocket 不受同源策略限制?
答案

WebSocket 连接一旦建立,就是全双工通信,不受 CORS 限制。但建立连接的**握手阶段**(HTTP Upgrade 请求)仍然受同源策略影响。所以 WebSocket 的安全性依赖服务器端的身份验证,而不是浏览器的同源策略。

如果后端设置了 Access-Control-Allow-Origin: *,前端 withCredentials: true 会怎样?
答案

浏览器会拒绝。规范明确要求:当请求携带凭据(Cookie、HTTP Auth)时,Access-Control-Allow-Origin 不能是通配符 *,必须是具体的源。这是为了防止任意网站都能通过你浏览器里的 Cookie 访问受保护资源。