CORS跨域资源共享

1.0 核心认知:跨域请求实际发出去了吗?

先说清楚最容易被误解的地方:


graph TD

    A["前端 fetch 跨域请求"] --> B{"浏览器检查请求类型"}

    B -->|"简单请求"| C["直接发出去,服务器正常处理"]

    B -->|"非简单请求"| D["先发 OPTIONS 预检,通过再发真正请求"]

    C --> E["服务器返回响应"]

    D --> E

    E --> F{"浏览器检查响应头里<br/>有没有正确的 CORS 头"}

    F -->|"有"| G["JS 拿到数据 ✅"]

    F -->|"没有"| H["浏览器把响应扔掉,控制台报 CORS 错 ❌"]

关键:请求确确实实发出去了,服务器也处理了。是浏览器拿到了响应但不给 JS。

类比——门卫看快递:快递员(服务器)把包裹(响应)送到你家楼下。门卫(浏览器)一看寄件地址(Origin)→ 不在白名单上 → 包裹扣下,不给你。包裹确实到了,快递确实送了——是门卫拦的。

1.1 简单请求 vs 非简单请求——分界线决定要不要预检

简单请求——同时满足三个条件(缺一不可)

条件说明
① 方法GET、POST、HEAD(三者之一)
② 头部只能用”安全列表”内的请求头:Accept、Accept-Language、Content-Language、Content-Type
不能加自定义头(如 Authorization、X-Token 等)
③ Content-Type只能是这三种:application/x-www-form-urlencodedmultipart/form-datatext/plain

注意:application/json 不是安全列表里的 Content-Type!只要 Content-Type 是 application/json → 非简单请求!

非简单请求——以下任何一条满足都是

条件说明
① 方法PUT、DELETE、PATCH、OPTIONS
② 头部带了自定义请求头(Authorization、X-Custom-Header、Content-Type 之外的头)
③ Content-Typeapplication/json(你项目里 99% 的接口都是这个!)

这就是为什么你的项目一定会触发 OPTIONS 预检——因为你用 axios 发 POST,Content-Type 默认就是 application/json

1.2 简单请求——直接发,浏览器在请求头加上 Origin

 
GET /api/users HTTP/1.1
 
Host: api.example.com
 
Origin: http://localhost:3000         ← 浏览器自动加的,告诉服务器"我来自哪里"
 
服务器收到后,如果想允许这个来源,就在响应头里加上:
 
HTTP/1.1 200 OK
 
Access-Control-Allow-Origin: http://localhost:3000   ← "我允许你跨域读我的数据"
 
Content-Type: application/json
 
浏览器收到响应 → 检查 Access-Control-Allow-Origin 是否包含当前 Origin
 
  → 包含 → JS 拿到数据 ✅
 
  → 不包含 → 扔响应,报 CORS 错 ❌
 

1.3 非简单请求——先问”能不能来”,服务器说行再来

 
axios.post('http://api.example.com/users', data)
 
// Content-Type: application/json  ← 非简单请求!触发预检!
 
第一步:浏览器自动发 OPTIONS 预检请求(不需要你写任何代码)
 
OPTIONS /api/users HTTP/1.1
 
Host: api.example.com
 
Origin: http://localhost:3000
 
Access-Control-Request-Method: POST                    ← "我打算用 POST 方法"
 
Access-Control-Request-Headers: content-type           ← "我会带 content-type 头"
 

服务器收到 → 检查自己是否允许这个来源 + 这个方法 + 这个头

  • 允许 → 返回:
 
HTTP/1.1 200 OK
 
Access-Control-Allow-Origin: http://localhost:3000
 
Access-Control-Allow-Methods: GET, POST, PUT, DELETE   ← "这些方法我允许"
 
Access-Control-Allow-Headers: Content-Type, Authorization ← "这些头我允许"
 
Access-Control-Max-Age: 86400                          ← "这个预检结果缓存 24 小时"
 
  • 不允许 → 返回 200 但没有这些头 → 浏览器拦截,真正请求不发
第二步:预检通过 → 浏览器才发真正的 POST 请求
 
POST /api/users HTTP/1.1
 
Host: api.example.com
 
Origin: http://localhost:3000
 
Content-Type: application/json
 
{"name": "张三", "age": 22}
 

服务器正常返回:

 
HTTP/1.1 200 OK
 
Access-Control-Allow-Origin: http://localhost:3000
 
Content-Type: application/json
 
{"id": 1, "name": "张三"}
 

浏览器再次检查 Access-Control-Allow-Origin → 通过 → JS 拿到数据 ✅


graph TD

    subgraph 简单请求

        A["真正请求"] --> B["服务器"] --> C["浏览器检查CORS头"] --> D["JS拿到 / 被拦截"]

    end

    subgraph 非简单请求

        E["OPTIONS预检"] --> F["服务器回CORS头"] --> G{"通过?"}

        G -->|"通过"| H["真正请求"]

        G -->|"不通过"| I["拦截,真正请求不发"]

    end

Access-Control-Max-Age 的作用——预检不用每次都发:

同一个 URL,同样的请求方法+头,浏览器会在 Max-Age 秒内复用预检结果。

比如 Max-Age=86400(24小时):

  • 第一次 POST /api/users → 发 OPTIONS

  • 接下来 24 小时内 POST /api/users → 不再发 OPTIONS,直接用缓存结果

类比——出入证:第一天进门,保安查你身份证、工作证、健康码(OPTIONS 预检)。给你办了个出入证(缓存),有效期 24 小时。今天之内再进出,出示出入证就行,不用再查三件套。

一句话讲清:“CORS 把请求分成简单和非简单两种。简单请求(GET/POST/HEAD + 安全列表头 + 安全列表 Content-Type)直接发,浏览器自动带 Origin。非简单请求(JSON Content-Type、自定义头、PUT/DELETE)先发 OPTIONS 预检——问服务器是否允许当前来源+方法+头,通过后才发真正的请求。用 Max-Age 缓存预检结果,避免每次都问。“

1.4 六个关键 CORS 响应头——缺一报错

Header干什么?缺了会怎样?
① Access-Control-Allow-Origin
例: http://localhost:3000
告诉浏览器”这些来源可以读我”
可以是具体域名,不能用 * + 凭证同用
最常见的 CORS 报错原因
② Access-Control-Allow-Methods
例: GET, POST, PUT, DELETE
告诉浏览器”这些方法我允许”
非简单请求必须包含你要用的方法
预检返回的方法不在列表里→拦截
③ Access-Control-Allow-Headers
例: Content-Type, Authorization
告诉浏览器”这些请求头我允许”
非简单请求必须包含你要用的头
你带了 Authorization 但没列在这→拦截
④ Access-Control-Allow-Credentials
例: true
告诉浏览器”允许携带 Cookie/凭证”
必须和前端 withCredentials 配对
设为 true 才能真正跨域传 Cookie
⑤ Access-Control-Max-Age
例: 86400(24小时)
预检结果缓存多久(秒)
建议设 86400,浏览器有上限(Chrome 2h)
每次请求都预检,多一次往返
⑥ Access-Control-Expose-Headers
例: X-Total-Count
告诉浏览器”这些响应头 JS 可以读”
你的自定义响应头 JS 读不到→加在这
JS 默认只能读 safe-listed 响应头

特别注意——两个经典翻车场景

场景一:带了 Authorization 头但没配 Allow-Headers

前端:axios.get(url, { headers: { Authorization: 'Bearer xxx' }})

后端:只配了 Access-Control-Allow-Origin

→ 预检时发现 Authorization 不在 Allow-Headers 里

→ 浏览器拦截:Request header field Authorization is not allowed by Access-Control-Allow-Headers

场景二:想跨域带 Cookie 但只配了 Allow-Origin: *

后端:Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true

→ 浏览器:你配了 Credentials 就不能用 *

→ 必须把 * 改成具体域名——这是浏览器的硬限制,不是后端 bug

1.5 CORS 头怎么配——后端代码直接抄

FastAPI 配 CORS(Python 后端——你的项目用这个)
 
from fastapi import FastAPI
 
from fastapi.middleware.cors import CORSMiddleware
 
app = FastAPI()
 
origins = [
 
    "http://localhost:3000",       # React 开发服务器
 
    "https://your-frontend.com",   # 生产环境前端域名
 
]
 
app.add_middleware(
 
    CORSMiddleware,
 
    allow_origins=origins,         # ✅ 白名单具体域名,不要用 ["*"]
 
    allow_credentials=True,        # ✅ 允许携带 Cookie
 
    allow_methods=["GET", "POST", "PUT", "DELETE"],  # 只开放需要的方法
 
    allow_headers=["Content-Type", "Authorization"], # 只开放需要的头
 
    expose_headers=["X-Total-Count"],                 # 让前端 JS 能读到自定义响应头
 
    max_age=86400,                 # 预检缓存 24 小时
 
)
 
Express.js 配 CORS(Node 后端)
 
const cors = require('cors');
 
app.use(cors({
 
    origin: ['http://localhost:3000', 'https://your-frontend.com'],
 
    credentials: true,
 
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
 
    allowedHeaders: ['Content-Type', 'Authorization'],
 
}));
 
Nginx 反向代理配 CORS(生产环境推荐——不需要改后端代码)
 
# 但如果仍需给外部暴露 API,加上 CORS 头:
 
location /api/ {
 
    # 处理 OPTIONS 预检
 
    if ($request_method = 'OPTIONS') {
 
        add_header Access-Control-Allow-Origin 'https://your-frontend.com';
 
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE';
 
        add_header Access-Control-Allow-Headers 'Content-Type, Authorization';
 
        add_header Access-Control-Max-Age 86400;
 
        return 204;    # 预检直接返回,不往后端转发
 
    }
 
    # 正常请求也加 CORS 头
 
    add_header Access-Control-Allow-Origin 'https://your-frontend.com';
 
    add_header Access-Control-Allow-Credentials 'true';
 
    proxy_pass http://backend:8000;
 
}
 

速记卡(面试闪卡)

Q1:一句话讲清「CORS跨域资源共享」到底是什么?

A:CORS 是浏览器的一道”门卫”:跨域请求服务器其实处理了,只是响应头不对就不让 JS 拿。

Q2:一、核心认知:请求到底发出去没 —— 怎么理解? —— 怎么理解?

A:像门卫扣快递:快递员(服务器)把包裹(响应)送到楼下,门卫(浏览器)一看寄件地址(Origin)不在白名单,就把包裹扣下不给。请求确确实实发出去了、服务器也处理了,是浏览器拿到响应却不交给 JS。英文:Origin / same-origin policy。

Q3:二、简单请求 vs 非简单请求 —— 怎么理解? —— 怎么理解?

A:像进楼分两种:简单请求(GET/POST/HEAD + 安全头 + 表单型 Content-Type)直接进,浏览器自动带 Origin;非简单请求(JSON 的 Content-Type、自定义头、PUT/DELETE)先发 OPTIONS 预检”问能不能来”,通过才发真请求。你的 axios 默认 JSON 必触发预检。英文:preflight / OPTIONS。

Q4:三、六个关键 CORS 响应头 —— 怎么理解? —— 怎么理解?

A:像门卫手里的六张牌:Allow-Origin 说”谁可进”、Allow-Methods 说”哪些动作”、Allow-Headers 说”带哪些头”、Allow-Credentials 说”能否带 Cookie”、Max-Age 说”预检缓存多久”、Expose-Headers 说”JS 能读哪些响应头”。缺一浏览器就拦截报错。英文:Access-Control-Allow-*。

Q5:四、经典翻车与怎么配 —— 怎么理解? —— 怎么理解?

A:像两个常踩的坑:带了 Authorization 却没配 Allow-Headers→拦截;想带 Cookie 却用 Allow-Origin:*(配了 Credentials 就不能用 *,必须写具体域名)。配法:FastAPI 用 CORSMiddleware 白名单、Nginx 反代把前后端代理同域从根上消跨域。英文:CORS middleware / reverse proxy。

Q6:核心速记主线有哪些?

  • 本质:CORS 是浏览器策略,请求已发出、服务器已处理,门卫拦响应

  • 两类:简单请求直接发带 Origin;非简单先 OPTIONS 预检再发

  • 六头:Allow-Origin/Methods/Headers/Credentials/Max-Age/Expose-Headers

  • 翻车:缺 Allow-Headers 拦自定义头;* 配 Credentials 必须改具体域

  • 配置:后端白名单中间件,或 Nginx 反代同域从根上消除跨域

口诀

A:CORS 门卫把关严,请求已发响应拦;

简单直进预检问,JSON 必触 OPTIONS 探;

六张头牌缺一张,浏览器前报错瘫;

带 Cookie 莫用星,同域反代最舒坦。

相关链接