CORS跨域问题深度解析:从原理到实战

2026-07-13 13 min read 6

线上故障:一个跨域请求引发的连锁反应

2024年3月,我负责的电商平台上线新版本后,用户反馈商品详情页无法加载。排查发现,前端Vue应用请求后端API时,浏览器控制台报错:

Access to XMLHttpRequest at 'https://api.example.com/products' from origin 'https://www.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

这个错误导致所有跨域请求失败,用户无法查看商品、加入购物车、下单。线上故障持续了45分钟,影响用户超过10万。事后复盘,根本原因是后端新部署的API服务没有配置CORS头。

这次事故让我决定彻底搞懂CORS,并整理出一套可复用的解决方案。本文会从原理讲起,对比三种主流方案,给出完整代码和压测数据,最后分享我踩过的坑。

CORS原理:浏览器为什么拦截跨域请求?

CORS(Cross-Origin Resource Sharing)是浏览器的一种安全机制。当网页请求不同源(协议、域名、端口任一不同)的资源时,浏览器会先发送一个OPTIONS预检请求(Preflight Request),检查服务器是否允许跨域。

预检请求包含以下关键头:

  • Origin:请求来源
  • Access-Control-Request-Method:实际请求方法
  • Access-Control-Request-Headers:实际请求的自定义头

服务器需要返回以下头来允许跨域:

  • Access-Control-Allow-Origin:允许的来源
  • Access-Control-Allow-Methods:允许的方法
  • Access-Control-Allow-Headers:允许的头
  • Access-Control-Max-Age:预检结果缓存时间

如果服务器没有正确返回这些头,浏览器就会拦截请求。注意:拦截的是响应,不是请求。请求已经发送到服务器,但浏览器不把响应交给JavaScript。

三种方案对比

我测试了三种常见方案:Nginx反向代理、后端中间件、前端代理。测试环境:

  • 前端:Vue 3.4.21,运行在http://localhost:8080
  • 后端:Laravel 11.0,运行在http://api.example.com:8000
  • Nginx:1.24.0
  • 压测工具:k6 0.49.0

压测场景:模拟100个并发用户,持续30秒,请求一个需要预检的POST接口(带自定义头Authorization)。

方案平均响应时间P99响应时间错误率配置复杂度
Nginx反向代理45ms120ms0%
后端中间件52ms150ms0%
前端代理68ms200ms0.5%

结论:Nginx反向代理性能最好,配置最简单。后端中间件适合需要动态控制CORS的场景。前端代理只适合开发环境。

方案一:Nginx反向代理(推荐)

原理:用Nginx统一处理跨域请求,后端服务不需要关心CORS。Nginx在返回响应时添加CORS头。

完整配置:

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

    location / {
        # 允许的来源,生产环境建议写具体域名
        add_header Access-Control-Allow-Origin 'https://www.example.com' always;
        # 允许的HTTP方法
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
        # 允许的自定义头
        add_header Access-Control-Allow-Headers 'DNT, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Range, Authorization' always;
        # 允许携带凭证(Cookie)
        add_header Access-Control-Allow-Credentials 'true' always;
        # 预检结果缓存1小时
        add_header Access-Control-Max-Age '3600' always;

        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin 'https://www.example.com' always;
            add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header Access-Control-Allow-Headers 'DNT, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Range, Authorization' always;
            add_header Access-Control-Allow-Credentials 'true' always;
            add_header Access-Control-Max-Age '3600' always;
            add_header Content-Type 'text/plain charset=UTF-8';
            add_header Content-Length 0;
            return 204;
        }

        # 代理到后端服务
        proxy_pass http://backend:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

注意:add_header后面的always参数确保即使请求失败也返回CORS头。if块处理OPTIONS请求,直接返回204,不转发到后端。

方案二:后端中间件(Laravel示例)

原理:在后端框架中编写中间件,为每个响应添加CORS头。适合需要动态控制CORS的场景(比如根据用户角色允许不同来源)。

Laravel 11中间件代码:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CorsMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $origin = $request->header('Origin');
        $allowedOrigins = [
            'https://www.example.com',
            'https://admin.example.com',
        ];

        if (in_array($origin, $allowedOrigins)) {
            $response->headers->set('Access-Control-Allow-Origin', $origin);
            $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
            $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
            $response->headers->set('Access-Control-Allow-Credentials', 'true');
            $response->headers->set('Access-Control-Max-Age', '3600');
        }

        // 处理预检请求
        if ($request->isMethod('OPTIONS')) {
            return response('', 204);
        }

        return $response;
    }
}

在bootstrap/app.php中注册中间件:

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(\App\Http\Middleware\CorsMiddleware::class);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

性能对比:Nginx方案比Laravel中间件方案快约15%,因为Nginx处理OPTIONS请求不经过PHP-FPM。

方案三:前端代理(开发环境)

原理:在Vue开发服务器中配置代理,把跨域请求转发到后端。只适合开发环境,生产环境必须用Nginx或后端中间件。

Vite配置:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://api.example.com:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

前端请求时用相对路径:

// 前端代码
fetch('/api/products')
  .then(response => response.json())
  .then(data => console.log(data))

注意:changeOrigin必须设为true,否则后端收到的Origin还是前端地址。

避坑指南

我踩过的坑,列出来供参考:

  1. 预检请求缓存时间设置不当:Access-Control-Max-Age设得太短(比如60秒),导致每次请求都发OPTIONS,增加延迟。建议设为3600秒(1小时)。
  2. 多个Origin需要动态处理:如果前端有多个域名(比如主站和后台),不能写死一个Origin。可以用正则匹配或白名单列表。
  3. 带Cookie的请求必须设置withCredentials:前端fetch需要加credentials: 'include',后端必须返回Access-Control-Allow-Credentials: true,且Access-Control-Allow-Origin不能是*。
  4. Nginx配置中add_header顺序:如果location块中有多个add_header,后面的会覆盖前面的。建议把所有CORS头写在一个location块中。
  5. OPTIONS请求返回204而不是200:204 No Content表示请求成功但没有内容,浏览器不会尝试解析响应体。返回200可能会被某些浏览器拦截。
  6. 后端中间件性能问题:如果中间件中做了复杂逻辑(比如数据库查询),会拖慢所有请求。建议把CORS处理放在最外层。

总结

CORS跨域问题本质是浏览器的安全策略。解决方案很多,但生产环境推荐Nginx反向代理,性能好、配置简单。如果后端需要动态控制,用中间件。前端代理只用于开发。

最后,建议在项目初期就配置好CORS,避免线上故障。如果已经出了问题,按照本文的步骤排查:先看浏览器控制台错误信息,再看响应头是否包含Access-Control-Allow-Origin,最后检查预检请求是否正常。

IT搬运工

专注技术分享,坚持原创深度内容。

分类
链接
订阅

RSS 订阅关注最新文章

© 2026 IT搬运工 · 站点地图 · 鄂ICP备19007640号-3