跨域解决方法

Posted by violetks on January 13, 2020

什么是跨域?跨域请求资源的方法有哪些?

一、什么是跨域

由于浏览器的同源策略,当发送请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

当前页面 url 被请求页面 url 是否跨域 原因
http://www.test.com/ http://www.test.com/index.html 同源
http://www.test.com/ http://www.test.com/index.html 跨域 协议不同(http&https)
http://www.test.com/ http://www.baidu.com/ 跨域 主域名不同
http://www.test.com/ http://blog.test.com/ 跨域 子域名不同
http://www.test.com:8080/ http://www.test.com:80/ 跨域 端口号不同
http://www.test.com/ http://192.168.4.12/ 跨域 域名和域名对应 ip

二、同源策略限制的内容

  1. 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  2. 无法接触非同源网页的 DOM
  3. 无法向非同源地址发送 AJAX 请求

三、跨域解决方法

(1)JSONP
简单适用,兼容性好,缺点是只支持 get 请求,不支持 post 请求。
原理:ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链接却可以访问跨域的 JS 脚本,网页通过添加一个<script>元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

// index.html
function jsonp ({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    window[callback] = function(data) {
      resolve(data);
      document.body.removeChild(script);
    }
    params = { ...params, callback }; // name=xx&callback=show
    let arrs = [];
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`);
    }
    //创建一个 <script> 标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)
    script.src = `${url}?${arrs.join('&')}`;
    document.body.appendChild(script);
  })
}

jsonp({
  url: 'http://localhost:3000/login',
  params: { name: 'Ray' },
  callback: 'show'
}).then(data => {
  console.log(data)
})
//上面这段代码相当于向 http://localhost:3000/login?name=Ray&callback=show 这个地址请求数据

(2)后端在服务器上设置 CORS
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
在发送请求时,分为两种情况,简单请求和复杂请求。
简单请求:
只要同时满足以下两大条件,就属于简单请求。
条件1:使用GETHEADPOST方法之一。
条件2:Content-Type 的值是 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一。

复杂请求:
除了简单请求,就是复杂请求。
复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

例子:
vue-shop后端用node.js写的,在app.js文件中设置跨域和相应数据格式。

app.all('/api/*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, mytoken')
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, Authorization')
  res.setHeader('Content-Type', 'application/json;charset=utf-8')
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With')
  res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')  // 允许什么方法访问
  res.header('X-Powered-By', ' 3.2.1')
  if (req.method == 'OPTIONS') res.send(200)
  /* 让 options 请求快速返回 */ else next()
})

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

// 修改 nginx.conf
server {
  listen       80;
  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;
  }

  # 静态资源
  location /vue-shop/ {
      root   html;
      index  index.html index.htm;
      try_files $uri $uri/ /vue-shop/index.html;
  }
}

(4)Node.js 中间件代理
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:

  • 接收客户端请求
  • 将请求转发给服务器
  • 拿到服务器响应数据
  • 将响应转发给客户端

lTJ7OP.png

//本地文件 index.html,通过代理服务器 http://localhost:3000 向目标服务器 http://localhost:4000 请求数据。

// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
  $.ajax({
    url: 'http://localhost:3000',
    type: 'post',
    data: { name: 'xiamen', password: '123456' },
    contentType: 'application/json;charset=utf-8',
    success: function (result) {
      console.log(result) // {"title":"fontend","password":"123456"}
    },
    error: function (msg) {
      console.log(msg)
    }
  })
 </script>

// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,需要设置 CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http.request(
    {
      host: '127.0.0.1',
      port: 4000,
      url: '/',
      method: request.method,
      headers: request.headers
    },
    serverResponse => {
      // 第三步:收到服务器的响应
      var body = ''
      serverResponse.on('data', chunk => { body += chunk })
      serverResponse.on('end', () => {
        console.log('The data is ' + body)
        // 第四步:将响应结果转发给浏览器
        response.end(body)
      })
    }
  ).end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})

// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') response.end(JSON.stringify(data))
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})

(5)postMessage
postMessage是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:

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

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

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

参数解释:
message:将要发送到其他 window 的数据。
targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串“*”(表示无限制)或者一个 URL。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
transfer(可选):是一串和message同时传递的Transferable对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

例子:
调用postMessage方法实现父窗口http://test1.com向子窗口http://test2.com发消息(子窗口同样可以通过该方法发送消息给父窗口)。

// 父窗口打开一个子窗口
var openWindow = window.open('http://test2.com', 'title');

// 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的 url)
openWindow.postMessage('Nice to meet you!', 'http://test2.com');
// 子窗口监听 message 消息
window.addEventListener('message', function (e) {
  console.log(e.source); // e.source 发送消息的窗口
  console.log(e.origin); // e.origin 消息发向的网址
  console.log(e.data);   // e.data   发送的消息
}, false);

(6)WebSocket 协议
WebSocket是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和 HTTP 都是应用层协议,都基于 TCP 协议。但是WebSocket是一种双向通信协议,在建立连接之后,WebSocketserverclient都能主动向对方发送或接收数据。同时,WebSocket在建立连接时需要借助 HTTP 协议,连接建立好了之后clientserver之间的双向通信就与 HTTP 无关了。
原生 WebSocket API 使用起来不太方便,我们使用Socket.io,它很好地封装了WebSocket接口,提供了更简单、灵活的接口,也对不支持WebSocket的浏览器提供了向下兼容。

//本地文件 socket.html 向 localhost:3000 发送数据和接收数据
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('to server'); //向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data); //接收服务器返回的数据
    }
</script>

// server.js
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) {
    console.log(data);
    ws.send('success')
  });
})

(7)window.name + iframe
window.name属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

//其中 a.html 和 b.html 是同域的,都是 http://localhost:3000; 而 c.html 是 http://localhost:4000
// a.html(http://localhost:3000/a.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>

<script>
    let first = true;
    // onload 事件会触发 2 次,第 1 次加载跨域页,并留存数据于 window.name
    function load () {
      if (first) {
        // 第 1 次 onload(跨域页)成功后,切换到同域代理页面
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      } else {
        // 第 2 次 onload(同域b.html页)成功后,读取同域 window.name中数据
        console.log(iframe.contentWindow.name);
      }
    }
</script>

#b.html 为中间代理页 a.html 同域内容为空
// c.html(http://localhost:4000/c.html)
<script>
    window.name = 'Ray'
</script>

总结:通过iframesrc属性由外域转向本地域,跨域数据即由iframewindow.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

(8)location.hash + iframe
实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用iframelocation.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.html
 <iframe src="http://localhost:4000/c.html#msg1"></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#msg2';
 document.body.appendChild(iframe);

(9)document.domain + iframe
该方式只能用于二级域名相同的情况下,比如a.test.comb.test.com适用于该方式。只需要给页面添加document.domain ='test.com'表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过 JS 强制设置document.domain为基础主域,就实现了同域。

//页面 a.zf1.cn:3000/a.html 获取页面 b.zf1.cn:3000/b.html 中变量 num 的值

// a.html
<body>
  helloa
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>

  <script>
    document.domain = 'zf1.cn';
    function load () {
      console.log(frame.contentWindow.a);
    }
  </script>

</body>

// b.html
<body>
  hellob
  <script>
    document.domain = 'zf1.cn';
    var num = 100;
  </script>

</body>

四、总结

CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案。
JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
不管是 Node 中间件代理还是 Nginx 反向代理,主要是通过同源策略对服务器不加限制。
注:日常工作中,用得比较多的跨域方案是 CORS 和 Nginx 反向代理。