跨域 – 九种方案和原理总结

一、同源策略

  • 同源策略指的是浏览器对不同源的脚本或者文本的访问方式进行的限制

  • URL由协议、域名、端口和路径组成,如果两个URL的协议、域名和端口相同,则表示他们同源。相反,只要协议,域名,端口有任何一个的不同,就被当作是跨域

二、为什么浏览器不支持跨域

  • 假设现在有a.com和b.com两个域,如果没有同源策略,那么当用户在访问a.com时,a.com的一段脚本就可以在不加载b.com的页面而随意修改或者获取b.com上面的内容。这样将会导致b.com页面的页面发生混乱,甚至信息被获取,包括服务器端发来的session。因为浏览器的同源策略,保证来至不同源的对象不会互相干扰,保证了访问页面最基本的安全。

  • 像本地存储的数据 cookie、LocalStorage,都是有同源策略的,目的就是防止别其他源窃取, DOM元素也有同源策略,包括iframe

三、jsonp

jsonp 跨域原理

jsonp 是利用script标签自身的跨域能力,像img、script,这种标签如果有相应的src属性,那么便会发起一个htttp请求来请求相应的资源,如果有script标签对应的路径是一个js文件,那么在下载完毕这个js之后会马上执行

jsonp 缺点

  1. 只能发送get请求。因为script只能发送get请求
  2. 需要后台配合。此种请求方式应该前后端配合,将返回结果包装成callback(result)的形式,因为script标签下载完毕之后会立即执行,如果返回的是一个Json,直接执行是要报错的
  3. 不安全 xss攻击 不采用

jsonp 实现

server.js (利用 express 编写一个后端接口)

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

app.get('/say',function (req,res) {
  let {wd,cb} = req.query;
  console.log(wd); 
  res.end(`${cb}('我不爱你')`) 
})
app.listen(3000);
复制代码

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>封装 jsonp</title>
</head>

<body>
  <script>
    // cb 是回调函数的意思
    function jsonp({ url, params, cb }) {
      return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        // 创建全局函数
        window[cb] = function (data) {
          resolve(data);
          // 删除绑定在window上的cb函数
          delete window[cb]
          // 删除标签
          document.body.removeChild(script);
        }
        params = { ...params, cb } // 目的是转成这种格式 wd=b&cb=show
        let arrs = [];
        for (let key in params) {
          arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
      });
    }
    // 缺点:
    // 1、只能发送get请求 不支持post put delete
    // 2、不安全 xss攻击  不采用
    jsonp({
      url: 'http://localhost:3000/say',
      params: { wd: '我爱你' },
      cb: 'show'
    }).then(data => {
      console.log(data);
    });
  </script>

</body>

</html>
复制代码

四、cors

cors 是什么

  • CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),是目前比较规范的服务端解决跨域的方案,由服务端进行控制是否允许跨域,可以自定义配置规则

cors 缺点

  • cors 解决跨域会产生额外的请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求, 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错

cors 实战

server1.js (为 index.html 启动一个本地服务)

let express = require('express');
let app = express();
// 设置静态文件目录 http://localhost:3000/index.html 就能被启动
app.use(express.static(__dirname));
app.listen(3000);


// 知识补充:
// app.use() : 将指定的一个或多个中间件函数安装在指定的路径上:当所请求路径的基数匹配时,将执行中间件函数path
// express.static() : 为了提供对静态资源文件(图片,css,js)的服务
// __dirname: 被执行 js 文件的绝对路径
复制代码

server2.js (编写后端接口,启动一个后端的本地服务)

let express = require('express');
let app = express();
let whitList = ['http://localhost:3000'] // 白名单
app.use(function (req, res, next) {
  let origin = req.headers.origin;
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin);
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name');
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT');
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true);
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6);
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name');
    if (req.method === 'OPTIONS') {
      res.end(); // OPTIONS请求不做任何处理
    }
  }
  next();
});
app.put('/getData', function (req, res) {
  console.log(req.headers);
  res.setHeader('name', 'jw');
  res.end("我不爱你")
})
app.get('/getData', function (req, res) {
  console.log(req.headers);
  res.end("我不爱你")
})
app.use(express.static(__dirname));
app.listen(4000);
复制代码

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>cors 跨域 (服务端解决方案)</title>
</head>

<body>
  <script>
    let xhr = new XMLHttpRequest;
    document.cookie = 'name=zfpx';
    xhr.withCredentials = true;
    xhr.open('PUT', 'http://localhost:4000/getData', true); // true 表示异步
    xhr.setRequestHeader('name', 'zfpx');
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
          console.log(xhr.response);
          console.log(xhr.getResponseHeader('name'));
        }
      }
    }
    xhr.send();
  </script>
</body>

</html>
复制代码

五、postMesssage

postMesssage 作用

  • postMessage 是 HTML5 中新增的方法,可以实现跨文档消息传输,该方法可以通过绑定 window 的message事件来监听发送跨文档消息传输内容

  • postMessage 可以解决页面与嵌套的 iframe 消息传递的跨域问题

postMesssage 实战

a.js (启动一个端口为3000的本地服务)

let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
复制代码

b.js (启动一个端口为4000的本地服务)

let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(4000);
复制代码

a.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>postMesssage 两页面之间通讯跨域</title>
</head>

<body>
  <!-- onload 事件在 iframe 载入完成后被触发,也就是 iframe 加载完成的回调 -->
  <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
  <script>
    function load() {
      // 获取 frame 的 window 并给自己发送信息
      let frame = document.getElementById('frame');
      frame.contentWindow.postMessage('我爱你', 'http://localhost:4000');
      window.onmessage = function (e) {
        console.log(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">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <script>
    // 接收信息时候触发
    window.onmessage = function (e) {
      console.log(e.data);
      // 获取 e 的来源并给他自己发信息
      e.source.postMessage('我不爱你', e.origin)
    }
  </script>
</body>

</html>
复制代码

六、window.name

window.name 解决跨域原理

  • window.name 指的是当前窗口的名字,同一个浏览器窗口或同一个 iframe 载入的页面共享一个window.name

  • window.name 的 name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)

  • 父页面能够通过 iframe 的 contentWindow 的属性,访问到子页面的内容(不跨域的情况下),意思是利用 iframe.contentWindow.name 是能够获取到 iframe 窗口内同一域名页面的 window.name。

window.name 实战

一共三个页面,a.html 和 b.html 是同域的,http://localhost:3000,c.html 是独立的 http://localhost:4000,现在 a.html 获取 c.html 的数据,a.html 先引用 c.html, c.html 把值放到 window.name,a.html 再把引用的地址改到 b.html

a.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>window.name 实现跨域</title>
</head>

<body>
  <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true
    function load() {
      if (first) {
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      } else {
        console.log(iframe.contentWindow.name);
      }
    }
  </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">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script>
    window.name = '我不爱你'  
  </script>
</body>
</html>
复制代码

七、location.hash

location.hash 解决跨域原理

  • 原理是利用 location.hash 来进行传值。在 url 中改变hash值并不会导致页面刷新,所以可以利用hash值来进行数据传递

location.hash 实战

  • 案例:一共三个页面,a.html 和 b.html 是同域的,c.html 是独立的,现在 a.html 想获取 c.html 传递的数据
  • 思路:a.html 通过 iframe 引入 c.html ,并给 c.html 传一个hash值,c.html 收到hash值后创建 iframe 引入 b.html,把hash值传给 b.html,b.html 将自己的hash值放在 a.html

a.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>hash 解决跨域</title>
</head>

<body>
  <iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
  <script>
    // 使用 hashchange 事件接收来自 b.html 设置给 a.html 的 hash 值
    window.onhashchange = function () {
      console.log(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">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
<script>
  console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#idontloveyou';
  document.body.appendChild(iframe);
</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">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script>
    window.parent.parent.location.hash = location.hash  
  </script>
</body>

</html>
复制代码

八、document.domain

document.domain 跨域原理

  • document.domain 存放的是载入文档的服务器的主机名,可以手动设置这个属性,不过是有限制的,只能设置成当前域名或者上级的域名,并且必须要包含一个.号,也就是说不能直接设置成顶级域名。例如:id.qq.com,可以设置成qq.com,但是不能设置成com

  • 具有相同 document.domain 的页面,就相当于是处在同域名的服务器上,如果协议和端口号也是一致,那它们之间就可以跨域访问数据

  • 所以不是万能的跨域方式,大多使用于同一公司不同产品间获取数据,必须是一级域名和二级域名的关系,如 music.baidu.com  与 video.baidu.com 之间

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">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>domain 解决跨域</title>
</head>

<body>
  <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>

</html>
复制代码

b.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

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

</html>
复制代码

九、websocket

websocket 解析

WebSocket 没有跨域限制,高级 API(不兼容),想要兼容低版本浏览器,可以使用 socket.io 的库,WebSocket 与 HTTP 内部都是基于 TCP 协议,区别在于 HTTP 是单向的(单双工),WebSocket 是双向的(全双工),协议是 ws:// 和 wss:// 对应 http:// 和 https://,因为没有跨域限制,所以使用 file:// 协议也可以进行通信

websocket 实战

node 服务中使用 WebSocket,需要安装对应依赖:npm install ws –save

server.js

const express = require("express");
let app = express();

// 引入 webSocket
const WebSocket = require("ws");
// 创建连接,端口号与前端相对应
let wss = new WebSocket.Server({ port: 3000 });

// 监听连接
wss.on("connection", function(ws) {
    // 监听消息
    ws.on("message", function(data) {
        // 打印消息
        console.log(data); // I love you
        // 发送消息
        ws.send("I love you, too");
    });
});

复制代码

socket.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>页面</title>
</head>
<body>
    <script>
        // 创建 webSocket
        let socket = new WebSocket('ws://localhost:3000');
        // 连接上触发
        socket.onopen = function () {
            socket.send('I love you');
        }
        // 收到消息触发
        socket.onmessage = function (e) {
            // 打印收到的数据
            console.log(e.data); 
        }
    </script>
</body>
</html>
复制代码

十、http-proxy

  • webpack-dev-server 是目前前端实现本地跨域最流行的方法,webpack-dev-server 的实现方法本质上是对 http-proxy 的封装
  • webpack-dev-server 配置代理非常的方便,只需要条件一个proxy属性,然后配置相关的参数就可以了

webpack.config.js

var webpack = require('webpack');
var WebpackDevServer = require("webpack-dev-server");
var path = require('path');
var CURRENT_PATH = path.resolve(__dirname); // 获取到当前目录
var ROOT_PATH = path.join(__dirname, '../'); // 项目根目录
var MODULES_PATH = path.join(ROOT_PATH, './node_modules'); // node包目录
var BUILD_PATH = path.join(ROOT_PATH, './dist'); // 最后输出放置公共资源的目录
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {

  //项目的文件夹 可以直接用文件夹名称 默认会找index.js ,也可以确定是哪个文件名字
  entry: {
    app: ['./src/js/index.js'],
    vendors: ['jquery', 'moment'], //需要打包的第三方插件
    // login:['./src/css/login.less']
  },

  //输出的文件名,合并以后的js会命名为bundle.js
  output: {
    path: path.join(__dirname, "dist/"),
    publicPath: "http://localhost:8088/dist/",
    filename: "bundle_[name].js"
  },
  devServer: {
    historyApiFallback: true,
    contentBase: "./",
    quiet: false, //控制台中不输出打包的信息
    noInfo: false,
    hot: true, //开启热点
    inline: true, //开启页面自动刷新
    lazy: false, //不启动懒加载
    progress: true, //显示打包的进度
    watchOptions: {
      aggregateTimeout: 300
    },
    port: '8088', //设置端口号
    //其实很简单的,只要配置这个参数就可以了
    proxy: {
      '/index.php': {
        target: 'http://localhost:80/index.php',
        secure: false
      }
    }
  } 
};
复制代码

代码解析:这里本地的端口号设置为了:8088,如果这个时候接口放在了端口为80的服务器上,并且我们请求的接口url如下:http://localhost:80/index.php,
这个时候只需要在 proxy 使用正则表达式匹配 /index.php ,然后 匹配到 转向 target 设置的目标接口;这个时候使用ajax请求接口就不要写上目标接口的域名,只需要写上index.php就可以了

十一、nginx

nginx 解决跨域原理

本质就是启动一个nginx服务器,将 server_name 设置为 aaa ,然后设置相应的 location 以拦截前端需要跨域的请求,最后将请求代理回 bbb 。如下面的配置:

## 配置反向代理的参数
server {
    listen    8080;
    server_name aaa

    ## 1. 用户访问 http://aaa.com,则反向代理到 https://bbb.com
    location / {
        proxy_pass  https://bbb.com;
        proxy_redirect     off;
        proxy_set_header   Host             $host;        # 传递域名
        proxy_set_header   X-Real-IP        $remote_addr; # 传递ip
        proxy_set_header   X-Scheme         $scheme;      # 传递协议
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}
复制代码
复制代码

这样可以完美绕过浏览器的同源策略:aaa.com访问nginxbbb.com属于同源访问,而nginx对服务端转发的请求不会触发浏览器的同源策略

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享