2026/9/24 20:41:24

Vue+ThinkPHP跨域请求配置全攻略:从开发代理到Nginx反向代理

Vue+ThinkPHP跨域请求配置全攻略:从开发代理到Nginx反向代理 前后端分离项目做到一半十有八九会被拦在跨域这道坎上。尤其是Vue做前端、ThinkPHP做后端的组合本地开发时Vue跑在8080端口ThinkPHP跑在8000端口端口不一样浏览器直接给你来一句CORS error接口明明能用页面里就是调不通。这篇就把vue thinkphp跨域请求配置这件事彻底讲明白从开发环境的代理转发到后端的CORS中间件再到生产环境的nginx反代每一步都给出可直接复制的配置和当时的踩坑记录。1. 跨域问题的真正根源同源策略与前后端分离的冲突1.1 同源策略到底拦的是什么浏览器有一个安全机制叫同源策略通俗点说就是页面里发出去的异步请求目标地址的协议、域名、端口必须和当前页面的完全一致三者只要有一个不同浏览器就会认为这是跨域请求默认不让你拿到响应。这里有个很关键的细节同源策略拦截的不是请求的发送而是响应的接收。也就是说你的请求到达了ThinkPHP后端后端也正常处理完返回了数据但浏览器看到响应头里没有允许跨域的字段就会把响应藏起来然后在控制台报一个CORS错误。很多新手在这里绕圈子以为后端没接到请求实际是后端接了也白接返回的东西被浏览器扣下了。生活里可以类比成小区门卫你住A栋快递员送到B栋门卫一看不是本栋楼的访客直接拦下不让进。快递本身是送到了但你的手就是收不到包裹。1.2 前后端分离架构为什么必然撞上这堵墙Vue开发模式下项目是跑在webpack-dev-server上的默认地址是http://localhost:8080ThinkPHP如果用的是内置服务器或PHPStudy地址通常是http://localhost:8000。两个服务都开在同一台机器上但端口不同这就触发了同源策略里的端口不一致条件跨域必然发生。就算你把后端端口改成80协议和域名相同只要前端源码里调接口时写的是http://localhost:8000/api/user或者前端页面和后端接口不在同一个域名下一样会出现跨域。前后端分离本身就是让前端资源和后端接口分居两地所以这个坑不是某个人配置错了而是架构带来的必然结果只不过配置得当才能绕过去。1.3 浏览器报错信息的读法常见的报错长这样Access to XMLHttpRequest at http://localhost:8000/api/user from origin http://localhost:8080 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.这句话信息量很大拆开看from origin http://localhost:8080说明当前页面的源at http://localhost:8000/api/user说明请求的目标No Access-Control-Allow-Origin header is present说明后端响应里没有携带跨域允许头。所以看到这种报错方向就很明确要么在后端加上CORS响应头要么让前端通过代理让请求变成同源。接下来的章节就是这两条路的具体走法。2. Vue开发环境的破局点devServer proxy代理配置2.1 代理为什么能绕开跨域在开发模式下最高效的跨域解决方案不是去后端开CORS而是用Vue CLI提供的devServer代理。原理说起来不复杂浏览器的请求目标是http://localhost:8080/api/xxx这个请求是同源的浏览器放行然后webpack-dev-server在node层收到这个请求再以服务端身份转发给http://localhost:8000/xxx。服务端到服务端的请求不受浏览器同源策略约束所以后端正常返回node层把响应再传回给浏览器。一句话总结让浏览器只访问同源地址跨域的部分由后端代理去做。这样前端代码里不需要任何跨域配置体验上和前后端同域部署完全一致。2.2 vue.config.js里的proxy配置大多数Vue 2 Vue CLI项目找到项目根目录的vue.config.js写入下面这段const { defineConfig } require(vue/cli-service) module.exports defineConfig({ devServer: { port: 8080, proxy: { /api: { target: http://localhost:8000, changeOrigin: true, pathRewrite: { ^/api: } } } } })逐个字段说明需要注意的点/api是路径匹配前缀前端所有以/api开头的请求都会走代理target是后端ThinkPHP的地址按实际端口来填changeOrigin建议设为true它会把请求头中的Host改写成target的域名避免后端有一些基于Host的安全校验时误伤代理请求pathRewrite是路径重写规则。如果后端路由本身就带/api前缀比如路由定义是Route::get(api/user)那这段可以删除如果后端路由是/user就必须保留这段把/api前缀剥掉再转发。配置改完后要重启devServer只改配置文件不重启很多时候不会生效。2.3 axios的baseURL怎么配合代理配好之后前端请求代码也要配合调整。很多人代理配了但接口还是跨域原因就是axios的baseURL写死了后端地址// 错误的写法绕过代理直连后端 axios.defaults.baseURL http://localhost:8000 // 正确的写法走代理路径 axios.defaults.baseURL /api写成/api之后项目里调接口时写成axios.get(/user)实际请求地址会被拼成/api/user正好命中代理规则。这个细节是代理配置能否生效的关键我见过太多人栽在这一步。2.4 开发环境代理的边界要清醒一点devServer的代理只在本地开发时有效。npm run build构建出来的是纯静态文件丢到Nginx以后没有webpack-dev-server这个中转层/api路径就会变成404或者被Nginx当作静态资源处理。所以开发环境代理只能解决开发阶段的问题生产环境还要额外处理这一块在第4章展开。3. ThinkPHP后端的CORS中间件配置两种可靠做法前端的proxy可以解决开发环境但有两个场景必须让后端本身支持CORS一是生产环境某些接口需要被第三方系统直接调用二是联调阶段别人不用代理直接访问你的后端。所以就算前端代理配好后端CORS中间件也应该配上属于我可以不用但你不能没有的兜底方案。3.1 ThinkPHP 6的中间件写法ThinkPHP 6引入了比较规范的中间件机制处理CORS非常顺手。在app\middleware目录下新建CorsMiddleware.php?php declare(strict_types1); namespace app\middleware; use Closure; use think\Request; use think\Response; class CorsMiddleware { public function handle(Request $request, Closure $next) { // 动态获取前端Origin比固定写死更灵活 $origin $request-header(origin, *); $header [ Access-Control-Allow-Origin $origin, Access-Control-Allow-Methods GET, POST, PUT, DELETE, PATCH, OPTIONS, Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With, Accept, Origin, Access-Control-Allow-Credentials true, Access-Control-Max-Age 86400, ]; // 预检请求直接返回204不再往后走 if ($request-method() OPTIONS) { return Response::create(, json, 204)-header($header); } $response $next($request); $response-header($header); return $response; } }这段代码逻辑很清晰普通请求正常处理完后给响应加上CORS头OPTIONS预检请求直接返回204不进入业务层。注册中间件有两种方式。全局注册在app/middleware.phpreturn [ \app\middleware\CorsMiddleware::class ];只注册到路由组的话在route/app.php或具体的路由文件里Route::group(api, function () { // 路由定义 })-middleware(\app\middleware\CorsMiddleware::class);全局注册更省心建议直接全局。3.2 ThinkPHP 5或老项目的header写法ThinkPHP 5及更早版本没有这么规范的中间件但思路是一样的在最外层把响应头加上就行。比较实用的做法是在公共入口文件public/index.php的require之前加上header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With); if (strtoupper($_SERVER[REQUEST_METHOD]) OPTIONS) { exit; }TP5.1以上的版本也支持中间件可以按3.1的模式写只是类名和命名空间需要适配TP5的规范。老项目如果改动成本高直接在入口文件加头是最低成本的兼容方案。3.3 CORS响应头的含义逐个说明配置写完了最好还是把每个头的含义弄明白不然出问题都不知道往哪里看。Access-Control-Allow-Origin允许哪些源跨域访问*表示任意源。实际项目里建议动态读取请求的Origin再回填比直接写*更灵活也为后面配合Cookie留了余地Access-Control-Allow-Methods允许的HTTP方法用PUT、DELETE或自定义方法时这里必须写上Access-Control-Allow-Headers允许的自定义请求头。前端如果带了Authorization做token鉴权这个头必须出现在列表里否则预检直接失败Access-Control-Allow-Credentials允许携带Cookie等凭据值为true。注意它和Allow-Origin: *不能同时使用Access-Control-Max-Age预检请求结果的缓存时间单位秒。设成一天浏览器在这段时间内就不需要重复发OPTIONS了能省不少无效请求。3.4 关于OPTIONS预检请求在3.1的代码里OPTIONS请求单独处理返回204这一步不是可有可无的细节而是整个CORS配置的核心。当请求体是application/json、携带自定义头、使用PUT/DELETE等方法时浏览器会先发一个OPTIONS请求探测服务器是否允许跨域这叫预检。如果后端没有对OPTIONS做处理而是把它当成普通请求进了业务逻辑返回的状态码如果不是2xx浏览器会直接判定预检失败真正的请求根本不会发出去。前端看到的报错往往是Access to XMLHttpRequest at ... from origin ... has been blocked by CORS policy: Response to preflight request doesnt pass access control check所以后端中间件里对OPTIONS单独返回204就是在给预检放行。4. 生产环境的跨域处理nginx反向代理与服务端CORS兜底4.1 为什么生产环境优先选反向代理第2章说过devServer proxy只活在开发模式生产环境需要另一个同思路的方案在Nginx层做反向代理。把Vue构建后的dist目录放到Nginx静态目录再把/api路径代理到ThinkPHP服务这样用户访问https://yourdomain.com/api/user实际上请求被Nginx转发到了http://127.0.0.1:8000/user。浏览器眼里自己全程访问的是同一个域名同一个端口压根不触发跨域策略。这个方案相比后端CORS的最大优势是前端代码和后端接口在浏览器看来完全同源不用操心Cookie、Header、预检这些CORS的边边角角而且还能顺手把后端服务端口隐藏掉少暴露一个攻击面。4.2 一份可以抄的nginx配置假设前端文件在/var/www/html/distThinkPHP后端监听127.0.0.1:8000server { listen 80; server_name yourdomain.com; # Vue前端静态资源 location / { root /var/www/html/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1: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; } }try_files $uri $uri/ /index.html;这一行是Vue Router用history模式时必须有的配置否则刷新/login这类前端路由页面Nginx会去磁盘找login.html找到不就直接404了。proxy_pass http://127.0.0.1:8000/;末尾的/很关键带/表示把匹配路径前缀的/api部分剥掉再转发。也就是/api/user转成/user和开发环境pathRewrite的效果一致。如果后端路由本身带/api把末尾的/去掉即可。配置改完记得nginx -t检查语法然后nginx -s reload重载。4.3 如果后端CORS必须先顶着怎么办有些场景下没有办法立刻上Nginx或者后端接口还要被第三方系统、移动端App直接调用那服务端CORS就得顶上去。生产环境的CORS跟本地联调有几点不一样要注意第一Access-Control-Allow-Origin别用*。生产环境如果接口涉及登录态、用户数据建议维护一个允许的域名白名单动态判断后把具体的Origin回填到响应头。用*意味着任意网站都能跨域调你的接口配合Cookie鉴权时浏览器还会直接因为安全性拒绝。第二如果要带CookieAllow-Credentials: true和具体的Allow-Origin必须成对出现。浏览器要求Allow-Origin不能是*否则就算前端配了withCredentials: true请求照样被拦截。第三Max-Age在生产环境可以设得大一点比如86400秒减少预检请求的频次接口响应会显得更快一些。4.4 同域部署的终极方案顺着这个思路往下走很多成熟项目到最后干脆选择同域部署前端构建后的文件和后端入口放在同一个域下面由Nginx统一分发。浏览器视角里根本不存在跨域这个东西前端代码里也不需要写代理相关的逻辑。这算是投入产出比最高的方案但需要运维和部署上配合适合团队里能hold住Nginx的情况。5. 实战中绕不开的坑OPTIONS预检、Session失联与调试技巧5.1 简单请求与非简单请求的划分CORS有一种简单请求的说法理解了它你就知道为什么有的接口没做跨域配置也能通有的就报错。简单请求需要同时满足几个条件方法限GET、POST、HEAD请求头只能有Accept、Content-Type等几个常规字段而且Content-Type只能是application/x-www-form-urlencoded、multipart/form-data或text/plain。一旦你的请求用了application/json这是axios的默认行为或者带了Authorization头浏览器就会把它判定为非简单请求先发一个OPTIONS预检。这也是为什么很多人发现GET请求后端不开跨域也能通但POST提交JSON就是报错——GET通常碰巧是简单请求POST JSON必然触发预检。5.2 预检通过后的真正请求以及后端容易忽视的点预检通过后浏览器会发出真正的请求这个请求同样需要后端响应携带CORS头。所以后端中间件一定要保证预检请求返回204带CORS头普通请求返回200带CORS头缺一个都会出问题。实操中我见过一个比较隐蔽的问题后端在OPTIONS请求时直接exit了导致没有输出CORS头预检失败或者有些老版本TP框架的异常处理会拦截OPTIONS返回一个HTML错误页状态码200但内容不对浏览器同样判预检失败。排查的时候先看Network面板里有没有OPTIONS请求、它的状态码和响应头是什么往往一眼就能定位问题。5.3 前端带Session或Cookie的跨域配置如果你要在Vue和ThinkPHP之间保持登录态跨越开发环境和跨域两种模式事情的复杂度会提升一档。后端CORS模式下前端axios要允许携带凭据axios.defaults.withCredentials true后端对应要求是Access-Control-Allow-Credentials: true而且不能用Allow-Origin: *。前端跨域模式下withCredentials: true会把当前域的Cookie带上但目标后端如果要读PHPSESSID还需要确认Set-Cookie的SameSite属性不会拦截。有些浏览器对跨域Cookie策略收紧得很厉害这个场景下我的建议是生产环境老老实实走Nginx反向代理不要跟浏览器Cookie策略硬刚。开发环境因为走的是devServer代理浏览器视角始终是localhost:8080Cookie也不存在跨域问题反而最省心。5.4 一个请求401的完整排查链路记录一个真实的排查过程方便以后照着走。现象Vue前端登录接口正常登录后调用用户信息接口Network面板显示Request failed with status code 401后端日志显示每个接口都在报未授权。排查第一步先看后端有没有收到请求。后端LOG显示收到了请求说明不是网络链路问题。第二步看后端为什么未授权。ThinkPHP常见的权限判断是读取Authorization头里的token但打印出来发现token为空。第三步回头看前端axios请求拦截器。代码里确实设置了config.headers.Authorization Bearer token那这个头为什么后端读不到这时候去Network面板看请求的Headers发现Authorization这一项根本不存在。第四步考虑到平台的问题去翻预检请求。页面里其实先发出了一个OPTIONS请求响应403真正的GET请求压根没发出去。第五步定位到根因后端CORS头里的Access-Control-Allow-Headers没有包含Authorization预检时浏览器发现服务器不允许这个头直接拦下。后端把这个字段补上问题立刻消失。这个链路很有代表性也说明了一个规律遇到跨域相关的报错先看Network里有没有OPTIONS请求再看OPTIONS的响应头比对Allow-Origin、Allow-Headers和Allow-Methods这三个字段是否覆盖了你实际请求的来源、头和方法。5.5 调试跨域的几个实用小技巧平时调试跨域我习惯看Network面板的几个关键位置请求行确认是不是走代理路径如果Request URL是http://localhost:8000/xxx而不是/api/xxx说明代理没生效请求类型出现OPTIONS说明触发了预检能提前知道后端有没有放行响应头直接搜Access-Control-Allow-看后端有没有输出、输出的是什么值请求状态(cancelled)或(cors error)直接对应浏览器拦截这时候点开看错误详情最有效。还有一个小工具浏览器里装一个CORS相关的插件可以在开发时临时给页面注入响应头方便快速验证接口本身是否正常。但插件只是调试辅助真正的项目里还是要按前面说的代理或后端配置来做别把插件方案当成上线方案。根据我这些年做前后端分离项目的经验最省事的组合是开发环境走devServer代理生产环境走nginx反代后端ThinkPHP的CORS中间件保持常开。三层配置各管一段互相兜底既照顾了开发效率也兼顾了线上安全性和兼容性。每次换环境部署照这个思路检查一遍基本一次就能跑通。