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(Cross-Origin Resource Sharing)是浏览器的一种安全机制。当网页请求不同源(协议、域名、端口任一不同)的资源时,浏览器会先发送一个OPTIONS预检请求(Preflight Request),检查服务器是否允许跨域。
预检请求包含以下关键头:
服务器需要返回以下头来允许跨域:
如果服务器没有正确返回这些头,浏览器就会拦截请求。注意:拦截的是响应,不是请求。请求已经发送到服务器,但浏览器不把响应交给JavaScript。
我测试了三种常见方案:Nginx反向代理、后端中间件、前端代理。测试环境:
压测场景:模拟100个并发用户,持续30秒,请求一个需要预检的POST接口(带自定义头Authorization)。
| 方案 | 平均响应时间 | P99响应时间 | 错误率 | 配置复杂度 |
|---|---|---|---|---|
| Nginx反向代理 | 45ms | 120ms | 0% | 低 |
| 后端中间件 | 52ms | 150ms | 0% | 中 |
| 前端代理 | 68ms | 200ms | 0.5% | 高 |
结论:Nginx反向代理性能最好,配置最简单。后端中间件适合需要动态控制CORS的场景。前端代理只适合开发环境。
原理:用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,不转发到后端。
原理:在后端框架中编写中间件,为每个响应添加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还是前端地址。
我踩过的坑,列出来供参考:
CORS跨域问题本质是浏览器的安全策略。解决方案很多,但生产环境推荐Nginx反向代理,性能好、配置简单。如果后端需要动态控制,用中间件。前端代理只用于开发。
最后,建议在项目初期就配置好CORS,避免线上故障。如果已经出了问题,按照本文的步骤排查:先看浏览器控制台错误信息,再看响应头是否包含Access-Control-Allow-Origin,最后检查预检请求是否正常。
专注技术分享与实战