ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PHP跨域解决方案:CORS与JSONP从原理到实战

PHP跨域解决方案:CORS与JSONP从原理到实战 做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 两个方向的解决办法后端为主还是前端绕行方案一叫CORSCross-Origin Resource Sharing跨源资源共享它是一个标准方案核心做法是PHP在响应头里声明允许的来源、请求方法、请求头等。现代浏览器只要看到符合要求的响应头就会放行。支持GET、POST、PUT、DELETE等也支持自定义头、Cookie跨域等复杂场景。方案二叫JSONPJSON 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-urlencoded、multipart/form-data、text/plain这三种。一旦你用了application/json或者加了Authorization、X-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-Cookie里SameSite是什么值。需要跨域带登录态时通常要把SameSite设为None并且同时给Secure也就是Cookie必须走HTTPS。这一块不在PHP代码里但在真实项目中经常成为跨域登录失败的最后一道坎。3. 方案二JSONP老牌前端绕过思路PHP只需输出一段剧本3.1 JSONP的本质借
返回列表