生产环境禁用Access-Control-Allow-Origin: *,因其与Access-Control-Allow-Credentials: true冲突会导致浏览器直接拒绝响应;非简单请求需先通过OPTIONS预检,若未正确响应204及必要头(如Allow-Methods、Allow-Headers),浏览器将阻断后续请求;Origin校验须严格标准化(去斜杠、大小写敏感),且必须配合Vary: Origin防止缓存污染;带凭证请求要求Origin精确匹配、Credentials头显式开启、Vary头存在三者缺一不可;反向代理和CDN可能覆盖或缓存CORS头,需逐层验证。

生产环境不能用 header("Access-Control-Allow-Origin: *"),哪怕只加一行也得立刻删掉——它和 Access-Control-Allow-Credentials: true 共存时浏览器直接拒绝响应,且会暴露接口给任意站点调用。
为什么 OPTIONS 预检失败后实际请求根本发不出去
浏览器对非简单请求(比如带 Content-Type: application/json、自定义 X-Auth-Token 头,或用 PUT/DELETE 方法)会先发一次 OPTIONS 请求。如果 PHP 没拦截并返回 200/204,或者返回了但没带 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers,浏览器就认定“这接口不支持跨域”,连后续的 POST 都不发。
常见错误现象:
- 控制台只看到一个
OPTIONS请求,状态码是 404 或 500,没有后续请求 - Network 面板里
OPTIONS响应头缺失Access-Control-Allow-Methods - Apache 下用了
mod_rewrite却没放行OPTIONS,被重写规则吞掉了
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 在所有跨域逻辑最开头加判断:
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } - 确保这个判断在任何
echo、print、HTML 输出或框架自动输出之前执行 - 不要只写
exit,用http_response_code(204)明确告诉浏览器这是成功预检响应
白名单匹配必须严格校验 Origin 字符串
$_SERVER['HTTP_ORIGIN'] 是浏览器传来的原始源,但它可能为空(比如从本地文件打开 HTML)、被篡改(不可信),或带末尾斜杠/大小写差异。直接 in_array($origin, $whitelist) 很容易漏判或误判。
使用场景:
- 前端部署在
https://admin.example.com,但用户从https://ADMIN.EXAMPLE.COM访问(大小写不同) - 后端收到的
Origin是https://admin.example.com/(结尾有斜杠) - 某些代理或调试工具会伪造或清空
Origin头
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 先做基础防御:
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (!$origin) { header('Access-Control-Allow-Origin: https://fallback.example.com'); exit; } - 标准化比对:用
rtrim($origin, '/')去掉末尾斜杠,再统一转小写或保持原样(推荐保持原样,因协议和域名区分大小写) - 白名单数组里每个域名都写成完整协议+域名+端口(如
https://app.example.org:8080),避免模糊匹配 - 务必加
header('Vary: Origin'),否则 CDN 或反向代理可能缓存错响应
带 Cookie 的请求必须同时满足三个条件
只要前端 fetch 设置了 credentials: 'include' 或 jQuery 的 xhrFields: { withCredentials: true },后端就必须同步满足三件事,缺一不可:
-
Access-Control-Allow-Origin不能是*,必须是具体域名(如https://web.example.com) -
Access-Control-Allow-Credentials: true必须显式设置 - 响应头里还得有
Vary: Origin,否则中间缓存可能把带凭证的响应错发给其他源
参数差异:
- 开发时用
http://localhost:3000测试,生产就得换成正式域名,不能混用 - 如果前后端同域(比如都走
https://api.example.com),根本不需要 CORS,强行加反而引发问题 -
Access-Control-Allow-Credentials: true一旦开启,所有跨域响应都必须带该头,不能只在登录成功时加
性能影响:
- 每次请求都要读取
Origin并比对白名单,开销极小,但若白名单数组过大(上百个),可用哈希表(array_flip())加速查找 - 不设
Access-Control-Max-Age会导致浏览器反复发OPTIONS,建议设为86400(24 小时)
框架里优先用中间件而非手动 header()
ThinkPHP 的 ->allowCrossDomain()、Laravel 的 spatie/laravel-cors、Symfony 的 NelmioCorsBundle 都不是“语法糖”,它们天然解决两个关键问题:一是确保响应头在框架最终输出前注入,二是自动拦截并响应 OPTIONS,且能按路由粒度开关。
容易踩的坑:
- 在 ThinkPHP 控制器里用
response()->header(),但没注意该方法只对当前响应生效,无法覆盖框架全局响应流程 - Laravel 中装了
laravel-cors却没在config/cors.php里配paths,导致 API 路由没被覆盖 - 自定义中间件放在
middleware.php数组末尾,结果被其他中间件(如日志、鉴权)覆盖了响应头
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- ThinkPHP 6+ 直接在路由定义末尾链式调用:
Route::post('login', 'LoginController@login')->allowCrossDomain(['https://web.example.com'], true); - Laravel 中确认
App\Http\Kernel的$middlewareGroups['api']里已包含\Spatie\Cors\Cors::class - 所有框架方案上线前,务必用 curl 检查真实响应头:
curl -I -X OPTIONS -H "Origin: https://web.example.com" https://api.example.com/login
最常被忽略的是缓存与代理层——Nginx、CDN、甚至公司内部网关,都可能缓存一个没带 CORS 头的响应,或者把 Origin 头过滤掉了。上线后遇到“本地测得好好的,线上不行”,第一反应不该是改 PHP 代码,而是查 Nginx access log 里有没有 OPTIONS 请求,再抓包看响应头到底从哪一层出来的。



















