拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

如何在 Vue + PHP 项目中正确解决本地开发跨域(CORS)问题

如何在 Vue + PHP 项目中正确解决本地开发跨域(CORS)问题

前言

本地开发时最常见的画面是:Vue 跑在http://localhost:5173,PHP 接口跑在http://127.0.0.1:8000,浏览器控制台一片红字——has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present。于是有人在 PHP 里加了一行header('Access-Control-Allow-Origin: *'),页面通了,但带上 Cookie 之后又不行了;或者接口一旦需要Authorization头,预检请求(preflight)直接 405。

跨域问题的麻烦之处在于它不是后端"坏了",而是浏览器按三个条件同时判断:来源是否被允许、是否允许携带凭据、预检请求是否被正确应答。缺任何一个条件,请求在浏览器层就被丢掉,PHP 那边的日志里可能连一条访问记录都没有。

本文先讲清浏览器到底在拦什么,再给出 PHP 端可以照抄的响应头写法(含预检提前返回),然后说明携带 Cookie 时的额外条件,最后给出本地开发更省事的方案:Vite 代理。示例的最低 PHP 版本是 8.0。

一、浏览器在拦什么:简单请求与预检请求

浏览器把跨域请求分成两类。判断依据是方法 + 请求头 + Content-Type:

条件简单请求(simple request)触发预检
方法GET/HEAD/POST其余方法(PUT/DELETE/PATCH…)
Content-Typeapplication/x-www-form-urlencoded、multipart/form-data、text/plainapplication/json、text/xml等
自定义头无带Authorization、X-Requested-With、自定义头
额外动作直接发出,读响应时校验 CORS 头先发一个OPTIONS预检

也就是说:用 axios/fetch 提交 JSON,一定会触发预检。预检请求会带上Access-Control-Request-Method和Access-Control-Request-Headers,服务器必须用Access-Control-Allow-Methods/Access-Control-Allow-Headers明确"答上"这些值,浏览器才肯发真正的请求。

一个容易被忽略的事实是:请求其实已经发到服务器了(简单请求尤其如此),只是响应被浏览器拦下不给 JS 读取。所以后端日志里能看到请求,前端却拿不到数据——这常常误导排查方向。

二、PHP 端正确写法:白名单 + 预检提前返回

先明确原则:不要回显任意 Origin,也不要用*。维护一个白名单,命中才回显,并且加上Vary: Origin,避免缓存层把 A 站点的响应发给 B 站点。

<?php // 最低版本:PHP 8.0 declare(strict_types=1); const ALLOWED_ORIGINS = [ 'http://localhost:5173', 'http://127.0.0.1:5173', ]; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if ($origin !== '' && in_array($origin, ALLOWED_ORIGINS, true)) { header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('Access-Control-Max-Age: 600'); } // 预检请求到此为止:不要继续执行业务逻辑 if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') { http_response_code(204); exit; }

三个细节必须注意:


  • Access-Control-Allow-Origin只能出现一次。如果 nginx 也加了同样的头,浏览器会报The 'Access-Control-Allow-Origin' header contains multiple values,两个头同时存在等于没配。只在一处加。

  • 预检必须提前exit。如果OPTIONS请求继续往下走业务逻辑,未登录时就会返回 401,浏览器把预检判为失败,真正的请求根本不会发出。

  • Access-Control-Allow-Headers要覆盖前端真实发送的所有头。后端漏掉Authorization,前端带 token 就直接失败。


三、携带 Cookie 或凭据时的三个额外条件

只要请求涉及 Cookie、Authorization头或fetch的credentials: 'include',浏览器会切换到"凭据模式",规则会变得更严:


  1. Access-Control-Allow-Origin不能是*,必须是具体来源,且要与当前页面来源完全一致(协议、域名、端口一个都不能差,http与https、localhost与127.0.0.1都算不同来源);

  2. 服务器必须返回Access-Control-Allow-Credentials: true;

  3. 前端必须显式开启:fetch用credentials: 'include',axios 用withCredentials: true(或在默认配置里统一打开)。


Cookie 本身还受SameSite与Secure约束。SameSite=Lax的 Cookie 不会随跨站请求发送;如果前后端是不同站点且确实需要携带 Cookie,需要SameSite=None; Secure,而Secure又要求 HTTPS。这也是为什么"本地 HTTP 环境下带 Cookie 的跨域"经常怎么配都不通——最省事的做法还是下面要说的代理方案。

四、本地开发更推荐的做法:用 Vite 开发代理

跨域限制只在浏览器里存在。开发阶段完全可以让浏览器只跟 Vite 开发服务器打交道,由它把/api请求转发给 PHP——对浏览器来说这是同源请求,也就根本不存在 CORS。

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, }, }, }, })

前端代码里把请求地址改成相对路径即可:

// 开发环境走 Vite 代理,生产环境由 Nginx 反代到 PHP-FPM const api = axios.create({ baseURL: '/api', withCredentials: true }) const { data } = await api.get('/products')

后端可以用 PHP 内置服务器起一个最简环境:

php -S 127.0.0.1:8000 -t public

代理方案的额外好处是:Cookie 的域、SameSite行为与生产环境一致,不用为了开发环境放宽安全设置。生产环境同样推荐"同域 + 反向代理",让 CORS 配置彻底消失,只在确实需要开放给第三方站点时才配置白名单。

常见坑点

1.*和凭据同时出现

❌header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Credentials: true');——浏览器直接拒绝,报cannot be used with credentials,且不管你怎么调前端都不通。 ✅ 回显白名单里命中的具体 Origin。

2. 预检没有提前返回

❌OPTIONS请求继续走鉴权中间件,未登录返回 401/403——浏览器认定预检失败,真正的POST压根没发出去,排查看起来像"后端没收到请求"。 ✅if ($method === 'OPTIONS') { http_response_code(204); exit; }放在所有鉴权逻辑之前。

3. nginx 与 PHP 各加一份 CORS 头

❌add_header Access-Control-Allow-Origin ...;写在 nginx 配置里,PHP 里又加一次——响应出现两个同名头,浏览器报contains multiple values。 ✅ 只在一个地方加;如果历史配置改不掉,可以在 PHP 里先header_remove('Access-Control-Allow-Origin')再自己设置。

4. 白名单用"包含"判断

❌if (str_contains($origin, 'localhost'))放行——http://evil-localhost.attacker.com也能过。 ✅ 用in_array($origin, ALLOWED_ORIGINS, true)精确匹配,比较时区分协议与端口。

5.Allow-Headers漏项

❌ 只写了Content-Type,前端却发了Authorization或X-Requested-With——预检响应与请求头对不上,报Request header field authorization is not allowed。 ✅ 把前端所有自定义头列全;也可以调试期先回显Access-Control-Request-Headers的值,稳定后再收紧成白名单。

6.header()之前已有输出

❌ 被include的文件开头有 BOM 或空行、调试时先echo了一句——header()报headers already sent,CORS 头一个都没生效,看起来就像"配了没用"。 ✅ 用无 BOM 的 UTF-8 保存,<?php前不留任何字符,出口文件结尾不写?>,必要时在入口ob_start()兜底。

7. 把 CORS 当成安全机制

❌ 认为"加了白名单就没人能调我的接口"——CORS 只是浏览器的策略,curl、Postman、服务端脚本完全不受影响。 ✅ 该做的鉴权、限流、CSRF 防护一个都不能少,CORS 只是让浏览器愿意把响应交给你的前端代码。

总结

场景推荐做法关键点
本地开发(推荐)Viteserver.proxy转发/api浏览器视角同源,无 CORS
必须跨域白名单回显 Origin不能用*,加Vary: Origin
带 Cookie具体 Origin +Allow-Credentials: true前端还要withCredentials
预检请求提前204返回不要走到鉴权逻辑
生产环境同域反向代理让 CORS 配置彻底消失

跨域问题的本质是"三件事必须同时成立":来源被允许、凭据被允许、预检被正确应答。开发阶段用 Vite 代理可以把这三件事一起绕开;必须跨域时,就按白名单回显、明确列出方法与会话头、预检提前返回这三条来做,同时记住 CORS 从来不是安全边界。

返回列表