JSONP && CORS
前天面试被问到了跨域的问题,自我感觉回答的并不理想,下面我就分享一下整理后的总结分享给大家
一、为什么要跨域
安全限制
JavaScript或Cookie只能访问同域下的内容——同源策略
同源策略
下表相对于: http://h5.jd.com/dir/ajax.js
注意
- 协议和端口造成的跨域问题,非前端解决范畴
- 所谓域,是通过“url首部”来识别,而非判断域与ip的对应关系
(“URL的首部”指window.location.protocol +window.location.host)
二、跨域方案
1. jsonp
原理
HTML里面所有带src属性的标签都可以跨域,如iframe,img,script等。
所以可以把需要跨域的请求改成用script脚本加载即可,服务器返回执行字符串,但是这个字符串是在window全局作用域下执行的,你需要把他返回到你的代码的作用域内,这里就需要临时创建一个全局的回调函数,并把到传到后台,最后再整合实际要请求的数组,返回给前端,让浏览器直接调用,用回调的形式回到你的原代码流程中。
- 首先,利用 script 标签的 src 属性实现跨域
- 通过将前端方法作为参数传递到服务器端,然后由服务器注入参数之后再返回,实现服务器端向客户端通信
- 由于使用script 标签的src 属性,因此只支持 get 方法
一个简单的jsonp实现,其实就是拼接url,然后将动态添加一个script元素到头部
1. 设定一个script标签 <script src="http://jsonp.js?callback=xxx"></script> 2. 服务器 $callback = !empty($_GET['callback'] ? $_GET['callback'] : 'callback'); echo $callback.'(.json_encode($data).)';
详见博客 JSON 和 JSONP两兄弟
2. cors
JSONP | CORS | |
目的 | 跨域 | 跨域 |
支持 |
get (受IE下url长度不能超过2083个字节的限制和出于安全考虑,一般不用来提交数据) |
所有类型的http请求 |
支持度 | 包括老式浏览器 | 不支持部分浏览器,移动端支持很好 |
缺点 |
1)安全问题(请求代码中可能存在安全隐患) 2)确定jsonp请求是否失败不太容易 3)只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题 |
支持率 |
原理 |
被包含在一个回调函数中的JSON 核心则是动态添加<script>标签来调用服务器提供的js脚本 (允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了) |
使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败 (只需由服务器发送一个响应标头即可) |
CORS需要浏览器和服务器同时支持
实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就可以跨域通信
1)两种请求方式
简单请求、非简单请求
a)简单请求:
跨域时,浏览器自动在头部信息中添加一个origin 字段(指定请求源-协议+域名+端口),如下图所示
服务器判断origin在域名许可范围内,返回响应:
若不存在 Access-Control-Allow-Origin 字段,则出错
以上头部信息中,CORS相关字段有
- Access-Control-Allow-Origin
必须字段,其值为 origin / *(可接受任意域名请求) - Access-Control-Allow-Credentials
可选,是否允许发送Cookie - Access-Control-Expose-Headers
可选,是否需要Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma之外的字段
withCredentials 属性
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
字段
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
注意
如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
b)非简单请求(不同时满足以上条件)
请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
浏览器对于非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
- Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
。
- Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
。
2.CORS 支持度
3. document.domain
两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain
共享 Cookie。
举例来说,A网页是http://w1.example.com/a.html
,B网页是http://w2.example.com/b.html
,那么只要设置相同的document.domain
,两个网页就可以共享Cookie
document.domain = 'example.com';
现在,A网页通过脚本设置一个 Cookie
document.cookie = "test1=hello";
B网页就可以读到这个 Cookie。
var allCookie = document.cookie;
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
4. iframe
(只有在主域相同时才能使用)
1)www.a.com/a.html中:
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://www.script.a.com/b.html';
ifr.display = none;
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
//在这里操作doc,也就是b.html
ifr.onload = null;
};
2) 在www.script.a.com/b.html中:
document.domain = 'a.com';
5. window.postMessage:
该方法是 HTML5 新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。
window.postMessage的功能是允许程序员跨域在两个窗口/frames间发送数据信息。基本上,它就像是跨域的AJAX,但不是浏览器跟服务器之间交互,而是在两个客户端之间通信
postMessage
方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*
,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法类似。
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message
事件,监听对方的消息。
window.addEventListener('message', function(e) { console.log(e.data); },false);
message
事件的事件对象event
,提供以下三个属性。
event.source
:发送消息的窗口event.origin
: 消息发向的网址event.data
: 消息内容
下面的例子是,子窗口通过event.source
属性引用父窗口,然后发送消息。
window.addEventListener('message', receiveMessage); function receiveMessage(event) { event.source.postMessage('Nice to see you!', '*'); }
event.origin
属性可以过滤不是发给本窗口的消息。
window.addEventListener('message', receiveMessage); function receiveMessage(event) { if (event.origin !== 'http://aaa.com') return; if (event.data === 'Hello World') { event.source.postMessage('Hello', event.origin); } else { console.log(event.data); } }
【a.qq.com页面代码】
【b.qq.com页面代码】
以上demo简单解决了前端跨域通信,跨域带cookie等问题,在逻辑上完全可以实现跨域通信。但是对于不支持PostMessage特性的老版浏览器是行不通的。比如IE8-浏览器就不能很好的支持PostMessage特性
6. 服务端代理
在数据提供方没有提供对JSONP协议或者window.name协议的支持,也没有对其它域开放访问权限时,我们可以通过server proxy的方式来抓取数据。
例如当www.a.com域下的页面需要请求www.b.com下的资源文件asset.txt时,直接发送一个指向 www.b.com/asset.txt的ajax请求肯定是会被浏览器阻止。
这时,我们在www.a.com下配一个代理,然后把ajax请求绑定到这个代理路径下,例如www.a.com/proxy/, 然后这个代理发送HTTP请求访问www.b.com下的asset.txt,跨域的HTTP请求是在服务器端进行的,客户端并没有产生跨域的ajax请求。
这个跨域方式不需要和目标资源签订协议,带有侵略性,另外需要注意的是实践中应该对这个代理实施一定程度的保护,比如限制他人使用或者使用频率。
其他跨域方案
window.name:
在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。
动态创建script
JSONP也就是利用这个原理。
利用iframe和location.hash
淘汰类技术
利用flash
淘汰类技术
参考链接:
http://tech.jandou.com/cross-domain.html
http://www.cnblogs.com/JChen666/p/3399951.html
http://www.ruanyifeng.com/blog/2016/04/cors.html
用HTML5里的window.postMessage在两个网页间传递数据
如果您认为本文对得起您所阅读所花的时间,欢迎点击右下角↘ 推荐。您的支持是我继续写作最大的动力,谢谢 (●'◡'●)
字节跳动职位长期内推,如有需求可发送简历至 lichaoran.cr@bytedance.com