跨域(非同源策略请求)常见的实现方式

我心飞翔 分类:javascript

跨域(非同源策略请求)常见的实现方式

作者:仙客

输入图片说明

什么是跨域

首先 在了解跨域,需要先知道什么是同源策略

同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。

限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制

浏览器只能访问同域名的内容,如果协议、域名、端口号有一个不同,那就会产生跨域。

跨域是如何产生的呢?

例如:一个域的页面去请求另一个域的资源(A 域的页面去请求 B 域的资源)

我们再来看一下跨域引起的报错提示:
输入图片说明

解决跨域的几种方式?

1.jsonp跨域

JSONP 实现方式

利用 script 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP 请求一定需要对方的服务器做支持才可以。
JSONP 都是 GET 请求的,不存在其他的请求方式和同步请求,且 jQuery 默认就会给 JSONP 的请求清除缓存。

//JS 的 JSONP 方式
let script = document.createElement("script");
script.type = "text/javascript";
// 传一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = "http://www.domain2.com:8080/login?callback=handleCallback";
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
  alert(JSON.stringify(res));
}
 
//jQuery 的 jsonp 形式
$.ajax({
  url: "http://www.domain2.com:8080/login",
  dataType: "jsonp",
  type: "get",
  jsonpCallback: "handleCallback",
  jsonp: "callback",
  success: function (data) {
    console.log(data);
  },
});
 

为什么 jsonp 只支持 get 请求?

JSONP 是请求一段 JS 脚本,把执行这段脚本的结果当做数据。所以,你能 POST 一段通过 script 标签引入的脚本吗?(如果看过 JSONP 库的源码就知道,常见的实现代码其实就是 document.createElement(‘script’) 生成一个 script 标签,然后插 body 里而已。在这里根本没有设置请求格式的余地)。

所以 JSONP 的实现原理就是创建一个 script 标签, 再把需要请求的 api 地址放到 src 里. 这个请求只能用 GET 方法, 不可能是 POST

2.cors跨域

什么是cors

CORS(Cross-Origin Resource Sharing)既跨域资源共享,它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而解决了 AJAX 只能同源使用的限制。
实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

简单请求

(1)工作中比较常见 【简单请求】:
方法为:GET, HEAD,POST

(2)请求 header 里面:
无自定义头
Content-Type 为以下几种:
text/plain, multipart/form-data , application/x-www-form-urlencoded

实现方式

//页面
axios.get("http://127.0.0.1:8888/index").then(function (data) {
  console.log(data);
});
 
//服务器端
//设置Access-Control-Allow-Origin 开启 CORS。
//该属性表示哪 些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
let express = require("express");
let app = express();

//配置中间件 来设置请求头 可以跨域
app.use(function (req, res, next) {
  res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500");
  next();
});
app.get("/index", function (req, res) {
  res.send("hello");
});
app.listen(8888, function () {
  console.log("8888端口监听成功");
});
 

结果:
输入图片说明

非简单请求

工作中常见的【非简单请求】有:请求方法是 PUT 或 DELETE,或者 Content-Type 字段的类型是 application/json。

在进行非简单请求之前,浏览器会在正式请求之前发送一次预检请求,这就是有时候我们会在控制台中看到的 option 请求,就是说,正式请求之前,浏览器会去问服务端我这个地址能不能访问你,如果可以,浏览器才会发送正式的请求,否则报错。

如何识别“预检”请求

“预检”请求用的请求方法是 OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是 Origin,表示请求来自哪个源。

除了 Origin 字段,“预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method

    该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT。

  • Access-Control-Request-Headers

    该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header。

    服务器收到“预检”请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应。

fetch 跨域请求

当使用 fetch 发起跨域请求
const body = {name:"Good boy"};
 fetch("http://localhost:8000/API",{
     headers:{
         'content-type':'application/json'
     }
     method:'POST',
     body: JSON.stringify(body)
}).then(response =>
     response.json().then(json => ({ json, response }))
).then(({ json, response }) => {
    if (!response.ok) {
      return Promise.reject(json);
    }
    return json;
}).then(
    response => response,
    error => error
  );
 
fetch 的 mode 配置项

如果服务器不支持 cors,fetch 提供了三种模式

针对跨域请求,cors 模式是常见跨域请求实现,但是 fetch 自带的 no-cors 跨域请求模式则较为陌生,该模式有一个比较明显的特点:

该模式允许浏览器发送本次跨域请求,但是不能访问响应返回的内容,这也是其 response type 为 opaque 透明的原因。

模式 描述
same-origin 该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个 error 告知不能跨域;其对应的 response type 为 basic。
cors 该模式支持跨域请求,顾名思义它是以 CORS 的形式跨域;当然该模式也可以同域请求不需要后端额外的 CORS 支持;其对应的 response type 为 cors
no-cors 该模式用于跨域请求但是服务器不带 CORS 响应头,也就是服务端不支持 CORS;这也是 fetch 的特殊跨域请求方式;其对应的 response type 为 opaque。

ajax 跨域请求 CORS 的使用

/*
服务器设置请求头:Access-Control-Allow-Origin启用CORS。
接下来直接上代码
*/

//初始化ajax对象
var xhr = new XMLHttpRequest();
//连接地址,准备数据
xhr.open("方式","地址","是否为异步");
//接收数据完成触发的事件
xhr.onload =function(){}
//发送数据
xhr.send();

 

fetch 和 ajax 的主要区别

  1. ajax 适用于 MVC 框架,但是 fetch 适用于 MVVM 框架
  2. fetch 与 ajax 相比较写法更方便
  3. .fetch 只对网络请求报错,400 500 都当做成功的请求,需要封装去进行处理

cookie 的跨域传参

首先 需要在服务端返回的头部信息中增加:response.setHeader("Access-Control-Allow-Origin","b.test.com");

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

 document.cookie="pin=test;domain=test.com;";

//js:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

//jquery
    $.ajax({
       url:_url,
       type:"get",
       data:"",
       dataType:"json",
       xhrFields: {
          withCredentials: true
  //开发者必须在AJAX请求中打开withCredentials属性.否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
       },
       crossDomain: true,
    })
 

使用 cors 跨域 注意事项

  • 如果要带 cookie(即设置了 withCredentials: true),那么 Access-Control-Allow-Origin 不能用通配符(‘*’),得用具体的 Origin
  • IE6/IE7 不支持 Access-Control-Allow-Origin
  • 虽然可以通过设置响应头和响应方式等支持非简单请求,但是不到万不得已的情况,不能允许客户端发送非简单请求.因为非简单请求会使服务器比简单请求的多一倍的压力.
  • 如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。
  • Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传
  • 原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

3.iframe+document.domain 跨域

子域中无法获取父域的数据的时候就可以利用 document.domain 都设置成相同的域名就可以完成,但是要注意的是,document.domain 的设置是有限制的,我们把 document.domain 设置成自身或者更高一级的父域,且主域必须相同

实现方式

//父页面 127.0.0.1:5500
    <iframe
      src="http://127.0.0.1:5501/index.html"
      id="iframepage"
      width="100%"
      height="100%"
      frameborder="0"
      scrolling="yes"
      onLoad="getData()"
    ></iframe>
    <script>
      /**
       * 使用document.domain解决iframe父子模块跨域的问题
       */
       window.parentDate = {
        name: "hello world!",
        age: 18,
      };
      let parentDomain = window.location.hostname;
      document.domain = parentDomain;
      function getData() {
        console.log("子域传递给父域的数据", top.childData);
      }
    </script>
 
//子页面 127.0.0.1:5501
<h1>子页面</h1>
<script>
      let childDomain = document.domain;
      document.domain = childDomain;
      top.childData = {
        name: "你好世界!",
        age: 26,
      };
      let parentDate = top.parentDate;
      console.log("从父域获取到的数据", parentDate);
</script>
 

注意:不过如果你想在www.example.com/a.html页面中通过…

4.iframe+location.hash跨域

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

<!--a.html  http://127.0.0.1:5501/a.html --> 
<iframe id="iframe" src="http://127.0.0.1:5501/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

 
<!--b.html  http://127.0.0.1:5501/b.html--> 
<iframe id="iframe" src="http://127.0.0.1:5500/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

 
<!--c.html  http://127.0.0.1:5500/c.html--> 
<script>
  // 监听b.html传来的hash值
 window.onhashchange = function () {
  // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

 

5.iframe+window.name跨域

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

//a.html
let proxy = (url, callback)=> {
    let state = 0;
    let iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe); 
};
   // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
// 请求跨域b页面数据
proxy('http://127.0.0.1:5501/b.html', function(data){
    alert(data);
});

 
<!--b.html-->
<script>
    window.name = '访问b.html';
</script>

 

6.postMessage 跨域

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

语法

postMessage(message, targetOrigin, [transfer]);

参数 说明
message 将要发送到其他 window 的数据
targetOrigin 指定那些窗口能接收到消息事件,其值可以是字符串 “*” 表示无限制,或者是一个 URI。
transfer 是一串和 message 同时传递的 Transferable 对象,这些对象的所有权将被转移给消息的接收方,而发送方将不再保留所有权

实现方式

//A页面
<iframe src="http://127.0.0.1:8888/B.html" id="ifm"></iframe>
<input type="button" value="ok" onclick = "run()">
<script>
let ifm=document.getElementById('ifm');
function run(){
  ifm.contentWindow.postMessage({name:6666},'http://127.0.0.1:8888')
}
</script>
 
//B页面
<div>111111</div>
<script>
window.addEventListener('message',function(e){
console.log(e.data)
})
</script>
 

7.webpack 的 devServer.proxy 处理跨域

const path = require("path");
module.exports = {
  entry: "./index.js",
  devServer: {
    host: "0.0.0.0",
    port: 8080,
    proxy: {
      "/api": {
        target: "https://www.xxx.com",
        secure: false, // 协议是https的时候必须要写
        changeOrigin: true, //changeOrigin: true后才可跨域
      },
    },
  },
  // ···省略了···
};

//设置完毕之后,重启一下服务,根据实际情况重启项目: npm run serve或者是npm run dev。
 

为什么反向代理可以跨域

即普通情况下,a.com 是无法向b.com 发出请求的。但是如果你启动一个 server, a.com 向 a.com/api 发出请求,server 接收到 a.com/api 的请求后,向 b.com 发出请求,拿到 b.com 返回的结果再丢回去,这个时候就不存在跨域了,因为在服务器端是不存在跨域限制的。此时服务器就起到了一层代理的功能。a.com 只需要跟a.com/api 打交道。
反向代理一般是针对服务器来说的,比如你访问 www.baidu.com ,此时访问的其实是一台代理服务器,代理服务器把请求分发给多台服务器,这样起到了一层隔绝的作用。因此反向代理是对服务器而言的,正向代理是对客户端而言的。

changeOrigin 如何解决跨域

从源码里可以清晰的看到,设置了changeOrigin只是更改了 request 请求中的 host

输入图片说明

小结

  1. JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是只支持 get 请求,不安全。
  2. JSONP 是 GET 形式,承载的信息量有限,所以信息量较大时 CORS 是不二选择。
  3. postMessage 设置跨域,若是发送数据段的第二个参数使用的是“*”,那么表示这个数据可以被任何域获取到,这时候构造 window.addEventListener 来接受数据就可以,伪造获取数据端,
  4. 如今前端开发使用 webpack-dev-server 作为本地服务器已经是基本配置,加上 proxy 功能可以很好的应对 SSL、跨域、线上环境切换等需求。Vue CLI 3 里也做了相应的集成,用起来很方便。

回复

我来回复
  • 暂无回复内容