前端的跨域问题

前端跨域有几种方案

首先想到的方法:jsonp

一、为什么要跨域?

扩展出 同源策略 的概念

原文来自百度百科

  • 同源策略的定义

    同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能会受到影响。可以说 Web 是构建在同源策略基础上的,浏览器只是针对同源策略的一种实现。

  • 概念/知识点:

    • 同源策略,它是由 Netscape 提出的一个著名的安全策略。
    • 现在所有支持 JavaScript 的浏览器都会使用这个策略。
    • 所谓同源是指:*协议、域名、端口- 相同
    • 当一个浏览器的两个 tab 页中分别打开来 百度 和 谷歌 的页面,即检查是否同源,只有和百度同源的脚本才会执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
  • 同源策略的目的

    他可以帮助我们在浏览器里进行隔离,使不同源的代码不会相互的影响,是一种安全策略。

浏览器同源政策及其规避方法

原文来自阮一峰老师的网络日志 该文章讲解的较为详细,这里便不再搬运喽~,下面是我所需要摘出的笔记。

  • 举例来说 http://www.example.com/dir/page.html 这个网址,协议是 http:// ,域名是 www.example.com,端口是 80(默认端口可以省略),基于这个网址的同源示例如下:

    • http://www.example.com/dir2/other.html:同源
    • http://example.com/dir/other.html:不同源(域名不同)
    • http://v2.www.example.com/dir/other.html:不同源(域名不同)
    • http://www.example.com:81/dir/other.html:不同源(端口不同)
  • 概念/知识点

    • 一台服务器默认的端口就是 80 例:www.baidu.com 这个默认的就是 80 端口,所以不用加 :80
    • 二级域名和一级域名不是同源的,父的优先级永远比子级高 例:www.example.com 为一级域名,example.com 为二级域名
    • 同源的目的就是为了保证用户的信息安全,防止恶意的网站窃取数据。
    • 虽然有这个同源政策的存在,但是有很多的网站也实现了 cookie 的共享,比如说:在京东的网站浏览了一些产品,在你打开其他网站的时候会发现在该网站的广告栏区域展示的就是你刚在京东网站浏览产品的图片,实际上这种共享的方式在如今的商业广告中已是十分的普遍了,虽然浏览器的同源政策就是为了避免此类事情发生的安全策略。
    • 浏览器不同的域名不能访问相应的 cookie,但是内部的表单是没有限制的。
    • 浏览器规定:提交表单不受同源政策的限制,所以就有了相应的 *跨站攻击- ,跨站攻击就是说有些人可以不在你的网站里写一个 form 通过表单的 action 属性将地址写成你的网站的网址,之后进行表单的提交,这个是没有跨域政策的限制是可以正常的提交的,这样子不停的往你的这个网址去提交表单就形成了所谓的 “跨站攻击” 。
    • 虽然浏览器有这个同源策略,但是对于网民的我们而言,还是没有什么隐私、安全可言的,因为我们在百度或者是其他的网站搜索或者是查看了什么东西之后,在别的网站的广告位置还是会看到搜索的产品的图片的~
  • 限制的范围:如果非同源,共有三种行为受到限制

    • Cookie、 LocalStorageIndexedDB 无法获取。
    • DOM 无法获得
    • AJAX 请求不能发送
  • 概念/知识点

    • Session Storage 如果接触过后端的话就会知道,这是一个 “会话间” 的存储机制,浏览器一关这个东西就没了
    • Cookie 一般就是用来存储一些用户名、密码之类的东西,这个存储的大小是浏览器的不同,大小也是不同的,一般的话就是 十几 KB,二十几 KB 撑死了,所以大部分人都会喜欢 LocalStorage 因为这个的储存空间很大。
    • LocalStorage 一般的话就是存 5MB,但是如果存储的东西超过 2.5 MB 的时候,就会出现一些性能的问题,
    • 一般的话移动端的网站都会做离线,就是把一些异步的 JS 放到 LocalStorage 里面来,所以这个里面一般都会放很多的 JS 文件 例:https://www.baidu.com/ 页面在 LocalStorage 查看到的结果
    • LocalStorage 一般是按照域名来进行存储的,操作里面的 JSON 格式存储的数据,也是很简单的,直接 LocalStorage.name 就可以获取到相应的 value 了,他是全局的变量
    • IndexedDBWeb SQL 是非常特殊的两种介质,IndexedDB 是属于关系型数据库的存储Web SQL 的容量是 50MB ,IndexedDB 的容量也将近是 50MB ,也就是说可以在浏览器这里存储 50MB 的 JS ,这个已经是相当的恐怖的了,但是他们两个都是异步的读取数据的,就是说你得基于回调来操作了 HTML5 Web SQL 数据库 的具体使用方法–菜鸟教程
  • Cookie 的定义

    Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain 共享 Cookie

  • 举例来说

    • A 网页是 http://w1.example.com/a.html ,B 网页是 http://w2.example.com/b.html ,那么只要设置相同的 document.domain ,两个网页就可以共享 Cookie ,具体实现操作如下:

      1
      2
      //  设置相同的 domain
      document.domain = 'example.com';
    • 现在,A 网页通过脚本设置一个 Cookie

      1
      document.cookie = "test1=hello";
    • B 网页就可以读到这个 Cookie

      1
      var allCookie = document.cookie;

      注意,这种方法只适用于 Cookieiframe 窗口,LocalStorageIndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的 PostMessage API

    • 另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如 .example.com

      1
      Set-Cookie: key=value; domain=.example.com; path=/

      这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie

不受同源策略限制的那些 html 标签/属性,这些都可以用来跨域

  • img -> src

    1
    <img src="http://f.hiphotos.baidu.com/image/h%3D300/sign=68049cfd5dda81cb51e685cd6267d0a4/4bed2e738bd4b31c426be0b28bd6277f9e2ff860.jpg" />

    原理:img -> srcbase解码的能力,如果里面嵌的有脚本的话是可以直接执行的。

  • img 拓展

    测试 一下手机网速 webapp 页面 根据这个网速 可以给用户 y 一些网速较慢的解决方案:直接跳转至简版(三个按钮,无图片、视频等等)

    • 示例:请求一个 1.1 kb 的图片
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      var img = new Image();
      var start = Date.now();
      img.src = 'http://baidu.com/s.gif';
      img.onload(function(){
      var end = Date.now();
      time = end - start;// 加载这个图片的时间差 单位是毫秒
      size = 1.1;//图片是 1.1 kb
      //计算网速
      speed = size / (time / 1000);// 得出每秒多少 kb
      //拓展:还可以根据这个网速进行自定义的评级 假设 0~1000 10 个等级
      // 求当前的等级
      var level = 10 - Math.floor(speed / 100);
      })
  • iframe -> src

    1
    <iframe src="https://www.baidu.com/" width="400px"></iframe>
  • script-> jsonp

    1
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
  • link -> href

    1
    <link rel="stylesheet" id="s_superplus_css_lnk" type="text/css" href="https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/css/super_min_0cd40cbc.css">
  • style -> background

    1
    2
    3
    4
    5
    <style>
    body{
    background:url()
    }
    </style>

    拓展出 CSS or XSS 攻击

  • 上述的三个标签 imgiframescript 是经常用来进行跨域,去处理复杂业务的 三大标签。

接下来进入 ajax 的领域

  • 示例 js 代码,这里使用的是 jQuery,封装的方法

    1
    2
    3
    4
    5
    6
    $.ajax({
    url:'http://www/xoxo/com',
    success:function(data){
    console.log(data)
    }
    })

    上例的代码会在控制台看到跨域的报错信息

  • 这个时候就引出了 jsonp 的概念

    现在前端实现跨域的方法,用的最多的就是 jsonp

    • 具体实现的做法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      <!-- 在 jsonp 上面的 script  标签中定义传过去的函数名称 -->
      <script>
      function test(data){
      console.log(data); // 这里可以输出在 index.php 执行时给传递的数据参数 {"data":"123"}
      }
      </script>
      <!-- html 这里向 index.php 传递一个 callback 参数 -->
      <script src="http://www.xxx.com/index.php?callback=test"></script>

      // php 在这里判断是否接受到 callback 的参数
      if(callback){
      // 有的话就执行
      test({"data":"123"}) // 这里执行时其实也是相当于 js 脚本执行了一遍
      }else{
      // 没有的话直接返回一些数据
      {"data":"123"}
      }

      上面 jsonp 的简单示例,我在 github 上有个可运行的小源码供参考jsonp-demo

  • 还有一个 CORS(跨源资源共享) 的概念,CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,是跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发 GET 请求,CORS 允许任何类型的请求。

    举例:我的网站里面的一个表单请求数据的接口,别人也可以在他的网站中用表单请求我这个网站表单提交的接口,这个就是 CORS
    这个也算是一种 攻击的手段

二、补充知识

1. 文件协议

如上图所示:以 file: 开头的访问的是本地文件,属于 文件协议,像我们正常在 浏览器中访问的是 http 协议,这两个东西差距大了去了。

2. window.postMessage

  • HTML5 引入了一个全新的 API:跨文档通信 API(Cross-document messaging)。

    • 这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源。

    • 举例来说,父窗口 http://aaa.com 向子窗口 http://bbb.com 发消息,调用 postMessage 方法就可以了。

      1
      2
      var popup = window.open('http://bbb.com', 'title');
      popup.postMessage('Hello World!', 'http://bbb.com');

      postMessage 方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即 “协议 + 域名 + 端口”。也可以设为 *,表示不限制域名,向所有窗口发送。

    • 子窗口向父窗口发送消息的写法类似。

      1
      window.opener.postMessage('Nice to see you', 'http://aaa.com');
    • 父窗口和子窗口都可以通过 message 事件,监听对方的消息。

      1
      2
      3
      window.addEventListener('message', function(e) {
      console.log(e.data);
      },false);
    • message 事件的事件对象 event,提供以下三个属性。

      1
      2
      3
      event.source:发送消息的窗口
      event.origin: 消息发向的网址
      event.data: 消息内容
    • 下面的例子是,子窗口通过 event.source 属性引用父窗口,然后发送消息。

      1
      2
      3
      4
      5
      window.addEventListener('message', receiveMessage);

      function receiveMessage(event) {
      event.source.postMessage('Nice to see you!', '*');
      }
    • event.origin 属性可以过滤不是发给本窗口的消息。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      window.addEventListener('message', receiveMessage);

      function receiveMessage(event) {
      if (event.origin !== 'http://aaa.com') return;
      if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
      } else {
      console.log(event.data);
      }
      }

3. 通过 window.postMessage,读写其他窗口的 LocalStorage

  • 下面是一个例子,主窗口写入 iframe 子窗口的 localStorage

    1
    2
    3
    4
    5
    6
    7
    window.onmessage = function(e) {
    if (e.origin !== 'http://bbb.com') {
    return;
    }
    var payload = JSON.parse(e.data);
    localStorage.setItem(payload.key, JSON.stringify(payload.data));
    };

    上面代码中,子窗口将父窗口发来的消息,写入自己的 LocalStorage。

  • 父窗口发送消息的代码如下。

    1
    2
    3
    var win = document.getElementsByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    win.postMessage(JSON.stringify({key: 'storage', data: obj}), 'http://bbb.com');
  • 加强版的子窗口接收消息的代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    window.onmessage = function(e) {
    if (e.origin !== 'http://bbb.com') return;
    var payload = JSON.parse(e.data);
    switch (payload.method) {
    case 'set':
    localStorage.setItem(payload.key, JSON.stringify(payload.data));
    break;
    case 'get':
    var parent = window.parent;
    var data = localStorage.getItem(payload.key);
    parent.postMessage(data, 'http://aaa.com');
    break;
    case 'remove':
    localStorage.removeItem(payload.key);
    break;
    }
    };
  • 加强版的父窗口发送消息代码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var win = document.getElementsByTagName('iframe')[0].contentWindow;
    var obj = { name: 'Jack' };
    // 存入对象
    win.postMessage(JSON.stringify({key: 'storage', method: 'set', data: obj}), 'http://bbb.com');
    // 读取对象
    win.postMessage(JSON.stringify({key: 'storage', method: "get"}), "*");
    window.onmessage = function(e) {
    if (e.origin != 'http://aaa.com') return;
    // "Jack"
    console.log(JSON.parse(e.data).name);
    };

重点笔记

什么是同源?

  • 协议相同
  • 域名相同
  • 端口相同

同源策略限制的对象(跨域)

  • CookieLocalStorageIndexedDB 无法读取
  • DOM 无法获取
  • AJAX 请求不能发送

如何设置同源策略(hosts

  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- test1.xxx.com a.html -->
    <script>
    document.domain = "xxx.com";// 设置同源策略
    document.cookie = "test1=hello";
    </script>

    <!-- test2.xxx.com b.html -->
    <script>
    var getCookie = document.cookie
    </script>

    // 现在大多数都是服务器去设置了
    domain=.baidu.com;// 最最实用的策略了

怎么突破同源策略

  • html 标签

    1
    2
    3
    4
    5
    img
    iframe
    script(jsonp)
    link(href)
    style(background)
  • 高阶操作 WebSocketpostMessage(可以通过 iframe image)

    代码写到 image里面 文章地址

坚持原创,坚持做个好人!