1. 跨域和同源
首先来看摘自MDN上对于跨域,较为标准的解释:
当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
比如,站点 http://domain-a.com 的某 HTML 页面通过 <img> 的 src 请求 http://domain-b.com/image.jpg。网络上的许多页面都会加载来自不同域的CSS样式表,图像和脚本等资源。
出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求。例如,XMLHttpRequest 和 Fetch 遵循同源策略。因此,使用 XMLHttpRequest或 Fetch 的Web应用程序如果不使用跨域技术,只能将HTTP请求发送到其自己的域。
同源策略(same-origin policy):
浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源,防止恶意的网站窃取数据、cookie等。
但不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。
比如我现在用的Chrome/61.0.3163.91,虽然有同源策略的存在,但是在调试工具的Network下,Status Code 200 OK, 说明数据是返回回来了, 并且可以在Preview 或者 Response里看到数据。
什么才是同源:
字符串完全匹配才是同源,协议不同 域名不同(子域名和主域名并不是同源)端口不同,都不算是同源。
本地调试时:
一个http-server服务器只能监听一个端口,监听多个可以设置不同端口,比如:
http-server -c-1 -p 80
http-server -c-1 -p 81
是跨域还是本域同源,看2个点:
- 发送AJAX 请求的当前页面 URL 是什么
- AJAX 请求的 URL 是什么
这两个 URL 同源,则不是跨域。
此外, <iframe> 标签,也是受同源策略限制的。
2. 跨域的几种方式
- JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
html中 <script> 标签可以引入其他域下的js,比如引入线上的jquery库。利用这个特性,可实现跨域访问接口, 但是需要后端支持。
首先引入标签,参数中指定回调函数名:
<script src="http://weather.com.cn?city=hefei&callback=showWeather">
请求到的数据, 由于受到后端支持,所以类似如下结构,后端把原始数据放在要执行的函数的参数里面返回给前端:
showWeather({
"city": "hefei",
weatheer: {xxx}
})
当数据返回到了客户端,由于是<script>标签,所以会自动当作js代码执行。
但是,我们虽然引入script标签时,在URL的中指定了callback的函数名,以便后端能用正确的函数名去‘包装’,可是这个传入了原始数据的函数在我们的浏览器中运行起来,到底要得到什么结果呢?
答案几乎是显而易见的,就像非跨域请求一样,我们需要对数据进行处理,比如解析数据,进行html的拼接,并最终展示到页面上。
所以我们要声明并完善这个callback函数:
function showWeather(json){
// do something
}
以上有个问题,就是在引入script标签的时候,是直接提前写在HTML文档里的,而我们拿到数据处理完后(js执行完毕后),这个标签就没有用处了,而且实际场景中有多个类似请求的情况时,如何才能保持HTML的干净利索呢?
可以考虑下面的方式,使用js创建script标签,引用到资源后,这里由于是立即并且逐条执行的,所以看似下一句立刻删除了标签,但实际上中间已经执行过了我们起初定义的函数。
document.querySelector('.change').addEventListener('click', function(){
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8080/getNews?callback=appendHtml';
document.head.appendChild(script)
document.head.removeChild(script) //请求数据,执行完毕后,就立即删除script的引入标签
})
而后端之前也提到了,只需要在同源请求中,增加判断语句,如果有callback参数,则返回使用函数‘包装’后的数据:
var cb = req.query.callback
if(cb){
res.send(cb + '(' + JSON.stringify(data) + ')')
}else{
res.send(data)
}
!注意演示中,使用了 server-mock 工具,使用随同NodeJS一起安装的包管理工具NPM进行server-mock的安装,然后把index.html 和router.js 放在一个文件夹,接着终端里进入当前文件夹, 使用 mock start,开启本地服务器即可。
- CORS(Cross-origin resource sharing) 跨域
CORS 也叫跨域资源共享,它是W3C标准,是跨源AJAX请求的根本解决方法,克服了AJAX只能同源使用的限制。相比JSONP只能发GET请求,CORS允许任何类型的请求,可以说是老式JSONP的现代升级版。
目前,除了 IE浏览器IE10以下外,所有浏览器都支持该功能。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,实现CORS通信的关键在与服务器支持与否,只要服务器实现了CORS接口,就可以跨域通信。
还是上一个实例,服务器端开启CORS的方法是,在响应头信息中添加 Access-Control-Allow-Origin:
res.header('Access-Control-Allow-Origin', 'http://wangpeng.com:8080')
//res.header('Access-Control-Allow-Origin', '*')
第二个参数用来声明哪些源站有权限访问哪些资源。
星号 *代表来自任意域名的请求,都不会受到浏览器同源策略限制。
- 降域实现跨域
和上面都是请求资源的场景不同。对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议,端口号,以及主机(document.domain 也就是原始域名----origin domain 设置为相同的值,也就是同时降域成一致)时,这两个脚本才能相互通信。
降域主要场景是, a.xxx.com 和 b.xxx.com 之间的访问,虽然都是xxx.com 子域名,但是也存在浏览器同源策略的限制, 也无法直接获取对方信息。此时,使用降域即可解决。
方法是: a.xxx.com 和 b.xxx.com 的页面JS中, 同时加入 document.domain = "xxx.com" 彼此都降域到主域名。
于是二者可以在各自的页面中,使用<iframe>
引入另一个域名下同时做了降域的页面,并可以进行相互操作。
但是,如果不是一个主域名下的两个二级域名,那么是不可能降域到一样的, 比如 a.baidu.com 和 b.taobao.com, 降域后分别是 baidu.com 和 taobao.com ,这显然不是同源。
使用server-mock工具 或者http-server 搭建本地服务,任意打开a页面,两个input中任意一个输入value值,另一个input会随之改变,说明实现了跨域操作。
- postMessage
上例中通过降域,向其他窗口比如 iframe 、执行window.open返回的窗口对象等发送数据,实现两个不同页面的脚本跨域通信,是存在局限性的,因为降域也要看域名条件。
HTML5为了解决这个问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。
这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。
举例来说,父窗口http://aaa.com向子窗口http://bbb.com发消息,调用postMessage方法就可以了。
对于不同的域下,可以向其发送数据,如果对方认可接受这个数据,那么就可以使用,如果对方没有监听接受这个数据,那么就没有任何效果。
postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。
还是上一个应用了降域的例子,这一次我们换用postMessage方法。
a页面:
//发送
document.querySelector('.main input').addEventListener('input', function(){
console.log(this.value)
//把输入框的值发给儿子iframe,第二个参数指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI
window.frames[0].postMessage(this.value, '*')
})
//监听iframe的消息
window.addEventListener('message', function(e){
document.querySelector('.main input').value = e.data
console.log(e.data)
})
//关于postMessage 的使用,MDN文档有详细描述,已经更规范更安全的建议,本文只是做跨域的简单探讨,简化了很多细节。
b页面:
//发送
document.querySelector('#input').addEventListener('input', function(){
window.parent.postMessage(this.value, '*') //把输入框的值发给parent
})
//监听parent的消息
window.addEventListener('message', function(e){
document.querySelector('#input').value = e.data
console.log(e.data)
})
关于跨域问题,MDN文档有详细使用描述,以及更规范更安全的建议。
另外,阮老师的这两篇博文也做了详尽的阐述,除了以上是最常见的几种跨域,还有其他场景下的跨域需求以及实现方式:
阮一峰-浏览器同源政策及其规避方法
阮一峰-跨域资源共享 CORS 详解
P.S. 最近学习了AJAX和跨域,刚好试着调用下API,发现很好玩! 可以做很多有趣的事情,唯一的局限是自身水平和想象力的局限。就在刚刚写这篇拙文的时候,就发现放在github上的音乐页面,不能请求到资源,说我请求的mixed content(混合内容,确切说是音频资源) 被block掉了。google一下,才恍然想起来,github pages 是 https协议的。赶紧跑去换了个https协议的API,搞定。
如有任何想法或疑惑,欢迎评论区提出,我们一起探讨:D