DevToolBox免费
博客广告合作

CORS 跨域错误完全解决指南

12 分钟阅读作者 DevToolBox

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 it

5 种最常见的 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 response

Go (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 在你的实际请求发送之前就会失败。常见的预检触发条件:

TriggerExample
Custom headersAuthorization, X-Request-ID, X-API-Key
Non-simple methodsPUT, DELETE, PATCH
Non-simple Content-Typeapplication/json, application/xml
ReadableStream bodyStreaming 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-OriginWhich origins can access the resourcehttps://yoursite.com or *
Access-Control-Allow-MethodsWhich HTTP methods are allowedGET, POST, PUT, DELETE
Access-Control-Allow-HeadersWhich request headers are allowedContent-Type, Authorization
Access-Control-Allow-CredentialsWhether cookies/auth can be senttrue
Access-Control-Max-AgeHow long to cache preflight (seconds)86400
Access-Control-Expose-HeadersWhich response headers JS can readX-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 本身。

这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

合作推荐

赞助这篇文章

把你的产品放到这个开发者主题旁边,并追踪点击效果。

咨询文章赞助

本站使用 Cookie 进行流量分析与广告展示。继续浏览即视为同意。 隐私政策