从 JSONP 开始跨域
跨域问题一直是困扰很多人的一个现实,我其实对这方面的很多细节也不了解,很多具体的实现仍然停留在前端的范畴内,至于后端的同学到底怎么配置的,一无所知。不得不说即便是前端开发,我觉得还是一样需要有那么一段时间接触一下后端实践,比如抽个一年半年的时间从事全职的后端开发,这对整个 Web 开发的体系会有一个更深层次的理解和延展。
但是事实也是,我毕竟还是致力于成为专业的前端开发,从前端的角度入手理解一下跨域的问题也是极好的。
起航
第一次接触跨域是从事全职前端的第二个月,当时一个客户希望改造他们的网店系统,新增一个销售员分级系统。后端直接采用 Node,前端则还是基于原先的店铺进行二次开发,那么数据并不在原先的店铺域名下,因此需要跨域抓取数据,由于只是展示的目的,因此采用了最兼容的 JSONP
幼稚的我当时其实压根没想过 JSONP 后面的实现基础和原理,我只知道在当时的场景中,跨域非常好用,jQuery 封装好了简便的 API,我只需要调用就好了,反正就是把 JSON 包装起来传来传去罢了。
事实上,JSONP 跟 JSON 没有半毛钱关系,这让我想起一个比喻 —— JavaScript 是 Java 的蠢弟弟。望文生义往往是我致命的一个弱点,任何技术都是,在没有认真尝试和理解前,我都觉得要保持一种敬畏的态度。
JSONP 系列
跨域的第一个,也是最常用的,就是 JSONP 所采用的跨域原理,这里不只是 JSONP,而是一系列的跨域解决方案都是基于一个浏览器的“恩惠”:我允许你随便引进来某几种东西
我清楚地记得,在学习 Bootstrap 的时候,其中的 demo 代码我是可以直接复制到 HTML 中然后打开就可以复现了,我并没有下载他们的脚本和样式文件,因为每一个学习前端的人都知道,浏览器自己会跑到 Bootstrap 的网站引用这些(当然,现在我们知道这是 CDN,有其特殊的用处)—— 我很久以前,大学的时候就知道只要在网页中插入引用路径,不管是 JS,CSS,还是图片
因此可以给一个定义,凡是带有 src 这个属性的标签,其实都是不受同源策略的限制的
<script src="..."></script>外部 JS 引用<img src=“...”>,可以是 PNG,JPEG,GIF,BMP,SVG 等<iframe>页面嵌套<link rel="stylesheet" href="...">外部 CSS 引用<video>和<audio>嵌入多媒体资源<object>,<embed>和<applet>的插件@font-face一些浏览器允许跨域引入字体
基于这类的跨域原理,一般我们只考虑用前三个(<script、<img>、<iframe)实现跨域技术:因为这三个可以兼容老旧的浏览器,至于现代浏览器么,交给 CORS 是更明智的选择。
基于 CSS 的跨域技术叫 CSST(CSS Text Transformation),与其他跨域技术一样对低级浏览器兼容不好(需要支持 CSS3)
最兼容最常用的 JSONP
JSONP 是利用资源跨域允许的最常见技术,我们知道,在 HTML 中,脚本的执行一般是顺序的,也就是说后面 <script> 标签一般是在前面 <script> 标签执行完后再执行的,同时这些脚本间共享全局变量,比如
第二个脚本可以获取到第一个脚本中的全局变量 name 并打印出它的值,就基于以上的规则,假如把第二个脚本换成外部脚本(里面的内容一致),那么实际上是会产生同样效果的。别忘了,外部脚本是不受同源限制的,这就是 JSONP 跨域的基本
那么具体是怎样实现的呢?考虑以下代码:
我们在第一个脚本中定义了一个函数,这个函数接受一个参数,调用这个函数会打印出参数中的一个属性值,我们在第二个脚本中直接传参调用了这个函数,于是打印出了我的名字缩写。不知道有没有注意到一个问题,第二个外部脚本是后面请求的,如果我们不是一开始就写好了这个脚本,它又是怎样知道这个函数名并且知道它需要怎样的参数呢?
继续往下挖掘,我们使用跨域技术,其实是希望能跟普通的 Ajax 请求一样,请求一个特定的网址并附上参数,就可以从服务器那边拿到想要的数据,因此 srcB 的服务器根本就不知道你想要执行的函数是什么。于是一个就需要一个通用的约定:请求脚本的时候,url 上带上参数,这个参数告诉了服务器 srcA 这个脚本定义的函数名字是什么,需要的数据又是什么,srcB 的服务器根据这些来动态生成脚本并返回。于是按照约定出现了以下代码:
这里动态生成一个 <script> 标签并插入到了页面 <head> 中,页面会立即向服务器请求这个脚本,注意到请求 url 中到这些参数:?callback=jsonp&id=2017,callback=jsonp 告诉服务器你生成到代码要把参数传到这个函数中调用,&id=2017 则告诉服务器需要作为参数传入到数据是 id 为 2017 的用户的资料,于是服务器动态生成了需要的脚本响应了浏览器的请求,于是 srcA 就打印出来了这个用户的资料!这就是 JSONP 的全部细节。
实际上,插入后的脚本在代码执行后一般就再也没有用了,但它在 DOM 中依然可以被查看到,像 jQuery 这种类库会帮您在回调执行完以后删除这个标签
同时,如果基于团队开发,一个页面中可能用到非常多但 JSONP 请求,很难避免两个人同时使用一个回调的函数名称(但有时简便的实现是也可能后台写死了),这会导致冲突,因此类似 jQuery 这种类库会生成一个足够随机的回调函数名来实现,总之,如果要兼容老旧的浏览器,加入 jQuery 确实少了很多麻烦
上面就是 JSONP 的技术实现和原理,其优点是非常简单易用,几乎兼容所有的浏览器实现跨域的双向信息交互
但是缺点也是明显的,通过请求脚本的方式注定了其只能支持 GET 请求,同时如果无法确定请求服务器是否安全的,在响应中有可能夹带恶意代码(XSS 攻击),最后是,无法判断请求是否失败(只能通过设置定时器来判断响应是否超时)
广告商的最爱 Image Ping
“图像Ping”,其实每天都活跃在我们的身边,每天我们打开的网页,有 90% 以上都使用到了这个技术
跟互联网的几乎所有的广告流量跟踪一样,我们在店铺的流量跟踪脚本 tracker.js 中也是利用了图片的跨域访问原理
广告是网站盈利最重要的一个地方,那么就需要监听广告到底有多少个人看,于是就需要有一个服务器来统计这些流量来源
假如你打开一个网页,里面有这么一段代码:
可以预见得到,你会向服务器 www.trace.com 发起一个图片的请求,这个请求 url 的参数传递了我的名字缩写
实际上,更常用更灵活的方法是使用脚本来进行这个操作,我们知道,在脚本中请求图片,即使并没有插入到 DOM 中(实际上使我们的 HTML 代码更干净),依然会触发网络请求,为后续可能的插入做好缓存。同时脚本中我们可以进行更多的操作(如服务器未响应可以考虑重新请求)
在单页 APP 中,这也常用来跟踪用户到底访问了哪个页面,每切换一次路由,就请求一次图像 Ping,参数附上当前所在页面
在这里需要注意一下服务器常见的反应:服务器会接受到完整的请求,因此就获得来需要的查询信息(在这里是我的名字缩写,但其实可以用脚本传递几乎所有字符串,因此可能存在安全问题),一般服务器接到请求后会返回一个(1px x 1px)的透明图片,或者只返回 204 状态码
图像Ping 和 JSONP 一样都只能发送 GET 请求,而且只能单向通讯,只有服务器可以拿到浏览器给的数据
iframe 系列
前面提到了 iframe 也可以跨域,但是并没有把它归并到 JSONP 一起讲解,这是因为 iframe 本质上并不能算是一种资源,而且其跨域的手段跟前面的资源类跨域实现也有很大区别。
跨文档消息传送(cross-document messaging),简称 XDM,主要是指一个页面通过某种技术手段与另外一个页面进行数据交互,一般说的这两个页面是跨域的,举例说,主页面需要向内嵌的 iframe 或者弹出的窗口传递信息
其技术基础一般有:
document.domainlocation.hashwindow.namepostMessage()
我听说古老的 IE6 的存在一个 bug,父页面和子页面都可以访问
window.navigator这个对象,通过在这个对象上添加属性或方法,就可以达到共享数据啦(然并卵,谁会这么做呢?)
其中,前三种都是基于 <iframe 标签进行的兼容性实现,而 postMessage() 实际上是 XDM 的规范化实现
document.domain
比较常用的跨域方法,但限制是,只能用于同一个主域下不同子域之间,比如 foo.com 与 img.foo.com,或者 img.bar.com 与 mp3.bar.com,但是如果用在 foo.com 与 baz.com 之间则会失败
下面的代码清晰地展示了这个机制的应用:
|
|
location.hash
这个方法是利用了同域名的子 iframe 是可以通过 top 或者 parents 来拿到父框架的引用的原理(及作为通讯的中转,hash 一般是在中转页面中处理)
|
|
这个方法实现起来相对繁琐,同时跨域的数据都需要暴露在 URL 中,由于 URL 的长度一般有所限制,导致传送的数据量也相对有限(只能传可 JSON 化的数据)
window.name
在同一个浏览器中(同一个窗口同一个标签页),window.name 可以在不同页面加载后依然保持,举例说:打开了 www.baidu.com这个页面,在控制台中输入 window.name = 'test',然后在地址栏输入 www.qq.com,然后再次查看 window.name 会发现其值依然保持为 test,利用这种特性就可以在嵌套的页面中进行数据传送
写作这篇时,我的 Safari 浏览器测试不到这个实现,建议放弃使用这个方法,因此也不过多进行赘述
postMessage()
前面所说的页面间的通讯方法,实现起来既麻烦又容易出错,还有各种限制,因此如果不需要兼容 IE8 以下的浏览器,我们迎来了一个归一的替换方法:postMessage(message, url),这个方法第一个参数是希望传递的数据,第二个参数则一般是一个 url,指定接受数据的源,只有源匹配才能成功通讯(可以指定为 * 允许所有源)
以上是发送信息,接受信息是以异步事件的形式完成的,以下是一个简单的应用:
点击父框架 foo 的按钮会向子框架传递一个字符串,子框架通过监听事件的方式可以接收到这个不限制接收源的信息
可以看到,相比于前面定义的其他框架间跨域通讯方法,postMessage 方法的使用清晰了很多,跟我们定义一个 DOM 事件并无太大的区别,而且本方法兼容 IE8,可以说在现今这个甚至可以抛弃 IE8 的大环境下,框架间的通讯完全可以只考虑 postMessage
CORS 系列
总算进入现代浏览器的范畴了,经历了前面那么多曲折的 Ajax 跨域实现,我们是多么希望这个同源策略可以在安全的范围内有所打破!
CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-Origin Resource Sharing),主要针对 Ajax 跨域,需要浏览器和服务器同时支持。一般而言,CORS 与同源的 Ajax 在前端开发者和用户体验上并没有什么区别,支持的浏览器会自动在请求头部加上特殊的头信息,因此,该技术在开发中关键在意服务器的配置(当然,一般只需要注意 IE8 和 IE9 并没有原生实现,需要通过兼容的版本 XDR),根据不同的请求在服务器中设置 Access-Control- Allow-Origin 并予以回应
对于本技术原理的详尽解析,建议参考阮一峰的博客:跨域资源共享 CORS 详解,这里我只讲解浏览器中的具体实现和兼容
原生的
CORS标准实现中,在前端代码层面上跟写普通的 XHR 没有任何区别,不过考虑安全,有一些限制- 不能使用
setRequestHeader()自定义请求头部 - 不会携带
cookie - 调用
getAllResponseHeaders()总是返回空字符串(无效化)
- 不能使用
IE 中的实现,同样也有一些限制
- 不会携带
cookie - 只能设置头部的
Content-Type - 不能访问响应的头部
- 只支持
GET和POST
- 不会携带
XDR 也跟 XHR 很类似,如下:
其他思路
前面介绍了很多跨域的实现,基本上都是假定浏览器会遵循同源策略,所以是以绕过同源限制的方法去考虑一些问题。但我有时想,能不能打破这个限制呢?其实是有的,还很多
你一定觉得很搞笑,同源策略是浏览器最基本的安全实现,怎么可能不遵守呢?
没错,浏览器一定会遵守同源策略,但是我们可以给浏览器一个“代理”,这个代理并不是浏览器,它可能就没有同源限制。
实际上,最简单的跨域实现是,让你的同源服务器代你跨域拿取数据!我们一直忽略了这一点:跟自己的服务器之间一直是同源的。
基于这个思路,其实就有很多这样的代理实现:臭名昭著的 Flash,各种浏览器控件,浏览器给了它们信任的接口,但它们自己却没有同源策略的限制。方法虽好,但随之带来的是安全性的下降,用户体验的缺失(浏览你的网站还需要特定的插件,想想就不好了),因此这些方法也即将随着现代浏览器的快速更新和普及而消亡……