文章目录
- 流程
- 缺点
- 名称由来
- demo
- JSONP安全性问题
- CSRF攻击
- 5XSS漏洞
- 服务器被黑,返回一串恶意执行的代码
- 封装工具函数
- 真实案例:获取淘宝搜索关键字推荐
流程
script 标签 src 属性发起的请求不受同源策略的限制,并且 script 标签默认类型是text/javascript
。只要定义了这个类型,则script请求的内容就会被浏览器以JS代码来执行。这就为跨域提供了可能性。
既然可以执行JS代码,那我们就可以用函数包裹真实数据。
定义一个带有形参的函数,将函数名通过url的额外参数传递给服务器,服务器拿到函数后,将函数名拼成函数调用的字符串形式,并将响应数据序列化成字符串,以实参的形式传递给函数,然后返回给客户端。
src 属性请求回来后,因为 script 标签类型是 text/javascript,所以会执行请求回来的代码,也就相当于在执行这个函数。而这个函数的实参就是请求的数据,至此实现了跨域请求数据。
流程:
- 在发请求先,准备一个前后端约定好的全局接收函数,如 fn
- 在 html 创建 script 标签,src 带着 函数名
"fn"
发出请求 - 服务器传入数据,响应
fn({"name": "ikun"})
字符串给客户端 - 因为是 script 标签,客户端会执行
fn({name: "ikun"})
,也就是调用了定义的 fn 函数。
- 数据会自动反序列化进内存
缺点
因为是使用 url 额外参数的形式传递的函数名,url 都是 get 请求,所以 jsonp 也只能支持 get 请求。
名称由来
平常的前后端数据交互一般是 json 格式,现在服务端响应时函数包裹了 json 数据,所以是 json with padding(包裹),也就是 jsonp。
demo
<body>
<button>Click me</button>
<script>
const createJsonpRequest = (url, callback) => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = `${url}?callback=${callback}`;
script.type = "text/javascript";
script.async = true;
document.body.appendChild(script);
script.onload = () => {
document.body.removeChild(script);
resolve();
};
script.onerror = () => {
document.body.removeChild(script);
reject();
};
});
};
document.querySelector("button").addEventListener("click", async () => {
const url = "http://localhost:3000/jsonp";
await createJsonpRequest(url, "callback");
console.log("请求完毕");
});
window.callback = res => {
// do something with the response
console.log(res);
};
</script>
</body>
import "reflect-metadata";
import express from "express";
import { Container } from "inversify";
import { InversifyExpressServer, controller, httpGet, queryParam, response } from "inversify-express-utils";
@controller("/jsonp")
export class MyController {
@httpGet("/")
public async getJsonp(@queryParam("callback") callback: string, @response() res: express.Response) {
const data = { name: "John", age: 30 };
const jsonp = `${callback}(${JSON.stringify(data)})`;
res.type("application/javascript").send(jsonp);
}
}
const container = new Container();
container.bind(MyController).to(MyController);
const server = new InversifyExpressServer(container);
server.setConfig(app => {
app.use(express.json());
});
const app = server.build();
app.listen(3000, () => console.log("server is running at http://localhost:3000"));
JSONP安全性问题
CSRF攻击
前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。
解决方法:验证JSONP的调用来源(Referer),服务端判断 Referer 是否是白名单,或者部署随机 Token 来防御。
5XSS漏洞
不严谨的 content-type 导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://abc.com?callback=douniwan, 然后返回 douniwan({ data }),那假如请求 http://abc.com?callback=({ data })了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback 参数,直接当 HTML 解析了,就是一个赤裸裸的 XSS 了。
解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成<,>换成>)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。
服务器被黑,返回一串恶意执行的代码
可以将执行的代码转发到服务端进行校验 JSONP 内容校验,再返回校验结果。
封装工具函数
(function (global) {
var id = 0,
container = document.getElementsByTagName("head")[0];
function jsonp(options) {
if(!options || !options.url) return;
var scriptNode = document.createElement("script"),
data = options.data || {},
url = options.url,
callback = options.callback,
fnName = "jsonp" + id++;
// 添加回调函数
data["callback"] = fnName;
// 拼接url
var params = [];
for (var key in data) {
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
url = url.indexOf("?") > 0 ? (url + "&") : (url + "?");
url += params.join("&");
scriptNode.src = url;
// 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法
global[fnName] = function (ret) {
callback && callback(ret);
container.removeChild(scriptNode);
delete global[fnName];
}
// 出错处理
scriptNode.onerror = function () {
callback && callback({error:"error"});
container.removeChild(scriptNode);
global[fnName] && delete global[fnName];
}
scriptNode.type = "text/javascript";
container.appendChild(scriptNode)
}
global.jsonp = jsonp;
})(this);
使用示例:
jsonp({
url : "www.example.com",
data : {id : 1},
callback : function (res) {
console.log(res);
}
});
真实案例:获取淘宝搜索关键字推荐
看一下淘宝的搜索框,关键字联想推荐接口的响应数据格式,就知道这是 jsonp 的接口。
json 数据被函数包裹,__jp2 为函数名。
__jp2({
"result": [
["ikun车贴", "4541.673992673993"],
["i酷neo9手机", "4705.093023255814"],
["ikun手办", "4486.0609756097565"]
]
})
jsonp 是跨域的,所以我们也能获取到这些数据。
<body>
<form>
<input type="text" placeholder="请输入关键字" />
<button type="submit">搜索</button>
</form>
<ul></ul>
<script>
const renderList = data => {
const ul = document.querySelector("ul");
ul.innerHTML = "";
data.result.map(item => {
const li = document.createElement("li");
li.textContent = item[0];
ul.appendChild(li);
});
};
document.querySelector("form").addEventListener("submit", e => {
e.preventDefault();
const keywordUrl = encodeURIComponent(e.target.elements[0].value);
jsonp({
url: `https://suggest.taobao.com/sug?k=1&area=c2c&q=${keywordUrl}&code=utf-8&ts=1713023304435&callback=callback`,
callback: function (res) {
renderList(res);
}
});
});
</script>