6.4 JSONP 与跨域通道 6.1 节说过,同源策略拦的是"读取响应"而不是"发出请求"。这个细节留了一道缝,本章最后一节就来讲这道缝里长出来的技术——JSONP:把数据包装成脚本、借脚本标签不受同源限制的便车过境。它是一代人的跨域解法,也是一代人的安全教材。本节讲清它的原理与局限,再走一遍现代的正路(CORS),最后把散落各节的安全底线一次收拢。 本节摘要:JSONP 的机制是"约定回调名 + 脚本标签取数":服务端把 JSON 包进函数调用返回,浏览器当脚本执行即完成交付;它只能发 GET、错误无处安放、且执行来路不明的代码,注定是过渡方案。现代正路是 CORS 响应头放行,原生 fetch 与 都能直连。
6.1 节说过,同源策略拦的是"读取响应"而不是"发出请求"。这个细节留了一道缝,本章最后一节就来讲这道缝里长出来的技术——JSONP:把数据包装成脚本、借脚本标签不受同源限制的便车过境。它是一代人的跨域解法,也是一代人的安全教材。本节讲清它的原理与局限,再走一遍现代的正路(CORS),最后把散落各节的安全底线一次收拢。
本节摘要:JSONP 的机制是"约定回调名 + 脚本标签取数":服务端把 JSON 包进函数调用返回,浏览器当脚本执行即完成交付;它只能发 GET、错误无处安放、且执行来路不明的代码,注定是过渡方案。现代正路是 CORS 响应头放行,原生 fetch 与
$.ajax都能直连。
普通 AJAX 取回的是数据,脚本标签加载的是代码——这就是 JSONP 全部的巧思。服务端不返回裸 JSON,而是返回一次函数调用,函数名与前端约定:
// 前端:先在全局备好接收函数 function onQueueArrive(data) { console.log('收到队列:', data.length); } // 再动态造一个脚本标签去"加载"数据 var s = document.createElement('script'); s.src = 'api/queue?callback=onQueueArrive'; document.body.appendChild(s);
// 服务端实际返回的"正文"是一行可执行代码 onQueueArrive([{ name: '张三' }, { name: '李四' }]);
浏览器的执行流:脚本标签加载这个地址,把响应当 JS 代码执行,恰好调用约定好的函数——数据就这样穿过了同源的门。jQuery 把这套仪式封装进 $.ajax:配置 dataType: 'jsonp' 并给出回调名,其余与普通请求无异。
$.ajax({ url: 'api/queue', dataType: 'jsonp', jsonpCallback: 'onQueueArrive', // 也可不指定,让 jQuery 自动生成临时函数名 success: function (data) { renderQueue(data); } });

JSONP 的三宗局限都源于"借道"这个出身。其一,只能 GET——脚本标签天生只会发起 GET,提交类需求无能为力。其二,错误无处安放:脚本加载失败既不走 error 回调也不抛出可捕获的异常,超时检测要自己造临时定时器。其三,也是最重的一条:它在页面上执行对方返回的代码。回调函数名是前端拼进地址的,若把用户输入直接拼进去,攻击者可以构造地址执行任意脚本;即使名字可控,服务端一旦被攻破,返回的"数据"也可以是任意恶意代码。所以老规矩必须立住:回调名绝不拼接用户输入,JSONP 只用于完全可信的数据源。
跨域的正解叫 CORS(跨源资源共享),机制朴素:服务端在响应头里声明允许谁读,浏览器校验通过即放行。对前端来说,服务端配好之后什么都不用做——普通请求直接发:
$.ajax({ url: 'https://api.example-partner.com/queue', dataType: 'json', success: renderQueue }); // 服务端放行即可,前端无特殊配置 fetch('https://api.example-partner.com/queue') .then(function (r) { return r.json(); }) .then(renderQueue);
要点有两条。其一,带凭据(Cookie)的跨域请求需要显式配置,$.ajax 加 xhrFields: { withCredentials: true },fetch 是 credentials: 'include',且服务端要相应放宽声明——三者缺一即失败。其二,非简单请求(如带自定义头的 POST)浏览器会先发一次探询请求确认,见到"多一次请求"的日志不要惊慌,那是安检流程。
本章把散落的安全要点归成一页清单,逐条自查:
.load、html() 填入的内容里若含脚本或事件属性,注入就地生效。不可信来源一律先转义或只用 text()。beforeSend 里禁用按钮、complete 里恢复,弱网下的重复下单多半缺这一步。timeout;全局 ajaxError 统一提示,避免"静默失败"变成"用户以为成功"。| 通道 | 前提 | 能力边界 | 适用判断 |
|---|---|---|---|
| 同源代理 | 开发期或网关层可配转发 | 无边界,浏览器眼里始终同源 | 本地联调外部接口、生产由网关统一收口 |
| CORS | 服务端愿意配响应头 | 全方法支持、可带凭据 | 自家多域名、可信合作方,新项目首选 |
| JSONP | 服务端支持回调包装 | 只 GET、错误无回调 | 存量接口已是 JSONP 且不可改动时 |
选型次序从上往下:能同源就同源,能 CORS 就 CORS,JSONP 只做存量兼容。开发期的代理转发值得多说一句——把外部地址映射成本地路径,既绕开了浏览器的安检报错,也让代码里的地址与环境无关,上线换网关配置即可,页面代码一个字不用改。
背景:联调日,页面请求合作方接口,控制台甩出跨域阻止的红字。同屏症状不同,处理也不同,按三种病因分诊。
病因一,协议不一致:页面是本地开发服务器,接口是远端地址,协议域名端口全对不上。处理走代理转发,本地映射后红字消失。病因二,服务端声明没配到位:接口在浏览器网络面板里能看到响应体,但脚本读不到——这是"拦读不拦发"的典型现场,返回体就在那里给你看,就是不让脚本碰。处理找服务端同事配放行头,配好后同一段代码直接工作。病因三,带凭据却没声明:请求本身通过,但 Cookie 死活带不过去。处理是前端 xhrFields 与服务端凭据声明同时到位,缺一半都失败。
解读:跨域故障的分诊口诀是"先看三件套差在哪,再看响应体到没到,最后查凭据配没配"。三种病因在控制台的报错几乎同屏同款,区分它们靠的是网络面板的证据而不是红字本身——这也是 6.1 生命周期站点表的应用题。
七章只剩最后一站。器械用过一轮,该进保养车间了:工具函数、性能账本与时代迁移。