杂谈
阿里已经三次技术面了,最近再等hr面,不知道还能不能等得到~ 看到牛客网上很多同学都已经在今天收到offer了,只拿到cvte的offer的我心里真不是个滋味,嗯,加油吧,秋招一定要拿到阿里的offer!
正文
面试中关于跨域的问题还是问了不少的,至少在阿里的这几面中,几乎每一面都会问到更跨域相关的问题,然后就是说了点后,对方继续问,结果可想而知,就是一脸懵逼~ 所以这里总结以下跨域相关的问题。
本文主要参考自这篇博客。
什么是跨域?
首先说一下同源策略,即同协议、域名、端口号,netscape浏览器规定如果有一个不满足就不允许访问另一个网页,如a.com和b.com就不是同源,那么a.com就不能得到b.com中的相关内容。但是,对于这个得到我一直是模棱两者的,到底是得到什么,怎么个不能访问法? 不甚了解, 而这篇博客中说的是这样的:
因为同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。
也就是说这里强调的有两点:
- 第一: 一定是js而不是其他东西来操作。js是一个关键词。
- 第二: 不能访问另一个源下的对象。 即不是不能访问什么任何东西,只是不能访问另一个源下的对象而已。这里的对象是一个关键词。
举例如下所示:
值得注意的是:
- 第一:如果是协议和端口造成的跨域仅仅通过前端是没有办法解决的。
- 第二:在跨域问题上,我们需要判断地仅仅是是否是 同协议、同域名(而不是IP)、同端口号,也就是说我们不会去考虑IP的相关问题,只看域名是否相同就可以了。 (域名和IP之间是多对多的关系)
几种不同的跨域方式
方法一: 通过 document.domain + iframe 的设置
这种方法主要解决的是跨子域的问题。具体的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html两个文件中分别加上document.domain = ‘a.com’;然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!代码如下:
www.a.com上的a.html
document.domain = 'a.com'; var ifr = document.createElement('iframe'); ifr.src = 'http://script.a.com/b.html'; ifr.style.display = 'none'; document.body.appendChild(ifr); ifr.onload = function(){ var doc = ifr.contentDocument || ifr.contentWindow.document; // 在这里操纵b.html alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue); };
注意: 这里我们设置iframe的display为none,所以在满足跨域的情况下,不会有任何影响。
script.a.com上的b.html
document.domain = 'a.com';
通过这样的设置,我们就可以跨(子)域来进行访问另一个域中的对象了,如例子中访问了 h1。 h1就是对象。
这种方式适用于{www.kuqin.com, kuqin.com, script.kuqin.com, css.kuqin.com}中的任何页面相互通信。
存在的问题:
- 安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
- 如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。
实例:
在 qq.com 网站中,就使用到了document.domain这种跨域方式,因为www.qq.com和v.qq.com就是这种关系,即跨子域的问题,使用document.domian = 'qq.com',就可以使得www.qq.com的脚本来获取到v.qq.com 的内容了,并且v.qq.com我们不需要显示出来,所以就display: none;, 而我们打开body下的连个src就可以发现,里面几乎都有 contentWindow 这个关键词,即对v.qq.com的内容进行操作。
总结:
这里我们可以看到: 跨域并不是说一定是跨域请求,往往跨域请求,我们是通过cors或者是jsonp来实现的,但是这里的时跨域获取某个对象,即使用document.domain设置为相同的域,然后我们再利用iframe,就可以使用contentDocument获取到另外一个子域的document了,这样,对于我们处理像阿里首页请求数据的情况或者网易首页请求数据的情况还是很有好处的。
2. 利用iframe和location.hash
这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。在url: http://a.com#helloword中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。代码如下:
先是a.com下的文件cs1.html文件:
function startRequest(){ var ifr = document.createElement('iframe'); ifr.style.display = 'none'; ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.html#paramdo'; document.body.appendChild(ifr); } function checkHash() { try { var data = location.hash ? location.hash.substring(1) : ''; if (console.log) { console.log('Now the data is '+data); } } catch(e) {}; } setInterval(checkHash, 2000);
cnblogs.com域名下的cs2.html:
//模拟一个简单的参数处理操作 switch(location.hash){ case '#paramdo': callBack(); break; case '#paramset': //do something…… break; } function callBack(){ try { parent.location.hash = 'somedata'; } catch (e) { // ie、chrome的安全机制无法修改parent.location.hash, // 所以要利用一个中间的cnblogs域下的代理iframe var ifrproxy = document.createElement('iframe'); ifrproxy.style.display = 'none'; ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata'; // 注意该文件在"a.com"域下 document.body.appendChild(ifrproxy); } }
a.com下的域名cs3.html
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值 parent.parent.location.hash = self.location.hash.substring(1);
当然这样做也存在很多缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等……
3. window.name 跨域
为什么使用window.name可以跨域呢? 首先我们需要的是,只要是在一个窗口之下,那么window.name的值就是不变的:
如test.html内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script> window.name = 'foo' </script> </head> <body> 这是第一个页面 <br> <script> document.write('当前window.name值为 ',window.name) </script> <br> <a href="./test2.html">跳转到第二个页面</a> </body> </html>
这里我们把window.name设置为foo。
test2.html内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> 这是一个新的页面 <script> document.write('当前window.name值为 ',window.name) </script> </body> </html>
当我们在同一个窗口中,从test.html跳转到test2.html中时我们可以发现,window.name都是foo,当然,直接打印name也是foo,这是一个非常好的特性,利用这个特性,我们就可以实现跨域通信了。
并且必须是 window.name, 如果我们把两个页面中的window.name修改为window.hhhh,然后再测试,发现在test2.html中window.hhhh的值就是undefined了。
但是呢? 这样还是做不了跨域的操作的,我们还是模仿一下吧,看文章《使用window.name跨域》
4、使用HTML5 postMessage
HTML5中最酷的新功能之一就是 跨文档消息传输Cross Document Messaging。下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。
otherWindow.postMessage(message, targetOrigin);
otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。
message: 所要发送的数据,string类型。
targetOrigin: 用于限制otherWindow,“*”表示不作限制
这种跨域方式比较适合于向iframe中的页面传递数据,比如一个网站在登录的时候,会弹出一个qq的框,这个Origin就是qq的,那么我们希望将数据传递到这个框中,该怎么办呢,这时候,我们就可以使用postMessage这个api了,如主要网站的test1.html如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test1</title> </head> <body> <h2>test1页面</h2> <iframe src="http://localhost:8088/test2.html" frameborder="1"></iframe> <script> var person = { name: 'wayne zhu', age: 22, school: 'xjtu' } var ifr = document.querySelector('iframe') ifr.onload = function () { var targetOrigin = 'http://localhost:8088'; ifr.contentWindow.postMessage(JSON.stringify(person), targetOrigin); } </script> </body> </html>
这里需要使用到iframe,所以就不隐藏了, 然后onload(很重要,否则会报错)之后就可以将数据传递到我们希望传递到的地方了。
在iframe里的那个页面的内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test2</title> </head> <body> <h2>test2页面</h2> <script> window.addEventListener('message', function (e) { console.log( e.origin ,'传递来的数据:',e.data); console.log(); }, false); </script> </body> </html>
这样,我们就可以获取到从Localhost:8081传递来的数据了,是不是很简单呢?
代码地址: https://github.com/zzw918/cross-origin
并且PostMessage使用的也比较广泛,一般,这种使用场景都是父子关系网页的使用场景,在sina.com.cn中就使用到了 postMessage 进行跨域:
当然,也是将iframe进行了隐藏,为的只是跨域。
另外,我们在 http://www.alibaba.com/ 中也可以看到使用iframe进行跨域的情况:
所以,到目前为止,我所看到的iframe的使用场景几乎都是跨域相关的。
5. CORS 跨域资源共享
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将跨域资源共享cors分为了两种方式,一种方式就是简单请求,另外一种自然就是非简单请求。只要同时满足下面的两大条件,就是简单请求:
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不能同时满足上面的两个条件,就是非简单请求,浏览器对于简单请求和非简单请求的处理是不同的。下面我们来分别介绍。
简单请求:
对于简单请求来说,就是直接发出跨域cors请求, 就是在头信息中,添加一个origin字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面的头信息中,Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin
指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头。
(1)Access-Control-Allow-Origin
这个字段是必须的,它的值也只有两种可能,第一种就是 请求时的 Origin 的值,要么就是一个*, 表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
这个字段可选。 是一个布尔值,表示是否运行发送cookie。 默认情况下,Cookie不包括在CORS请求中。设置为true,则表示服务器明确许可,Cookie可以包含在请求中,一起发送给服务器,注意:这个值也只能设置为true,如果服务器不要浏览器发送Cookie,直接删除这个字段即可。
(3)Access-Control-Expose-Headers
这个字段可选。 CORS请求时,xhr对象的getResonseHeader() 方法只能拿到6个基本字段: Cache-Control、 Content-Language、 Content-Type、Expires、Last-Modified、Pragma。 如果希望拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定它,上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
withCredentials属性
上面说到: CORS请求摩尔你不发送Cookie和HTTP认证信息,如果把Cookie发到服务器,一方面需要服务器同意,即指定 Access-Control-Allow-Credentials为true,另外一方面需要开发者在ajax请求中打开 withCredentials 属性:
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送,或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略了withCredentials设置,有的浏览器还是会一起发送 Cookie, 这时, 可以显示的关闭 withCredentials, 即:
xhr.withCredentials = false
需要注意的时: 如果希望发送Cookie,那么Access-Control-Allow-Origin 就不能设置星号*, 而是必须明确的指明与请求网页一致的域名, 同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传, 而且(跨源)原网页中的document.cookie也无法读取服务器下的Cookie。
非简单请求
1、预检请求
非简单请求时值那种对服务器有特殊要求的请求, 比如请求方法是 PUT 或者 DELETE, 或者Content-Type 字段的类型是 application/json,也就是说, 不满足上面所讲的简单请求的条件,那么这个请求就是非简单请求。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,成为‘预检’请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用那些HTTP动词和头信息字段,只有得到了肯定答复,浏览器才会发出正式的xhr请求,否则就会报错。
如下面的js脚本:
var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();
上面代码中,http请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header, 那么浏览器就会发现,这是一个非简单请求,就会自动发出一个‘预检’请求,要求服务器确认可以这么请求, 下面就是这个‘预检’请求的HTTP头信息:
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
即这个预检请求的请求方法是OPTIONS,表示这个请求时用来询问的,关键字origin来表示请求是来自那个源的,接着,除了Origin字段,‘预检’请求的头信息包括两个特殊字段:
(1)、Access-Control-Request-Method
这个字段是必须的。 用来列出浏览器的CORS请求会用到哪些HTTP方法,上面例子已经说了就是发送一个PUT请求,所以在预检请求中,Access-Control-Request-Method的值是PUT。
(2)、Access-Control-Request-Headers
因为简单请求中,不会有多余的请求字段,但是这是非简单请求,有了多余的请求字段,所以Access-Control-Request-Headers的值就是上面的X-Custom-Header。
2、预检请求的回应
在上面的介绍中,我们已经发出了预检请求,但是回应是怎么样的呢? 首先回应之前肯定是要检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers 字段的,通过后,回应如下:
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
其中,比较关键的就是 Access-Control-Allow-Origin字段,表示http://api.bob.com 是可以请求数据的,该字段也可以是*,表示同意任意的跨源请求。
Access-Control-Allow-Origin: *
如果浏览器否定了‘预检’请求,就会返回一个正常的http回应, 但是没有任何CORS相关的头信息字段,这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获,控制台会打印出如下的报错信息:
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS字段如下:
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
这个字段是必须的,使用逗号分隔,表示服务器支持的所有的跨域请求的方法,注意:返回的是所有支持的方法,而不单是浏览器请求的那个方法,这是为了避免多次预检请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括 Access-Control-Request-Headers 字段,则Access-Control-Allow-Headers 字段就是必须的, 他也是一个逗号分隔的字符串, 表明服务器支持的所有头信息字段,不限于浏览器在预检请求中请求的字段。
(3)Access-Control-Allow-Credentials
这个字段和简单请求时的含义相同。
(4)Access-Control-Max-Age
这个字段是可选的,用来指定本次预检请求的有限期,单位为秒,上面结果中,有效期是20天(1728000秒),即允许缓存该条回应 1728000s, 在这期间,不用发出另外一条预检请求。
3、浏览器的正常请求和回应
一旦服务器通过了‘预检’请求,那么以后每次浏览器正常的CORs请求都和简单请求时一样的,会有一个Origin头信息字段,服务器的回应,也会有一个Access-Control-Allow-Origin头信息字段。
下面是预检请求之后,浏览器的正常CORS请求:
PUT /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com X-Custom-Header: value Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面头信息的Origin字段是浏览器自动添加的。
下面是服务器正常的回应:
Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
总结: CORS还是非常方便的, 它和JSONP使用的目的是相同的,但是比JSONP更为强大,因为JSONP只能支持get请求,CORS支持所有类型的HTTP请求。JSONP的优势就在于在老师浏览器和不支持CORS的网站请求数据。
6、JSONP
JSONP即JSON with padding 。 我们知道,如果希望请求其他域来获取数据,这是不可能能的,因为同源策略的限制,那么怎么才能获取到呢?
不难想到,src属性有这个功能,无论是哪个域的文件,我们都可以通过src这个属性。 那么,开发者就开始设计,即在本地创建一个函数,然后引入一个script,这个script可以请求远程的js文件,而这个js文件恰好就是在调用我们之前写的函数,调用的时候,将我们想要的数据填充进来,然后在函数里,我们就可以处理这个传进来的数据了,这不就结束了嘛!
但是,我们怎么才能让远程js文件知道这个函数的名称是什么呢? 很简单,只要在js文件之后添加一个callback=函数名 即可,而为了做到如此,大部分时间我们的做法都是这个script是动态创建的,这样就可以很方便的传入callback和其他一些必要的参数了,查看网易和阿里的网站可以发现,使用了大量了JSONP,而之所以他们没有使用更好用的CORS,是因为IE10一下还不能很好地支持,所以用JSONP可以很好地解决兼容性问题,只是这是一种hack方法,并且没有那么方便而已!