跨域问题解决方案的弃子——JSONP
今天一位学弟“哭着”来找我,说他面试美团(实习岗)的时候,被问到了跨域问题的解决方案,回答的并不好。我还正想着,这么常见的问题回答不出来,这不就是基础不过关。但谁知道,面试官让他封装 JSONP 来实现跨域!怪我当时还告诉他们这种方案现在很少用,看一眼八股了解即可,现在可谓是啪啪打脸!
既然都问到了这个问题,那这篇文章就来详细讲讲跨域解决方案的弃子——JSONP!
-
为什么 JSONP 可以跨域?
之前的文章《跨域问题解决方案汇总》中详细阐述了跨域问题的由来和其他解决方案,这里就不再赘述了。
JSONP 可以实现跨域主要是利用了 <script src="https://api.example.com/data?callback=cb"> 脚本被浏览器加载,不受 SOP 限制(script、img、link 等资源请求是允许跨域的)。服务端返回 cb({...}) 回调函数,浏览器执行这段 JS,从而把数据传回页面。也就是说,JSONP 不是“绕过”安全策略,而是利用浏览器允许跨域 加载并执行脚本 的特性把数据“嵌回”到当前页执行。
-
JSONP 存在的安全隐患
- 任意脚本执行:服务端返回的内容会作为脚本执行,若服务器被劫持或返回恶意代码,会导致 XSS。如果不验证 callback 参数,攻击者可以传入
callback=alert(1);evil等恶意字符串导致执行。 - 只能使用 GET 请求,存在 CSRF 风险。
- 无响应体读取控制:无法控制响应头,自带所有风险。
至于什么是 XSS 和 CSRF 攻击,我们下篇文章再细嗦~
-
回到面试题——自己实现 JSONP
现在初入行的很多同学可能都没有自己实现过 JSONP,甚至因为考察较少,都没有听过 JSONP 。下面我将带着大家一步一步分析一下 JSONP 应该如何解决跨域。
我们需要考虑一下常见的网络请求,再结合 JSONP 实现:
- 客户端:
- callback 回调函数名
- 支持 Promise(成功/失败/超时)
- 动态插入
<script>,设置onerror、超时处理、清理 DOM 与全局回调
- 服务端:
- 只允许可信 callback 名(白名单校验)
- 返回
application/javascript,并输出callback(data)
完整示例
客户端,我们封装一个 jsonp 函数作为统一调用。
/**
* jsonp(url, opts) -> { promise, cancel }
*
* opts:
* - param: cb作为查询参数名称 (默认: 'callback')
* - timeout: ms (默认: 10000)
* - prefix: 参数名的前缀-保证唯一性(默认: '__jp')
* - name: 可选,显式的 callback 名称
*
* 返回:
* {
* promise: Promise<any>,
* cancel: () => void
* }
*
* Promise 成功:resolve(data)
* Promise 失败:reject(Error)
*/
function jsonp(url, opts = {}) {
const {
param = 'callback',
timeout = 10000,
prefix = '__jp',
name = null
} = opts;
return new Promise((resolve, reject) => {
const callbackName = name || `${prefix}_${Date.now()}}`;
const script = document.createElement('script');
// 插入 callback 参数到 url
script.src = `${url}${encodeURIComponent(param)}=${encodeURIComponent(callbackName)}`;
script.async = true;
let timer = null;
// 成功回调:全局函数
window[callbackName] = (data) => {
cleanup();
resolve(data);
};
// 处理错误与超时
script.onerror = () => {
cleanup();
reject(new Error('JSONP script error'));
};
// 超时
timer = setTimeout(() => {
cleanup();
reject(new Error('JSONP timeout'));
}, timeout);
// 清除掉 script 节点
function cleanup() {
if (script.parentNode) script.parentNode.removeChild(script);
if (timer) {
clearTimeout(timer);
timer = null;
}
try {
// 删除全局回调(避免泄露)
delete window[callbackName];
} catch (e) {
window[callbackName] = undefined;
}
}
// 插入到 document.head(或 body)
(document.head || document.body).appendChild(script);
});
}
客户端使用示例:
jsonp('https://api.example.com/get-article?id=1111')
.then(data => {
console.log('jsonp data', data);
})
.catch(err => {
console.error('jsonp error', err);
});
服务端示例(Node.js + Express)
// server.js (Express)
const express = require('express');
const app = express();
app.get('/api/jsonp/article', (req, res) => {
const callback = req.query.callback;
if (!callback) {
res.status(400).type('text/plain').send('Invalid callback');
return;
}
// 例如查询 DB 获得数据
const data = {
id: 111,
title: '跨域解决方案的弃子——JSONP',
author: 'Heo',
content: '...'
};
// Content-Type 应设置为 application/javascript
res.type('application/javascript');
res.send(`${callback}(data);`);
});
// 启动
app.listen(3000, () => console.log('JSONP server on 3000'));
这样看来,其实并不难,只是同学们可能对于 jsonp 的接触都仅限于八股,还是就是面试时候的紧张,导致自己想不到这一系列的封装过程,最终被问到实现细节只能发呆,白白错过了 offer。

浙公网安备 33010602011771号