那些年我们遇到的跨域问题
关于跨域问题的三三两两。
1. 同源策略
跨域的根本原因是浏览器的同源策略,那么什么是同源策略呢?
同源策略,它是由Netscape提出的一个著名的安全策略,现在所有支持 JavaScript 的浏览器都会使用这个策略。所谓同源是指协议,域名,端口相同,相反,只要协议,域名,端口有任何一个的不同,就被当作是跨域。
浏览器采用同源策略,禁止页面加载或执行与自身来源不同的域的任何脚本。比如一个恶意网站的页面通过iframe嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的javascript脚本就可以在用户登录银行的时候获取用户名和密码。
2. 解决跨域
跨域影响:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 Js对象无法获得
- AJAX 请求不能发送
1. CORS
跨域技术-CORS (CrossOrigin Resources Sharing,跨源资源共享),它是 HTML5 的一项特性,定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
浏览器将CORS请求分成两类:简单请求和非简单请求。
只要同时满足以下两大条件,就属于简单请求,反之,不满足以下条件的都属于复杂请求。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
1. 简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段。表明该请求来自哪个源,服务器根据这个值,决定是否同意这次请求:如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段(如下)。如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,从而抛出错误。
1 | // 同源允许,多出的几个头信息字段 |
2. 非简单请求
非简单请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求,也就是我们常说的 OPTIONS 请求。服务器收到”预检”请求以后,检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认是否允许跨源请求。如果浏览器肯定了“预检”请求,就会返回一个正常的且带有CORS相关字段的回应,反之,如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
1 | // 预检通过,允许请求的回应 |
3. CORS 简单实现
Node.js 实现 CORS 跨域请求
1 | app.use(function (req, res, next) { |
2. JSONP
利用 script,img 等这些元素的开放策略,即不受同源策略的影响。其工作流程如下:
- 声明一个回调函数(cb),其函数名当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据data
- 创建一个 script 标签,把跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=cb)
- 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是cb,它准备好的数据是cb({“name”:”aaaaaandy”})。
- 最后服务器把准备的数据通过HTTP协议返回给客户端,同时修改返回头中’Content-Type’: ‘text/javascript’,则数据返回客户端后会马上执行之前声明的回调函数(cb),最后对返回的数据进行操作。
需要注意的是:jsonp 只能实现 GET 请求而不能实现 POST 请求。
3. nginx 代理
利用 nginx 通过反向代理 满足浏览器的同源策略实现跨域,不需要目标服务器配合
将不同的域名转换成相同的就解决了跨域的问题,客户端发送请求时不直接到服务器而是先到代理的中间层,由中间层把请求源地址改为与服务器地址相同,当数据返回时,也是先经过中间层,将数据来源地址改为与客户端地址相同。这样就不受同源策略的影响。
1 | server { |
还可以在nginx中添加头部实现跨域
1 | server { |
4. websocket
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,它本身允许跨域通讯,是server push技术的一种很好的实现。
1 | // 客户端 |
1 | var io = require('socket.io')(server); |
5. postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源。也就是说,它实现的是不同页面之间的通讯。
1 | // 1.html |
1 | // 2.html |
6. window.name
当在浏览器中打开一个页面,或者在页面中添加一个iframe时即会创建一个对应的window对象,当页面加载另一个新的页面时,window的name属性是不会变的。这样就可以利用在页面动态添加一个iframe然后src加载数据页面,在数据页面将需要的数据赋值给window.name。然而此时承载iframe的parent页面还是不能直接访问,不在同一域下iframe的name属性,这时只需要将iframe再加载一个与承载页面同域的空白页面,即可对window.name进行数据读取。
1 | // localhost:8088/2.html |
1 | // localhost:8081/1.html |
这里是使用iframe将2.html加载过来,因为只是为了实现跨域,所以将之隐藏,但是,这时已经完成了最重要的一步,就是将iframe中window.name已经成功设置,但是现在还获取不了,因为是跨域的,所以,我们可以把src设置为当前域的proxy.html。
1 | // proxy.html 其实就是一个空白页面 |
7. document.domain
这种方式只适合主域名相同,但子域名不同的iframe跨域。比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。