前端跨域

什么是跨域

1.跨域的定义

广义的跨域是指一个域下对的文档或者脚本试图去请求另外一个域下的资源。

  • a链接、重定向、表单提交
  • <frame>、<link>、<script>、<img>等标签
  • background:url()、@font-face()
  • ajax 跨域请求
  • ……

狭义的跨域是指浏览器同源策略限制的一类请求场景。

####同源策略

                   前端跨域

前端跨域的主要解决方法

1.jsonp跨域

原理:动态创建<script>标签,然后利用<script>的src不受同源策略约束来跨域获取数据。

缺点:只支持get方式请求

  • 原生js实现
 1 var script = document.createElement('script');
 2    script.type = 'text/javascript';
 3 
 4    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
 5    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=jsonPCallback';
 6    document.head.appendChild(script);
 7 
 8    // 前端回调执行函数
 9    function jsonPCallback(res) {
10        alert(JSON.stringify(res));
11    }
12    
13   //服务端返回如下(后端返回执行函数):
14 jsonPCallback({"status": true, "user": "admin"})
  • jquery实现
1 $.ajax({
2     url: 'http://www.domain2.com:8080/login',
3     type: 'get',
4     dataType: 'jsonp',  // 请求方式为jsonp
5     jsonpCallback: "handleCallback",    // 自定义回调函数名
6     data: {}
7 });

2.CORS(跨域资源共享)

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

与jsonp相比:支持所有类型的HTTP请求。但JSONP支持老式浏览器。

1.简单请求

(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

同时满足以上两个条件的就是简单请求

 1 //简单请求,浏览器自动增加Origin字段,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
 2 Content-Type: text/plain
 3 Origin: http://www.domain.com
 4 User-Agent: Mozilla/5.0
 5 
 6 //如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器检查这个响应的头信息有没有包含Access-Control-Allow-Origin字段,没有的话,就会抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
 7 //如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
 8 
 9 Access-Control-Allow-Origin: http://www.domain.com  
10 //值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
11 
12 Access-Control-Allow-Credentials: true
13 //值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
14 同时,请求中也要设置
15 var xhr = new XMLHttpRequest();
16 xhr.withCredentials = true;
17 //Access-Control-Allow-Origin此时不能设为星号
18 
19 Access-Control-Expose-Headers: FooBar
20 //用于拿到XMLHttpRequest对象非基本字段

2.复杂请求

 1 //与简单请求不同的是,复杂请求多了2个字段,进行服务器预检:
 2 Access-Control-Request-Method:该次请求的请求方式
 3 Access-Control-Request-Headers:该次请求的自定义请求头字段
 4 
 5 //预检成功,服务器返回的响应
 6 //指定允许其他域名访问
 7 'Access-Control-Allow-Origin:
 8 //是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回
 9 'Access-Control-Allow-Credentials:true'
10 //预检结果缓存时间
11 'Access-Control-Max-Age: 1800'
12 //允许的请求类型
13 'Access-Control-Allow-Methods:GET,POST,PUT,POST'
14 //允许的请求头字段
15 'Access-Control-Allow-Headers:x-requested-with,content-type'

4.iframe 家族

1.window.name

window.name有以下特征:

  • 每个窗口都有独立的window.name与之对应;
  • 在一个窗口被关闭前,窗口载入的所有页面同时共享一个window.name,每个页面对window.name都有读写的权限;
  • window.name一直存在与当前窗口,即使是有新的页面载入也不会改变window.name的值;
  • window.name可以存储最多2M的数据,数据格式按需自定义。

原理:在页面中动态创建一个iframe页面指向另一个域,将数据赋值给ifram的window.name属性。(此时页面不能直接读取iframe的window.name),我们还需要将将iframe的src指向相同域的空白页面。之后再将iframe删除就可以了

 1 <!--a.html-->
 2 var proxy = function(url, callback) {
 3     var state = 0;
 4     var iframe = document.createElement('iframe');
 5 
 6     // 加载跨域页面
 7     iframe.src = url;
 8 
 9     // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
10     iframe.onload = function() {
11         if (state === 1) {
12             // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
13             callback(iframe.contentWindow.name);
14             destoryFrame();
15 
16         } else if (state === 0) {
17             // 第1次onload(跨域页)成功后,切换到同域代理页面
18             iframe.contentWindow.location = 'http://www.domain.com/aa.html';
19             state = 1;
20         }
21     };
22 
23     document.body.appendChild(iframe);
24 
25     // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
26     function destoryFrame() {
27         iframe.contentWindow.document.write('');
28         iframe.contentWindow.close();
29         document.body.removeChild(iframe);
30     }
31 };
32 // 请求跨域b页面数据
33 proxy('http://www.domain1.com/b.html', function(data){
34     alert(data);
35 });
36 
37 <!--b.html-->
38 <script>
39     window.name = 'This is domain1 data!';
40 </script>

2.document.domain

主域相同,子域不同的跨域应用场景。
原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

 1 <!--a.html-->
 2 <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
 3 <script>
 4     document.domain = 'domain.com';
 5     var user = 'admin';
 6 </script>
 7 <!--b.html-->
 8 <script>
 9     document.domain = 'domain.com';
10     // 获取父窗口中变量
11     alert('get js data from parent ---> ' + window.parent.user);
12 </script>

3.location.hash

location.hash:指的是URL的#后面的部分,比如www.domain1.com/b.html#hello 的#hello,只改变hash是不会刷新页面。
原理:通过中间页面来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

 

 1 <!--http://www.domain.com/a.html-->
 2 <iframe id="iframe" src="http://www.domain1.com/b.html" style="display:none;"></iframe>
 3 <script>
 4     var iframe = document.getElementById('iframe');
 5 
 6     // 向b.html传hash值
 7     setTimeout(function() {
 8         iframe.src = iframe.src + '#user=admin';
 9     }, 1000);
10     
11     // 开放给同域c.html的回调方法
12     function onCallback(res) {
13         alert('data from c.html ---> ' + res);
14     }
15 </script>
16 
17 <!--http://www.domain1.com/b.html-->
18 <iframe id="iframe" src="http://www.domain.com/c.html" style="display:none;"></iframe>
19 <script>
20     var iframe = document.getElementById('iframe');
21 
22     // 监听a.html传来的hash值,再传给c.html
23     window.onhashchange = function () {
24         iframe.src = iframe.src + location.hash;
25     };
26 </script>
27 
28 <!--http://www.domain.com/c.html-->
29 <script>
30     // 监听b.html传来的hash值
31     window.onhashchange = function () {
32         // 再通过操作同域a.html的js回调,将结果传回
33         window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
34     };
35 </script>

5.window.postMessage

otherWindow.postMessage(message, targetOrigin, [transfer]);

window.postMessage() 方法被调用时,会在所有页面脚本执行完毕之后(e.g., 在该方法之后设置的事件、之前设置的timeout 事件,etc.)向目标窗口派发一个 MessageEvent 消息。 该MessageEvent消息有四个属性需要注意: message 属性表示该message 的类型; data 属性为 window.postMessage 的第一个参数;origin 属性表示调用window.postMessage() 方法时调用页面的当前状态; source 属性记录调用 window.postMessage() 方法的窗口信息。
优势:页面和其打开的新窗口的数据传递、 多窗口之间消息传递、嵌套的iframe消息传递的信息传递

 

 1 <!--http://www.domain.com/a.html-->
 2 <iframe id="iframe" src="http://www.domain1.com/b.html" style="display:none;"></iframe>
 3 <script>       
 4     var iframe = document.getElementById('iframe');
 5     iframe.onload = function() {
 6         var data = {
 7             name: 'aym'
 8         };
 9         // 向domain1传送跨域数据
10         iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain1.com');
11     };
12 
13     // 接受domain返回数据
14     window.addEventListener('message', function(e) {
15         alert('data from domain2 ---> ' + e.data);
16     }, false);
17 </script>
18 
19 <!--http://www.domain1.com/b.html-->
20 <script>
21     // 接收domain的数据
22     window.addEventListener('message', function(e) {
23         alert('data from domain ---> ' + e.data);
24 
25         var data = JSON.parse(e.data);
26         if (data) {
27             data.number = 16;
28             // 处理后再发回domain
29             window.parent.postMessage(JSON.stringify(data), 'http://www.domain.com');
30         }
31     }, false);
32 </script>

注意:

如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。

如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。

当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*

6. nginx

7. Nodejs中间件代理

阮一峰CORS

window​.post​Message