解决跨域问题!

发布于 2020-07-26  269 次阅读 本文共8871个字


前边介绍了《什么是跨域?》,现在呢,来看看如何解决掉跨域问题!

第一种:JSPON

在《什么是跨域?》中我们提到img、css、js这三种标签是允许跨域加载资源的,这是为了减轻web服务器的压力,就把img、css、js等静态资源分离到另一台独立域名的服务器上,在html中在通过相应的标签从不同的域名下加载静态资源。这是被浏览器允许的,所以基于此,我们可以通过动态创建script标签,来请求一个带参网址实现跨域通信。

<script>
    const script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "http://scar.vip:80/login?callback=handcb";
    document.head.appendChild(script);
    function handcb(res) {
        console.log("O(∩_∩)O哈哈~ 就是我呀!");
    };    
</script>

看这个栗子:
首先就是创建一个<script>标签,把那个跨域的api数据接口地址,赋值给script的src,还要再这个地址中向服务器传递该函数名(可以通过问号传参 ?callback=handcb)。
服务器接收到请求之后,需要进行特殊处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串。
最后,服务器通过HTTP协议将数据返回给客户端,可续断在执行之前声明的函数回调,对数据进行操作即可!

如果在开发中用到这种方式,封装一个JSPON函数还是很有必要的,我这里就不举例了。

不过注意:JSPON有个缺点,就是只能实现GET这一种请求。

第二种:跨域资源共享(CORS)

CORS是W3C标准,全称是 Cross-origin resource sharing,他允许浏览器向服务器跨源发送XMLHTTPRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和后端同时支持。IE8、9需要通过XDomainRequest来实现。

服务端设置 Access-Control-Allow-Origin 就可以开启CORS,前端无需设置,如果需要携带Cookie,则前后端都需要设置。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有的网站都可以访问资源。

浏览器将CORS的请求分为两个,分别是 简单请求 和 复杂请求。

简单请求:只要同时满足一下两大条件就属于简单请求

条件一: 使用下列方法之一

  • HEAD
  • GET
  • POST

条件二:请求的Header是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type 的值仅限于三种:application/x-www-form-urlencoded、multipart/form-data、text/plain

不同时满足上面的两个条件的话,就属于复杂请求了。

简单请求

对于简单请求,浏览器直接发出CORS请求,具体的说,就是在header信息中,增加一个Origin字段:

CORS请求设置的响应头字段,都以Access-Control-开头:

  • Access-Control-Allow-Origin (必选)
    它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  • Access-Control-Allow-Credentials(可选)
    它的值是一个布尔值,表示是否允许发送Cookie
  • Access-Control-Expose-Headers(可选)
    CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

复杂请求:

复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求用的请求方法是OPTIONS,通过该请求来知道服务端是否允许跨域请求。"预检"请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method(必选)
    用来列出浏览器的CORS请求会用到哪些HTTP方法
  • Access-Control-Request-Headers(可选)
    该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段

服务器接收到请求之后,检查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,确认允许跨域请求后就做出回应。

HTTP回应中,除了关键的是Access-Control-Allow-Origin字段,还有其他几个相关的字段:

  • Access-Control-Allow-Methods(必选)
    它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法
  • Access-Control-Allow-Headers (分情况)
    如果浏览器请求包括Access-Control-Request-Headers字段,则这个字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段
  • Access-Control-Allow-Credentials (可选)
    这个字段与简单请求一样,是一个布尔值,表示是否允许发送Cookie
  • Access-Control-Max-Age(可选)
    本次预检请求的有效期,单位/s

第三种:nginx代理

nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin...等字段。

1、nginx配置iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,所以可以在nginx静态资源服务器中加入以下配置:

location / {
  add_header Access-Control-Allow-Origin *;
}

2、Nginx 反向代理

使用nginx反向代理实现跨域是最简单的跨域方式,只需要修改nginx的配置即可解决跨域问题。支持所有浏览器、支持session、不需要修改任何代码,并且不会影响服务器性能。

跨域原理:同源策略是浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要经过同源策略,也就不存在跨域。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理domain2接口,并且可以顺便修改Cookie中的domain信息,方便当前域Cookie写入,实现跨域访问。

首先需要搭建一个中转nginx服务器,用于转发请求。下载nginx,在修改nginx目录下nginx.conf文件:

# proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

然后重启nginx:

nginx -s reload

第四种:Node中间件代理

Node中间件代理其实和Nginx代理原理基本是一样的,都是通过启动一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中Cookie中域名,实现当前域Cookie写入。

使用中间件实现跨域请求,必须卸载使用路由之前

app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*"); // 允许的来源
    res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS'); // 允许请求方法
    res.header("Access-Control-Allow-Headers", "X-Requested-With"); // 请求头部
    res.header('Access-Control-Allow-Headers', 'Content-Type'); // 请求头部
    next();
})

第五种:postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,postMessage可以实现跨文档消息传输(Cross Document Messaging),Internet Explorer 8, Firefox 3, Opera 9, Chrome 3和 Safari 4都支持postMessage。

该方法可以通过绑定window的message事件来监听发送跨文档消息传输内容。可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

发送端:

targetwindow.postMessage(message, targetOrigin, [transfer]):

  • targetwindow:窗口的引用,如全局window对象,iframe.contentWindow、window.open 的返回值
  • message:需要发送到另一个窗口的信息,值为字符串或者数据对象(html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果)。
  • targetOrigin: 字符串参数,协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/",这个参数是为了安全考虑。
  • transfer:可选参数

postMessage的兼容性官方API文档

接收端:

在 postMessage() 执行成功后,会在目标窗口触发一个 MessageEvent 事件,我们需要用AddEventListener来监听其它窗口通过 postMessage 发送过来的消息,receiveMessage 为监听函数,该函数的参数 event 含有以下三个属性:

  • data:postMessage函数的message参数值
  • origin:postMessage函数调用者origin
  • source:对发送消息的窗口对象的引用

来看例子:A页面向B页面传递信息"土豆土豆,我是地瓜",B页面回应"地瓜地瓜,我是土豆"

// a.html
<iframe id="iframe" src="http://www.test.com/b.html"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = "土豆土豆,我是地瓜";
        // 向b传送跨域数据
        iframe.contentWindow.postMessage(data, 'http://www.test.com');
    };

    // 接受b返回数据
    window.addEventListener('message', function(e) {
        alert('data from b ---> ' + e.data);
    }, false);
</script>
// b.html
<script>
    // 接收a的数据
    window.addEventListener('message', function(e) {
        var data = JSON.parse(e.data);
        if (data.origin !== "http://www.test.com") {
            return;
        };
        // 处理后再发回a
        window.parent.postMessage('地瓜地瓜,我是土豆','http://www.test.com');
    }, false);
</script>

postMessage跨域的精髓就是不管你俩是否跨域,只要你俩能跟一个window扯上关系,那就能传消息,收到消息就可以处理下一步事物。

两个同域的window如果不是同一个window也是不能收发消息的。

窗口间的通讯只能是一个页面嵌入多个iframe,多个iframe因为能靠主页面找到同一个window,才能做到多页面消息传递。

第六种:WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

WebSocket是高级api,不兼容,但是可以使用socket.io这个库,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000'); //ws协议是webSocket自己创造的
    socket.onopen = function () {
      socket.send('hello world');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//你好,再见。 // 接收服务器返回的数据
    }
</script>
// server.js
// 一般起的服务是http服务,但是websocket需要起ws服务,ws是webSocket自己定义的。需要使用ws协议,就需要装一个ws包。
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) { //先连接
  ws.on('message', function (data) { // message监听客户端发来的消息
    console.log(data); // hello world
    ws.send('你好,再见。')
  });
})

第七种:iframe跨域

document.domain + iframe跨域

此方法仅限于主域形同,子域不同,也就是二级域名不同的情况下,包括端口都要一致,就像news.baidu.com 和 map.baidu.com,他俩主域虽然都是baidu.com,但是子域名一个是a一个是b,就适用于这种方法,非此种形式的跨域就不能用此方法了。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

举个例子,比如B页面获取A页面中的user

// A页面  http://news.baidu.com/index.html
<script>
    document.domain = "baidu.com";
    var test = "O(∩_∩)O哈哈~  我是A啊!"
</script>
// B页面 http://map.baidu.com/index.html
<iframe id="iframe" onload="load()" frameborder="0" src="http://news.baidu.com/index.html"></iframe>
<script>
    document.domain = "baidu.com";
    function load() {
        console.log('get js data from A': frame.contentWindow.test);
    };
</script>

location.hash + iframe跨域

页面域关系:
a.html所属域A:www.a.com
b.html所属域B:www.b.com

实现原理: a欲与b跨域相互通信,但a、b不属于同一个域,所以通过中间代理页面c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现:a.html给c.html传一个hash值,c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000,a与c不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但a与b同域,所以b可通过parent.parent访问a页面所有对象。

// a.html
  <iframe src="http://localhost:4000/c.html#happy"></iframe>
  <script>
    window.onhashchange = function () { //检测hash的变化
      console.log(location.hash);
    }
  </script>
// b.html
  <script>
    window.parent.parent.location.hash = location.hash 
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  </script>
// c.html
 console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#sad';
  document.body.appendChild(iframe);

window.name + iframe跨域

window.name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)或者说 window.name属性值在文档刷新后依旧存在的能力。同理,在iframe中,即使url在变化,iframe中的window.name也是一个固定的值。利用此原理,我们就可以实现跨域了,同时也是安全操作。

// A.html 
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true;
    let iframe = document.getElementById('iframe');
    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    function load() {
      if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      } else {
        // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
        console.log(iframe.contentWindow.name);
        // 释放内存,销毁iframe
        iframe.contentWindow.close();
        document.getElementsByTagName("head")[0].removeChild(iframe);
      };
    };
  </script>

Proxy.html为代理页,只起到window.name转同域作用,与A.html同域,内容为空即可

// B.html
<script>
    window.name = "我叫高佳睿,今年十八岁!"
</script>

总结

JSNOP(只支持GET请求,支持老式IE浏览器)适合加载不同域名的img、css、js等静态资源

CORS(支持所有类型HTTP请求,但IE10试下浏览器不支持),适合做AJAX跨域请求

Nginx反向代理,适合做前后端分离的前端项目调用后端接口。

Nodejs中间件代理和Nginx原理也都一样,都是搭建一个服务器,直接在服务端代理请求HTTP接口。

iframe 相关三种之类的跨域,也都有不同适用的场景

PostMessage、WebSocket 都是H5 新特性,兼容性不是很好,适用于IE10以上主流浏览器


努力,只为遇见更好的自己!