2017-10-01
jsonp
jsonp、跨域和同源策略
jsonp (JSON with Padding) 是一种解决浏览器跨域请求的一种方案,它需要前端代码和后台的共同努力才能实现。
本文着重介绍它的原理和实现。

# 同源策略

要理解 jsonp 的出现,先得清楚同源策略,浏览器对同源的定义是:
在 http 请求的 url 里,如果协议,端口(如果指定了一个)和域名对于两个页面是相同的,则两个页面具有相同的源
浏览器指定了许多策略来对待不同源的 http 请求,这些策略约束了 ajax 或者 img 标签的行为。
如果没有同源策略,基于 Session 存储会话的 Web 系统可能会很危险:
  1. 某个用户登录了银行 但是没退出
  2. 然后他去了别的网站,刚好那个网站含有破坏性 JavaScript 代码
  3. 虽然 JS 不能访问到其他源的 cookie ,但是可以发起 XHR 请求,这些请求里面会带上 Session
  4. 如果攻击者分析过银行的系统设计,他就有能力获得用户的交易记录,甚至发起转账请求等等
而有了同源策略,攻击者的 XHR 将会被浏览器 ban 掉,不会发起,保证了安全。
而有时候,同源策略太严格了,不适合开发,尤其是有多个域名和子域名的时候就更明显了。

# 跨域资源共享

这个是服务端的解决方案,借助 Access-Control-Allow-* 一系列字段让浏览器解除策略影响。
00app.use('*', function(req, res, next){
01    res.header("Access-Control-Allow-Origin", "*");
02    res.header("Access-Control-Allow-Headers", "X-Requested-With");
03    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
04
05    next(); 
06})

# jsonp

jsonp 的原理非常精巧,它利用了 JavaScript 可以直接解读 JSON 的特性,还有 Script 标签 src 属性可以跨域请求脚本的历史遗留问题,它的过程如下:
假设某接口URL为 http://xxx.com/api/ping?xx=abc&d=asd
且 GET 它会得到 JSON 字符串:
00{"hello": "world"}
在同源策略的情况下,我们无法利用 ajax 在其他源下请求这个 URL
因此,出现了如下标签的写法:
00<script src="http://xxx.com/api/ping?xx=abc&d=asd"></script>
很显然,这样会执行那个 JSON 字符串,也就是说,成功捕获到了这个 JSON 字符串对应的对象。
如果后台稍作修饰,让这个接口返回这样的 JSON
00alert({"hello": "world"});
那么以下标签,将会 alert 一个对象出来:
00<script src="http://xxx.com/api/ping?xx=abc&d=asd"></script>
其中 alert 可以算是 ajax 中的回调函数,该过程可以称呼为 alert 捕获了 http 请求结果。
这便是 jsonp 的原理。

# 实现

在此实现一个简单的 jsonp 封装
00// 把对象转化成查询字符串
01var queryStringify = o => {
02    return Object.keys(o).map(key => {
03        // let val = encodeURIComponent(o[key]); 
04        let val = o[key]; 
05        return [ key, val ]
06    }).map(temp => {
07        let [key, val] = temp; 
08
09        return `${key}=${val}`; 
10    }).join('&')
11}
12
13// 计数器和函数名的命名空间
14let JSONP_COUNTER = 0;
15let CB_NAME = 'GW_JSONP';
16
17var getFuncName = () => CB_NAME + JSONP_COUNTER ++;
18
19function jsonp(url, query){
20    // 每次调用的名字都不一样 
21    let funcName = getFuncName();
22    // 用来告诉后台修饰返回的 json 需要的捕获函数名
23    query.callback = funcName; 
24
25    // 计算出 script 需要的 url 
26    let fullurl = url + '?' + queryStringify(query)
27
28    console.log('URL', fullurl); 
29
30    return new Promise((res, rej) => {
31        // 先设置回调 (即 alert 这样的捕获函数)
32        window[funcName] = res; 
33
34        // DOM 创建一个 src 为 fullurl 的 script 并插入到最后面
35        var body = document.getElementsByTagName('body')[0];
36        var script = document.createElement('script');
37        script.setAttribute('src', fullurl);
38        body.appendChild(script);
39    });
40}

# 例子

利用上面封装的 jsonp 去做一次跨域请求:
目标 API http://m.kugou.com/app/i/getSongInfo.php , 该接口由酷狗提供,支持 jsonp 跨域。
它需要的参数有:
  1. cmd, 一般是 ‘playInfo’
  2. hash, 歌曲的 hash , 可以通过酷狗提供的其他接口提供,这里使用常量 a97e9c9ebdee85bc52147de6825f3da0& 进行获取
  3. format 表示返回的结果格式,这里填 ‘jsonp’
  4. callback 表示用于修饰json用的捕获函数名,这个字段由我们封装的函数内部维护,无须操作
00let target_url = `http://m.kugou.com/app/i/getSongInfo.php`;
01let query = {
02    cmd: 'playInfo', 
03    hash: 'a97e9c9ebdee85bc52147de6825f3da0&', 
04    format: 'jsonp'
05}
06
07jsonp(target_url, query).then(console.log);
结果如图:
jsonp 调用结果
jsonp 调用结果
当然也可以正常播放和查看专辑封面的
埃若漫鹅老师
埃若漫鹅老师
还有就是这链接可能会失效 233

# 使用过程中的种种

其实原理我很早看过,但没有亲手写一个,感觉现在才理解到精华,此外,今晚在用的时候发现 jsonp 还是有些问题:
它只支持 GET, 这是 script 的原因, 而且也无法在 https 下请求 http 资源, 不过我找到了一个替代的方案去解决这个问题,即利用 https 代理请求来完成 http 资源的获取。
关于代理,我选择了 JsonBird, 当然不是打广告,可是它确实比较好使,开源免费,如果有所顾忌,可以挂在自己服务器上运行。




回到顶部