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 本身。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

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

无垃圾邮件,随时退订。

试试这些相关工具

🛡️CSP Header GeneratorNXNginx Config Generator.ht.htaccess Generator4xxHTTP Status Code Reference

相关文章

REST API 最佳实践:2026 完整指南

学习 REST API 设计最佳实践,包括命名规范、错误处理、认证、分页、版本控制和安全头。

内容安全策略 (CSP) 完全指南:从基础到生产部署

从零学习 CSP:所有指令、常见配置、报告机制和分步部署指南。