PHP跨域解决方案:CORS与JSONP从原理到实战
2026/9/14 5:16:08 网站建设 项目流程

做PHP接口开发的同学,十有八九都被跨域请求折磨过。明明接口在Postman里测得好好的,一放到浏览器里调用,控制台就飘红:Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy。我在前后端分离项目里维护过不少PHP接口,也帮前端同事排查过很多次这类问题,今天想把我实际用过的两种实用解法完整拆开讲一讲:一种是后端正统的CORS响应头方案,另一种是经典的前端绕行思路JSONP。无论你是刚接触PHP的初学者,还是被Vue、React前端项目跨域问题缠住的老手,这篇文章应该都能给你一个能直接抄作业的方向。

1. 跨域问题到底是哪儿疼

1.1 同源策略与浏览器的"多管闲事"

跨域这个词,指的不是服务器拒绝了你,也不是网络不通,而是浏览器基于同源策略,把“跨源请求的响应”拦下来了。同源策略要求页面请求的协议、域名、端口三者完全一致,比如https://a.example.com去请求https://b.example.com,域名不一样,就属于跨域。哪怕localhost:8080请求localhost:8081,端口不同也算跨域。这里注意一点:请求往往会发出去,服务器也会收到并处理,但浏览器在看到响应头里没有明确允许当前来源的标记时,会直接不让JS读取响应内容。所以你会看到Network里请求是200,但控制台仍报错。Postman这类工具之所以测着没事,是因为它们不是浏览器,不存在同源策略这道关卡。理解了这一点,就明白PHP解决跨域问题的核心不是让服务器拒绝或接受请求,而是想办法让浏览器相信“这个来源可以读这个响应”。

很多刚入门的朋友会有个误会,以为跨域错误是后端报的错。其实后端可能一直正常工作,参数校验、数据库查询、日志记录全都在跑,只是响应的内容被浏览器藏了起来。我之前就遇到过排查半天后端日志,结果发现接口处理完全正常,最后才意识到纯粹是响应头没配CORS导致的浏览器拦截。所以遇到跨域报错,第一反应不应该是改业务代码,而是先看响应头。

1.2 一个典型的PHP跨域失败现场

为了有画面感,我举个真实场景:前端页面跑在http://localhost:8080,调用http://api.local/index.php/user/info获取用户信息。PHP接口逻辑很简单,可能就是这样:

<?php $user = ['name' => '张三', 'level' => 8]; header('Content-Type: application/json'); echo json_encode($user);

浏览器打开页面后,控制台就会看到类似这样的报错:

Access to XMLHttpRequest at 'http://api.local/index.php/user/info' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在这个阶段如果你马上用curl http://api.local/index.php/user/info去测,会发现数据能正常返回。这就是典型的浏览器拦截问题。解决的办法就是让PHP返回一个或多个Access-Control-*响应头,或者用JSONP躲开XHR限制。

1.3 两个方向的解决办法:后端为主还是前端绕行

方案一叫CORS(Cross-Origin Resource Sharing,跨源资源共享),它是一个标准方案,核心做法是PHP在响应头里声明允许的来源、请求方法、请求头等。现代浏览器只要看到符合要求的响应头,就会放行。支持GET、POST、PUT、DELETE等,也支持自定义头、Cookie跨域等复杂场景。

方案二叫JSONP(JSON with Padding),思路完全不同。它利用<script>标签天然可以跨站加载JS文件的特性,让前端把数据请求包装成一次脚本加载,PHP返回的是一段“用回调函数包起来的JS代码”。JSONP老但实用,缺点也很明显:基本只能用于GET,且安全性需要格外注意。接下来两章我把这两种方案的实现和坑位都撸一遍。

2. 方案一:PHP后端响应头里的CORS,最正统的解法

2.1 CORS是怎么工作的:从一次带Origin的请求说起

CORS靠响应头工作。跨域请求发出时,浏览器会自动在请求头里带一个Origin: http://localhost:8080,标明本次请求来自哪个源。服务器收到后,可以选择在响应里返回Access-Control-Allow-Origin。如果这个头的值和请求的Origin一样,或者干脆是*(通配),浏览器就认为服务端允许这个来源访问;如果响应里没有这个头,或者值对不上,浏览器就会按“被拦截”处理。

所以当你给PHP接口加上一行:

header('Access-Control-Allow-Origin: *');

一般最简单的跨域GET就能通了。*表示不区分来源,所有网站都能读取接口数据。这种写法适合完全公开的接口,比如天气数据、城市列表。但只要涉及用户登录态、Cookie、敏感数据,就不能随便用*,原因后面细说。

这里要多说一句,Origin头是浏览器自动加上的,你不需要在前端手动设置,也设置不了。如果请求是同一个源发起的,浏览器可能不会带Origin头,或者带上的值和当前页面一致,这都不会触发跨域逻辑。很多PHP框架里,你可以在入口处打日志看$_SERVER['HTTP_ORIGIN']是否存在,这是判断请求是否跨域最直接的方式。

2.2 核心PHP代码:Access-Control-Allow-Origin等关键头设置

实际项目中,单一行Access-Control-Allow-Origin往往不够。比如前端用了application/json的POST请求,或者带了Authorization头,浏览器会先发一次预检请求,要求服务端把允许的方法和头都一并声明。所以一个相对完整的PHP跨域响应头模板是这样:

<?php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('Access-Control-Max-Age: 86400');

Access-Control-Allow-Methods是服务端允许的HTTP方法,Access-Control-Allow-Headers是允许前端携带的请求头,Access-Control-Max-Age表示预检结果能缓存多少秒。有Max-Age在,浏览器在一段时间内不会对同样条件的请求重复发预检,能减少一次额外的网络往返。

如果你的接口需要按域名做白名单,就不能写死*,而是要先取Origin再判断:

<?php $allowedOrigins = [ 'https://admin.example.com', 'http://localhost:8080', ]; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowedOrigins, true)) { header("Access-Control-Allow-Origin: $origin"); header('Vary: Origin'); header('Access-Control-Allow-Credentials: true'); }

这里我习惯把Vary: Origin也加上。原因和缓存有关:如果浏览器或CDN缓存了某个带Access-Control-Allow-Origin的响应,而源站没有告诉缓存层“这个响应和Origin有关”,那么来源A的响应可能被后面来源B的用户复用,带来跨域数据泄漏风险。Vary: Origin是让缓存区分不同Origin,属于很容易被忽略但很重要的细节。

2.3 预检请求OPTIONS:为什么GET没事POST却经常失败

很多人会碰到“GET接口已经通了,POST一调就报CORS错误”的怪事。这就要提到预检请求了。浏览器把跨域请求分成两类:一类叫简单请求,一类叫预检请求。简单请求的条件比较苛刻:方法只能是GET、HEAD、POST,而且请求头不能带自定义头,Content-Type也仅限于application/x-www-form-urlencodedmultipart/form-datatext/plain这三种。一旦你用了application/json,或者加了AuthorizationX-Requested-With之类的自定义头,浏览器就会先发一个OPTIONS方法请求,去问服务器:“我准备这么跨域请求,你允许吗?”只有预检通过,浏览器才会发真正的业务请求。

PHP接口如果没有专门处理OPTIONS,框架路由可能直接返回404或者空的200,预检自然过不了。一个常见的处理方式是放在入口文件或公共中间件里,在业务代码执行之前直接处理掉:

<?php if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { header('Access-Control-Allow-Origin: http://localhost:8080'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With'); header('Access-Control-Max-Age: 86400'); http_response_code(204); exit; }

这个写法能解决大部分POST跨域问题。但有一点需要注意:Access-Control-Allow-Headers不能随便写,必须把你前端实际会带的自定义头都列进去。比如前端请求头里有Authorization,你这里漏了,浏览器照样会拦。排查时可以直接在浏览器的Network里看预检请求的Access-Control-Request-Headers,它会把实际请求要带的自定义头列出来,照着这个字段回填到Access-Control-Allow-Headers里基本就不会错。

还有一个容易被忽略的点:OPTIONS预检请求本身不应该执行业务代码,也不应该要求登录态。很多PHP框架有全局鉴权中间件,如果把OPTIONS请求也拦截下来返回401,浏览器同样会认为预检失败。所以处理预检的逻辑一定要放在鉴权之前,让所有OPTIONS请求先拿到“放行”的响应头并返回204。

2.4 支持带Cookie的跨域请求(credentials)与白名单动态配置

如果前端代码里用了fetch(url, {credentials: 'include'}),或者XHR设置了withCredentials = true,就表示跨域请求要带Cookie。这时候浏览器对CORS的要求会变得非常严格:Access-Control-Allow-Origin不能是*,必须是具体的源;同时响应头里必须要有Access-Control-Allow-Credentials: true。否则请求会被浏览器拦下,哪怕前面的响应头看着都正常。PHP这边设置如下:

<?php $allowedOrigins = [ 'https://admin.example.com', 'http://localhost:8080', ]; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowedOrigins, true)) { header('Access-Control-Allow-Origin: ' . $origin); header('Access-Control-Allow-Credentials: true'); header('Vary: Origin'); }

这段代码只对白名单内的来源返回CORS头,白名单外的来源拿不到Access-Control-Allow-Origin,浏览器自然也不会放行。比起无脑*,这种动态白名单的方式更适合需要登录态的接口。还有一点要提醒:如果接口同时需要提供开放访问和带Cookie访问,不要尝试设置多个Access-Control-Allow-Origin头,因为浏览器只认一个明确的值;你应该通过判断Origin来动态决定是否返回Access-Control-Allow-Credentials。这样逻辑清晰,也不容易留下安全风险。

另外,跨域带Cookie还有一个现实问题:Cookie本身的SameSite属性。现代浏览器默认把SameSite设为Lax,跨站请求时很多Cookie不会自动带上。这意味着即使你PHP端的CORS配置全部正确,如果Cookie的SameSite策略不允许,后端也收不到登录态。排查这类问题时,除了看CORS响应头,还要看目标站点的Set-CookieSameSite是什么值。需要跨域带登录态时,通常要把SameSite设为None并且同时给Secure,也就是Cookie必须走HTTPS。这一块不在PHP代码里,但在真实项目中经常成为跨域登录失败的最后一道坎。

3. 方案二:JSONP,老牌前端绕过思路,PHP只需输出一段"剧本"

3.1 JSONP的本质:借

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询