CORS 错误是 Web 开发中最令人沮丧的问题之一。你看到控制台中可怕的红色错误消息,API 调用失败,似乎什么都不管用。本指南将帮助你准确理解什么是 CORS、诊断你的具体错误并使用正确的方案修复它。
什么是 CORS?(30 秒解释)
跨源资源共享(CORS)是一种浏览器安全机制,它阻止网页向与提供页面不同的域发出请求。当你的前端 http://localhost:3000 尝试从 http://api.example.com 获取数据时,浏览器会检查 API 服务器是否通过特殊的 HTTP 头显式允许了这个跨源请求。
CORS 工作原理:流程图
┌─────────────────┐ ┌─────────────────────┐
│ Browser │ │ API Server │
│ (Frontend) │ │ (Backend) │
│ │ │ │
│ http://localhost │ ──────> │ https://api.example │
│ :3000 │ fetch │ .com │
│ │ │ │
└─────────────────┘ └─────────────────────┘
│
┌───────────────┴───────────────┐
│ │
CORS Headers No CORS Headers
Present? Present?
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Response sent │ │ Browser BLOCKS │
│ to JavaScript │ │ the response │
└─────────────────┘ └──────────────────┘当 CORS 配置正确时,服务器响应中包含的头会告诉浏览器:"是的,我允许来自这个源的请求。"
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Content-Type: application/json
{"data": "success"}当 CORS 未配置时,浏览器会阻止响应(即使服务器可能已经成功处理了请求)。
HTTP/1.1 200 OK
Content-Type: application/json
# No Access-Control-Allow-Origin header!
{"data": "success"} <-- Browser received this but BLOCKS JavaScript from reading it5 种最常见的 CORS 错误消息
错误 1:缺少 Access-Control-Allow-Origin 头
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.含义:服务器在响应中没有包含 Access-Control-Allow-Origin 头。这是最常见的 CORS 错误,意味着服务器完全没有 CORS 配置。
错误 2:通配符与凭据冲突
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.含义:你正在发送 Cookie 或 Authorization 头(credentials: "include"),但服务器返回了 Access-Control-Allow-Origin: *。当涉及凭据时,服务器必须指定确切的源,不能使用通配符。
错误 3:预检请求失败
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check.含义:在实际请求之前,浏览器发送了一个 OPTIONS 请求(预检)来检查权限。服务器要么没有处理 OPTIONS 方法,要么没有在预检响应中包含 CORS 头。
错误 4:方法不被允许
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.含义:服务器的 Access-Control-Allow-Methods 头不包含你正在使用的 HTTP 方法(如 PUT、DELETE、PATCH)。服务器需要显式允许这些方法。
错误 5:请求头不被允许
Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers in preflight response.含义:你发送了一个自定义头(如 X-Custom-Header 或 Authorization),服务器没有在 Access-Control-Allow-Headers 响应头中显式允许它。
诊断流程图:问题在哪里?
使用此决策树快速识别你的 CORS 问题是前端问题、后端问题还是代理配置问题:
步骤 1:打开浏览器 DevTools > Network 标签。在实际请求之前你是否看到了 OPTIONS 请求?
是 -> 浏览器正在发送预检请求。检查 OPTIONS 响应是否有状态码 200 并包含 CORS 头。如果没有,你的后端没有处理 OPTIONS 请求。
否 -> 这是一个简单请求(GET/POST 配合标准头)。问题是服务器响应缺少 CORS 头。
步骤 2:错误消息是否提到了"preflight"?
是 -> 后端问题。你的服务器需要处理 OPTIONS 请求并返回正确的 CORS 头。
否 -> 检查你是否使用了凭据(Cookie、Authorization 头)。如果是,不能使用通配符 (*)。
步骤 3:直接调用 API(如使用 curl 或 Postman)是否正常工作?
是 -> 确认这是 CORS 问题(仅浏览器)。服务器本身没问题,只是缺少 CORS 头。
否 -> 这不是 CORS 问题。API 本身有问题(认证、路由、服务器错误)。
总结:90% 的 CORS 错误通过在后端服务器上配置正确的头就能解决。
各框架服务端修复方案
Express.js (Node.js)
// Method 1: Using the cors package (recommended)
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Allow specific origins
app.use(cors({
origin: ['http://localhost:3000', 'https://yoursite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
}));
// Method 2: Manual middleware
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
});Django (Python)
# Install django-cors-headers
pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be FIRST
'django.middleware.common.CommonMiddleware',
...
]
# Option 1: Allow specific origins
CORS_ALLOWED_ORIGINS = [
'http://localhost:3000',
'https://yoursite.com',
]
# Option 2: Allow all origins (dev only!)
# CORS_ALLOW_ALL_ORIGINS = True
# Allow credentials (cookies, auth headers)
CORS_ALLOW_CREDENTIALS = True
# Allowed headers
CORS_ALLOW_HEADERS = [
'content-type',
'authorization',
'x-requested-with',
]
# Allowed methods
CORS_ALLOW_METHODS = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS',
]Flask (Python)
# Install flask-cors
pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Option 1: Allow specific origins
CORS(app, origins=['http://localhost:3000', 'https://yoursite.com'],
supports_credentials=True,
allow_headers=['Content-Type', 'Authorization'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
# Option 2: Manual approach
@app.after_request
def after_request(response):
origin = request.headers.get('Origin')
allowed = ['http://localhost:3000', 'https://yoursite.com']
if origin in allowed:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type,Authorization'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return responseGo (net/http)
package main
import (
"net/http"
"strings"
)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
allowedOrigins := map[string]bool{
"http://localhost:3000": true,
"https://yoursite.com": true,
}
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods",
strings.Join([]string{"GET","POST","PUT","DELETE","PATCH"}, ","))
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
}
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handleData)
handler := corsMiddleware(mux)
http.ListenAndServe(":8080", handler)
}Nginx 配置
server {
listen 80;
server_name api.example.com;
# CORS headers for all locations
set $cors_origin "";
if ($http_origin ~* "^https?://(localhost:3000|yoursite\.com)$") {
set $cors_origin $http_origin;
}
location /api/ {
# CORS headers
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
proxy_pass http://backend:8080;
}
}反向代理作为 CORS 中间件
如果你无法修改后端 API(如第三方 API),可以使用反向代理来添加 CORS 头。这是最可靠的生产环境方案。
Nginx 反向代理
# Nginx as CORS proxy for a third-party API
server {
listen 80;
server_name cors-proxy.yoursite.com;
location /proxy/ {
# Add CORS headers
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://yoursite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
# Strip /proxy/ prefix and forward to third-party API
rewrite ^/proxy/(.*) /$1 break;
proxy_pass https://third-party-api.com;
proxy_set_header Host third-party-api.com;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Usage: fetch('https://cors-proxy.yoursite.com/proxy/endpoint')Apache 反向代理
# Apache as CORS proxy
<VirtualHost *:80>
ServerName cors-proxy.yoursite.com
# Enable required modules
# a2enmod proxy proxy_http headers rewrite
<Location "/proxy/">
Header always set Access-Control-Allow-Origin "https://yoursite.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
# Handle preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
# Proxy to backend
ProxyPass "http://backend:8080/"
ProxyPassReverse "http://backend:8080/"
</Location>
</VirtualHost>仅开发环境的解决方案
这些方案仅适用于本地开发,绝不要在生产环境中使用。
Vite 代理
Vite 内置代理将请求从开发服务器转发到 API,完全避免 CORS,因为浏览器只看到同源请求:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
// Proxy /api requests to backend
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// Optional: rewrite path
// rewrite: (path) => path.replace(/^\/api/, ''),
},
// Proxy with WebSocket support
'/ws': {
target: 'ws://localhost:8080',
ws: true,
},
},
},
});
// In your code, use relative URLs:
// fetch('/api/users') instead of fetch('http://localhost:8080/api/users')Webpack devServer 代理
Create React App 和其他基于 webpack 的项目支持类似的代理:
// package.json (Create React App)
{
"proxy": "http://localhost:8080"
}
// Or for more control, create src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
})
);
};
// webpack.config.js (manual setup)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
};Next.js 重写
Next.js 可以通过自身服务器使用 rewrites 代理 API 请求:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*',
},
];
},
};
module.exports = nextConfig;
// In your code:
// fetch('/api/users') -- proxied to http://localhost:8080/api/users常见陷阱和边界情况
陷阱 1:通配符 (*) 与凭据
当前端发送凭据(Cookie、Authorization 头)时,服务器不能使用 Access-Control-Allow-Origin: *,必须回显确切的源:
错误写法(凭据模式下会失败):
// Frontend
fetch('https://api.example.com/data', {
credentials: 'include' // Sending cookies
});
// Backend response header:
Access-Control-Allow-Origin: * // FAILS! Cannot use * with credentials正确写法(动态源配合凭据):
// Backend (Express.js example)
app.use((req, res, next) => {
// Echo back the exact origin
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'https://yoursite.com'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // Exact origin
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});陷阱 2:缺少 OPTIONS 处理器(预检)
非简单请求会触发预检 OPTIONS 请求。如果服务器对 OPTIONS 返回 404 或 405,CORS 在你的实际请求发送之前就会失败。常见的预检触发条件:
| Trigger | Example |
|---|---|
| Custom headers | Authorization, X-Request-ID, X-API-Key |
| Non-simple methods | PUT, DELETE, PATCH |
| Non-simple Content-Type | application/json, application/xml |
| ReadableStream body | Streaming request body |
// Ensure your server handles OPTIONS for all API routes:
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
});陷阱 3:Cookie 与 SameSite
跨源 Cookie 需要三个条件同时满足:
// 1. Frontend: include credentials
fetch('https://api.example.com/data', {
credentials: 'include'
});
// 2. Backend: allow credentials + exact origin
res.setHeader('Access-Control-Allow-Origin', 'https://yoursite.com'); // NOT *
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 3. Cookie must have correct attributes
Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly; Path=/
// IMPORTANT: SameSite=None requires Secure (HTTPS).
// This means cross-origin cookies do NOT work on HTTP (localhost).
// For local dev, use a proxy instead of cross-origin cookies.陷阱 4:缓存预检响应
预检(OPTIONS)请求可能很慢。使用 Access-Control-Max-Age 来缓存它们:
// Server response header:
Access-Control-Max-Age: 86400 // Cache preflight for 24 hours
// Without this header, the browser sends a preflight OPTIONS
// request before EVERY non-simple request, adding latency.
// With caching, the browser remembers the CORS permissions
// and skips the preflight for subsequent requests.注意:Chrome 上限为 7200 秒(2 小时),Firefox 上限为 86400 秒(24 小时)。
陷阱 5:重定向中的 CORS
如果你的 API 端点重定向(301/302)到另一个 URL,CORS 头必须同时存在于原始响应和重定向后的响应中。许多开发者会忽略这一点。尽量避免在 API 端点中使用重定向。
// PROBLEM: API endpoint redirects
// GET https://api.example.com/users -> 301 -> https://api.example.com/v2/users
// CORS headers must be on BOTH responses!
// SOLUTION: Avoid redirects in API endpoints
// Option 1: Update the frontend URL
fetch('https://api.example.com/v2/users'); // Use final URL directly
// Option 2: Make the redirect include CORS headers
// (requires server configuration on both the old and new endpoint)快速参考:CORS 头
| 头名称 | 用途 | 示例值 |
|---|---|---|
| Access-Control-Allow-Origin | Which origins can access the resource | https://yoursite.com or * |
| Access-Control-Allow-Methods | Which HTTP methods are allowed | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Which request headers are allowed | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Whether cookies/auth can be sent | true |
| Access-Control-Max-Age | How long to cache preflight (seconds) | 86400 |
| Access-Control-Expose-Headers | Which response headers JS can read | X-Total-Count, X-Request-ID |
常见问题
可以在浏览器中禁用 CORS 吗?
技术上可以,通过使用 --disable-web-security 参数启动 Chrome,但这非常危险,除了快速调试外不应使用。它会禁用所有浏览器安全策略。开发环境请改用代理(Vite、webpack)。
为什么我的 API 在 Postman 中可以用,在浏览器中不行?
CORS 是仅浏览器的安全机制。Postman、curl 和服务器到服务器的请求不执行 CORS。浏览器检查 CORS 头是因为它运行不受信任的代码(网站的 JavaScript),需要保护用户免受恶意跨源请求的侵害。
CORS 是前端修复还是后端修复?
CORS 几乎总是后端修复。服务器必须发送正确的 Access-Control-Allow-Origin、Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 响应头。前端无法绕过 CORS(设计如此)。唯一的前端变通方案是使用代理使请求看起来是同源的。
生产环境应该用 Access-Control-Allow-Origin: * 吗?
仅适用于不使用 Cookie 或认证的真正公开 API。任何需要凭据的 API 都必须指定确切的源。使用 * 配合凭据总是会失败。对于大多数应用,应根据请求的 Origin 头动态白名单特定源。
什么是预检请求,为什么会发生?
预检请求是浏览器在实际请求之前自动发送的 OPTIONS 请求。当请求使用自定义头、非简单 HTTP 方法(PUT、DELETE、PATCH)或非表单编码/multipart/纯文本的 Content-Type 时会触发。预检检查服务器是否允许实际请求。你必须在服务器上处理 OPTIONS。
如何修复 WebSocket 连接的 CORS?
WebSocket 连接(ws:// 或 wss://)不遵循 CORS 规则。初始 HTTP 升级握手包含 Origin 头,但服务器必须手动验证。大多数 WebSocket 库(Socket.IO、ws)有 origin 选项来白名单允许的源。如果 WebSocket 出现 CORS 错误,问题通常在于初始 HTTP 握手端点,而不是 WebSocket 本身。