一文掌握9大跨域解决方案

作者:Hanpeng_Chen

公众号:前端极客技术

文章首发个人博客:代码视界-一文掌握9大跨域解决方案

什么是跨域

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源。通常我们讲的跨域,是由浏览器同源策略限制的一类请求场景。

同源策略

同源指的是两个URL的协议、域名和端口三者都相同,即使两个不同 的域名指向相同的IP地址,也非同源。

同源策略(SOP:Same origin policy)是浏览器的一套基础的安全策略制约,用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。

同源策略主要表现在DOM、web数据和网络三个层面。

DOM层面: 同源策略限制了来自不同源的JavaScript脚本对当前DOM对象读和写的操作。

数据层面: 同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。

网络层面: 同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。

跨域的解决方法

主要有以下九种解决方案:

  • JSONP
  • CORS(跨域资源共享,最常用)
  • postMessage + iframe
  • document.domain + iframe
  • window.name + iframe
  • location.hash + iframe
  • WebSocket
  • nginx代理跨域
  • nodejs中间件代理跨域

JSONP

浏览器只对XMLHttpRequest请求有同源请求限制,而对script标签src属性、link标签ref属性和img标签src属性没有这这种限制,利用这个“漏洞”就可以很好的解决跨域请求。JSONP就是利用了script标签无同源限制的特点来实现的。当然需要后端服务器的配合,返回一个合法的JS脚本,一般是一条调用js函数的语句,数据作为函数的入参。

我们通过下面的例子来简单展示如何通过JSONP来解决跨域。

const express = require('express');
const app = express();

app.get('/jsonp', (req, res) => {
  let {wd, cb} = req.query;
  console.log(wd, cb);
  res.end(`${cb}('接口返回测试数据')`);
})

app.listen(3000, () => {
  console.log('app listening on port 3000');
})
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>jsonp解决跨域</title>
</head>
<body>
  <script>
    function jsonp({url, params, cb}) {
      return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        window[cb] = function(data) {
          resolve(data)
          document.body.removeChild(script);
        }
        params = {...params, cb};
        let arrs = [];
        for (let key in params) {
          arrs.push(`${key}=${params[key]}`)
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
      })
    }
    jsonp({
      url: 'http://localhost:3000/jsonp',
      params: {
        wd: 'b'
      },
      cb: 'show' // 回调函数名
    }).then(data => {
      console.log(data)
    })
  </script>
</body>
</html>

JSONP有以下几个缺点:

  • 只支持GET请求而不支持POST等其它类型的HTTP请求
  • 只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
  • JSONP在调用失败的时候不会返回各种HTTP状态码。
  • 不安全。万一提供jsonp的服务存在页面注入漏洞,容易遭受xss攻击。

CORS(跨域资源共享,最常用)

跨源资源共享 (CORS,Cross-origin resource sharing)是一种基于HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

接下来我们通过一个简单的例子来一起看下CORS的用法:

const express = require('express');
const app = express();

let whiteList = ['http://localhost:3000']

app.use(function (req, res, next) {
  console.log(req.headers);
  let origin = req.headers.origin;
  if (whiteList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin); // 接受origin这个域名的请求
    res.setHeader('Access-Control-Allow-Headers', 'x-name'); // 表明服务器支持的所有头信息字段
  }
  next();
})

app.get('/getData', (req, res) => {
  res.end('接口返回测试数据');
})

app.use(express.static(__dirname));

app.listen(4000)

我们通过http://localhost:3000/index.html打开下面的HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CORS</title>
</head>
<body>
  <p>cors test html</p>

  <script>
    let xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:4000/getData', true)
    xhr.setRequestHeader('x-name', 'test')
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
          console.log(xhr.response)
        }
      }
    }
    xhr.send();
  </script>
</body>
</html>

该方法主要是后端服务接口在响应报文中设置相应的正确CORS响应头。这也是目前最常用的解决跨域问题的方法。

更多详细内容可以查看阮一峰老师的文章:https://www.ruanyifeng.com/blog/2016/04/cors.html

postMessage + iframe

postMessage是H5引入的一个API,该方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

我们起两个服务,a.html在http://localhost:3000上,b.html在http://localhost:4000上。

两个HTML代码如下:

a.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>a page</p>
  <span id="message"></span>
  <iframe src="http://localhost:4000/b.html" frameborder="1" id="frame"></iframe>

  <script>
    window.onload = function() {
      let frame = document.getElementById('frame');
      frame.contentWindow.postMessage('测试消息', 'http://localhost:4000/b.html')
    }
    window.onmessage = function (e) {
      // 接受消息
      document.getElementById('message').innerHTML = `收到${e.origin}的消息:${e.data}`;
    }
  </script>
</body>
</html>

b.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>b page</p>
  <span id="message"></span>

  <script>
    window.onmessage = function (e) {
      // 接受消息
      console.log('b page onmessage', e.data);
      document.getElementById('message').innerHTML = `收到${e.origin}的消息:${e.data}`;
      top.postMessage('b 页面收到消息', 'http://localhost:3000/a.html');
    }
  </script>
</body>
</html>

window.name

页面在浏览器端展示时,我们能拿到全局变量window,window变量有个name属性,该属性具有下面几个特征:

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

我们准备三个页面:a.html和b.html在http://localhost:3000上,c.html在http://localhost:4000上。

目标:要在a页面获取c页面发送的数据

思路:a先引用c,c把值放到window.name,把a的引用地址改到b

代码如下:
a.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>a page</p>
  <iframe src="http://localhost:4000/c.html" frameborder="0" id="frame" onload="load()"></iframe>

  <script>
    let first = true;
    function load() {
      if (first) {
        let frame = document.getElementById('frame');
        frame.src = 'http://localhost:3000/b.html';
        first = false;
      } else {
        console.log(frame.contentWindow.name);
      }
    }
  </script>
</body>
</html>

b.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <p>b page</p>
</body>
</html>

c.html

<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    window.name = 'window.name实现跨域'
  </script>
</body>
</html>

location.hash

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

我们准备三个页面:a.html和b.html在http://localhost:3000上,c.html在http://localhost:4000上。

目标:在a页面获取c页面发送的数据。

思路:a给c传一个hash值,c收到hash值后,c把hash值传递给b,b将结果放到a的hash值中

代码如下:

a.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe src="http://localhost:4000/c.html#testData" frameborder="0" id="frame" onload="load()"></iframe>

  <script>
    window.onhashchange = function() {
      console.log(location.hash);
    }
  </script>
</body>
</html>

b.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    window.parent.parent.location.hash = location.hash
  </script>
</body>
</html>

c.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    console.log(location.hash);
    let iframe = document.createElement('iframe');
    iframe.src = 'http://localhost:3000/b.html#cPageToBData';
  </script>
</body>
</html>

document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。

只需要给页面添加 document.domain =‘test.com’ 表示二级域名都相同就可以实现跨域。

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

a.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe src="http://b.test.com/b.html" frameborder="0" id="frame" onload="load()"></iframe>

  <script>
    document.domain = 'test.com'
    function load() {
      console.log(frame.contentWindow.data);
    }
  </script>
</body>
</html>

b.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    document.domain = 'test.com';
    var data = '这是b页面的数据';
  </script>
</body>
</html>

WebSocket

WebSocket是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。

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

同源策略对WebSocket不适用。

我们来看个简单例子:本地socket.html向localhost:3000发送数据和接受数据:

// socket.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 高级API 不兼容,  socket.io 库
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function() {
      socket.send('test data');
    }
    socket.onmessage = function(e) {
      console.log(e.data);
    }
  </script>
</body>
</html>
// server.js
let express = require('express');
let app = express();

let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000})
wss.on('connection', function(ws) {
  ws.on('message', function(data) {
    console.log(data);
    ws.send('response data');
  })
})

nodejs中间件代理跨域

同源策略针对的是浏览器,如果是服务器向服务器请求则无需遵循同源策略。nodejs中间件代理跨域就是利用这个原理,将跨域请求发给代理服务器,代理服务器去做请求转发。

代理服务器需要做以下几个步骤:

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

我们来看下面的例子:本地index.html文件,通过代理服务器 localhost:3000 向目标服务器 localhost:4000 请求数据:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>http proxy</title>
</head>
<body>
  <p>http://localhost:3000/index.html</p>

  <script>
    let xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3000/getData', true)
    xhr.setRequestHeader('x-name', 'test')
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
          console.log(xhr.response)
        }
      }
    }
    xhr.send();
  </script>
</body>
</html>

proxyServer.js

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// 利用 Express 托管静态文件,可通过http://localhost:3000/index.html来访问index.html,实现跨域
app.use(express.static(__dirname));

// 代理服务器操作
// 设置允许跨域访问该服务
app.all('*', (req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});

// http-proxy-middleware
// 中间件 每个请求来之后 都会转发到 http://localhost:3001 后端服务器
app.use('/', createProxyMiddleware({target: 'http://localhost:4000', changeOrigin: true}))

app.listen(3000)

server.js

const express = require('express');
const app = express();

app.get('/getData', (req, res) => {
  res.end('nodejs中间件代理跨域 返回数据');
})

app.use(express.static(__dirname));

app.listen(4000)

nginx

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

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

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

nginx的配置简单示例如下:

// 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;
    }
}

文中示例代码cross-domain

如果你觉得这篇内容对你有帮助的话:

1、点赞支持下吧,让更多的人也能看到这篇内容

2、关注公众号:前端极客技术,我们一起学习一起进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值