对于javascript程序员来说,发送ajax请求获取后台数据然后把数据和模板拼接成字符串渲染回DOM实现无刷新更新页面这样的操作可谓是轻车熟路。但众所周知,ajax有一个不好,就是不能跨域传输数据,而跨域传输有时候又是必须用到的,比如我们可能需要调用第三方网站提供的某些API来获取某些信息,提供给我们网站的用户。
例如,要开发一个天气应用,你可能需要调用第三方的天气API,这个时候就必然涉及到跨域请求数据,因为毕竟我们不可能为了开发一个天气应用就自己搭建一个天气API。在少数情况下,如果第三方网站的服务器上设置了CORS机制,这时是可以直接用ajax发送请求的。但在大多数情况下,第三方网站都没有这么慷慨,因为涉及到安全问题,不可能允许任意人都能自由调用自己网站的接口。这个时候jsonp就应运而生了。
在HTML中有这样一类标签,它们都有一个src属性,src属性的值是一个链接,当标签一被解析到DOM中,就会开始把src属性中的链接指向的资源下载到本文档,例如,设置了src的img标签会自动从src属性的链接中下载资源,下载的资源又会被浏览器解析成图片,加到DOM中;设置了src属性的iframe元素会从src链接里下载一张网页;设置了src属性的script元素也会自动从src链接里下载javascript代码并执行,而这个过程中DOM本身也是没有刷新的,更令人心动的是,src属性的链接根本没有同域的限制。这个原理就是实现JSONP的基石。
但是,我们想用它来实现跨域还很有难度。这个src属性的元素的加载和一般的ajax请求有一个很大的不同,回想一下,在一个典型的ajax请求中,我们可以完全控制请求的过程,我们可以对指定的网页实行open,可以设置请求头,可以指定响应的MIMEtype,关键的是,我们可以从xhr对象的responseText属性中获取响应数据,然后拼接模板,渲染DOM。但在img, iframe, script这些标签中呢,我们的控制权就被剥夺了,我们设置src属性,浏览器负责发送请求,服务器端返回的响应直接就被加载回DOM了,我们根本没有插手修改数据的机会。
怎么办呢?这就像向遥远的深空发射一艘飞船,当飞船远离我们几十光年的时候,我们就不太可能从地面对它发送实时控制信号了,更好的做法就是把指令提前写在飞船上,让它自动执行。
和飞船的例子一样,一个典型的jsonp过程就是:创建一个script标签,设置src属性,这个src属性中包括了目标API的地址,我们的查询字符串querystring,querystring中最最重要的是我们的“指令”,因为script标签src返回之后,我们并不能控制返回的结果,所以最好让服务器返回的时候自己执行我们想要执行的操作。这个“指令”也就是jsonp中的“p”了。
举一个栗子:
1.我需要某个数据,比如就按照前面讲的,天气数据吧,于是我构造查询url。
var script = document.createElement('script'); script.src = 'www.weather.fake/get/?province=hubei&city=wuhan&callback=instruction'; document.getElementsByTagName('head')[0].appendChild(script);
//先写好指令,即回调 function instruction(data){ console.log(data); } //注意这里的instruction就是我们告诉服务器的“指令”。我们跟服务器说,我需要province为hubei,city为wuhan的地方的天气数据,但由于数据返回后我自己不能处理,所以你返回数据的时候自己处理好了,具体怎么处理,我已经写在名字叫做instruction的指令里面了。
2.weather.fake的服务器收到我的请求,从数据库里一找,找到了数据。一看,还有个指令,于是它就执行instruction指令。说起来很高级,实际上也就是把返回的数据包裹在instruction函数里面(jsonp的p,padding)。服务器于是返回这样一个东西:
//response.js instruction({
"city":"wuhan",
"weather":"cloudy"
})
3.服务器端的writeheader设置和浏览器端的accept设置会保证返回的东西会被浏览器解释成一个js文件,于是我们事先写好的指令instruction函数就得到了执行,整个jsonp过程就完成了。
需要注意的几点:
1.返回的js文件是在全局作用域执行的,所以你要保证你写的回调函数instruction在全局作用域里。
2.这里的callback=instruction。其中,callback只是一个普通的querystring,是你和服务器事先约定的,不同API提供方,名字也不同,有的可能就叫cb,等等。至于instruction,你爱写什么写什么,但要保证和你写的回调处理函数名字一致。我这里为了方便理解,就写instruction了。
余论:ajax跨域,危险在哪里?
有些人说,禁止ajax跨域,是为了防止攻击者利用它向自己的服务器发送敏感信息(例如cookie等等),这显然是错误的。对于一个已经被注入攻击代码的网站,攻击者如果只是想向自己的服务器发送信息而已,就算不用ajax,也完全可以用img等标签直接向自己的服务器执行get请求发送数据,况且创建一个img标签可比写一个完整的ajax过程简单多了,根本用不着兴师动众,也就是说,单向GET请求ajax并没有优势。
为什么要限制ajax,因为它太强大了。看看CORS机制,它规定的是服务器方面的接受白名单。也就是说,由服务器来决定可以接受哪些客户可以向它请求数据。说明跨域限制主要是为了保证服务器端安全的。
一般能用src做到的,ajax都能做到,ajax能做到的src却不一定能够做到。例如src只能发起get请求,但ajax能发起任意类型的请求。
举个栗子,某博客网站的删除文章功能API可能必须要用DELETE方法发送请求才能执行,这个时候攻击者如果只用src构造请求就无能为力了,但ajax却可以轻易模拟用户动作。这个时候禁止跨域就显得很重要了。
具体过程:假设我的攻击网站是www.evil.com。而一个允许CORS跨域传输ajax的博客网站是www.blog.com。该博客网站中,当用户点击了删除按钮时,就会向服务器发送DELETE请求。具体为:www.blog.com/user/delete(?id=10000)(由于是DELETE方法,实际上querystring是不会附在url上的,这里为了便于说明而已。)这个时候,假如用户访问我的evil.com网站,而我在我网站的脚本里写ajax:
$.ajax({ url:'www.blog.com/user/delete', method:'delete', data:{id:10000}, })
这样是能成功的,因为当发送ajax时,会自动带上用户在www.blog.com的cookie,而请求方法又合法,所以完全能取得blog.com服务器的信任,也就能不知不觉地删除用户在blog.com上写的文章。