跨域是怎么回事

本文最后更新于:1 年前

跨域是怎么回事

从一个常见的问题开篇

request has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

相信上面的错误很多时候都能碰到,一般发生在前端发起请求时,而可能很多开发都不知道怎么解决,甚至是谁去解决。其实这个就是比较典型的跨域访问问题,而绝大部分这种情况,都不是单纯靠前端能解决的,因为禁止跨域访问原本就是 web 安全规范的一项,如果纯前端能绕过去,那还有什么安全意义呢?

什么是跨域

通常说的跨域(cross origin),其实指的是跨不同源的资源请求,而浏览器会对非同源的请求进行安全性限制,这源自于浏览器的同源策略(Same-origin policy)约定,从而一定程度上提升了浏览器的安全和提高了攻击成本。

那什么是不同源呢?可以将一个资源地址看成是 scheme + host + port + path 的组合:

https://abc.com:8888/home/index
scheme|  host  |port|   path   |

https 是协议
abc.com 是域名
8888 是端口
home/index 是路径

下面用 https://abc.com/home/index 作为例子发出了如下几种请求:

URL 结果 原因
https://abc.com/index 成功 只是路径不同
http://abc.com/home/index 失败 协议不同
https://a.abc.com/home/index 失败 域名不同
https://abc.com:8888/home/index 失败 端口不同

而在 IE 中,未将端口纳入同源策略的检查中,所以上面的最后一个在 IE 中并不受限制。

可组合性是 Web 非常强大的一项能力,让 Web 可以随时加载不同来源的资源以增强自身功能,包含字体、图片、音视频、脚本等。但是强大的策略同样会带来信息泄露的风险,攻击者很容易能利用漏洞获取用户的信息,但是浏览器在安全方面也做的很好,这个就是同源策略。而同源策略所限制的交互包含以下 3 类:

  • 跨域网络访问:包含 XMLHttpRequest、fetch 或者<script>、<link>、<img>、<video>、<audio>、<object>、<embed>、<iframe>等引入的资源。
  • 跨域 dom、api 访问:主要是 iframe 的内容和 window.open 后的内容限制。
  • 跨域数据存储访问:包含 cookie、localStorage 和 IndexedDB。

同源策略的意义

可能在日常开发中,用 link 标签、script 标签、img 标签引入一些外部资源很常见,而且似乎也没出过问题,并且 form 里的 action 好像也可以跨域发送,为什么换成 js 请求(xmlhttprequest、fetch 等)就不行呢?

如果没有同源接口请求限制

现在你的公司内网有些内部服务在跑,外部攻击者即使没有同源策略外部也无法访问,这个很安全。但是如果攻击者做一个攻击网站,想办法让你打开,请求你的内部 ip 上的服务,然后获取结果再返回到攻击者服务器里,这就能窃取到你公司内网内的信息了。你可能觉得获取内部的地址很难,如果我是直接遍历请求localhost:所有端口呢?这样子就能拿到所有你跑在 localhost 里的内容了。

如果没有同源 dom 限制

假如有个银行网站叫yinhang.com,然后攻击者做了个钓鱼的网站叫yinghang.com,内部用 iframe 嵌入了yinhang.com。因为你的疏忽没看清地址,进入了钓鱼网站并登录了,而攻击者的网站又能畅通无阻地获取 iframe 内所有 dom 和对象,那你的账号密码将直接暴露给攻击者了。

如果没有同源存储访问

这个就更恐怖了,大量的登录 token 放在 cookie,只要你登录过银行账号等,攻击者的网站就能直接向银行接口发起请求并且带上你的银行账号 token 的 cookie,然后做任何他想做的事.

安全性

其实说到底是个安全性的问题。

那你可能会好奇为什么前者允许:因为这些资源对于浏览器来说,属于页面内容、网页资源的一部分,而且浏览器会将各个站点的不同资源放在不同的 Context Group 下,不同的 Context Group 下的资源无法互相访问。所以你放在标签内的图片等,可以展示,但是 js 无法读取,而无法得知其内容也无法将其传到别的地方,所以这里也没有外泄的问题。

form 表单中 action 其实也没有跨域问题,因为原页面使用 form 传送信息给新页面后,原页面 js 无法获取结果,所以浏览器也是觉得安全的。其实总结来说就是,浏览器本质上并不阻止你发送请求和信息,只是在未经许可前,不允许你获取返回的结果而已。

所以同源策略是非常必要且有意义的,像平时开发中遇到开篇中那种常见的问题之类的跨域问题,基本都不是前端的问题,而且纯前端手段是无法解决的。因为如果能用前端脚本手段绕过,那安全何在呢?但是并不代表前端就可以不了解这块了。而同源策略也仅仅只是限制了浏览器环境,如果是服务端之类的就没有这个问题。

如何解决跨域 dom、api 访问?

在了解了跨域问题后,可能你需要在主页面打开新窗口,或者创建 iframe,并且获取其信息,那需要如何解决呢?

1、postMessage

window.postMessage() 是 HTML5 的 api,利用它可以实现主页面和其打开的新窗口通信、创建的 iframe 通信:

const childWindow = window.open('http://localhost:8080/app2.html', 'title');
// 第一个参数为传递信息,第二个参数为目标origin
childWindow.postMessage('hello word', 'http://localhost:8080/app2.html');

子页面可以监听message事件来获取信息

window.addEventListener(
  'message',
  e => {
    console.log(e.source); // 发送信息的窗口
    console.log(e.origin); // 信息发向的网址
    console.log(e.data); // 发送的信息
  },
  false
);

2、document.domain

这个方法只适合主域名相同的情况,例如aaa.com打开了www.aaa.com页面,这种情况下,可以修改document.domainaaa.com,如此就可以访问对方的 window 对象以及其执行的所有对象了。

如何解决跨域请求?

除了跨域 dom、api 等对象访问以外,还有就是跨域请求访问。如果还是需要给不同源的地址发送请求,那需要怎么解决同源策略这个问题呢?

其实方法有很多,可以先列举几个的方法:

1、关闭浏览器的安全设置

同源策略本质上只是浏览器做的限制,所以只要关闭了这个限制就可以,不过这个仅仅只对自己的设备,是个治标不治本的方案。具体各个浏览器如何关闭可以自行搜索。

2、将 fetch mode 设为 no-cors

fetch 方法里有个参数 mode,可以将其设置成no-cors

fetch('http://localhost:9999/info', {
  mode: 'no-cors',
  method: 'POST',
})
  .then(res => {
    console.log(res);
    return res.text();
  })
  .then(console.log);

我们本地起个服务试试,本身会跨域的请求,加上这个参数后会打印出:

fetch的mode为no-cors打印结果

可以看到状态码为 0,type 为 opaque,拿到的请求返回为空,但是控制台没任何报错。在控制台的 network 内又能看到正确返回。

因为mode: ‘no-cors’并非解决了跨域问题,而是告诉浏览器你需要发送一个跨域请求,并且你确实不关心返回。如此,这次请求不会报任何错误,但是也不会返回任何信息到 js 里,即使已经设置了 Access-Control-Allow-Origin 的 header。所以这个设置只是在某些场合下可用而已,并非真正解决跨域请求问题。

3、不使用 ajax 获取数据

不是用 ajax 发出的请求确实就没有这种限制了,那考虑到的就是 html 标签和 form 表单。

当我们发送请求时,变成创建一个script标签,那这种请求就不会受同源策略限制:

// 前端
const request = (url, data, cb) => {
  const id = parseInt(Math.random() * 10 ** 10);
  const callbackName = `callback${id}`;
  const urlSearchParams = new URLSearchParams();
  // 将回调函数名告诉服务端
  urlSearchParams.set('callbackName', callbackName);
  // 组装参数到url中
  Object.keys(data).forEach(key => {
    urlSearchParams.set(key, data[key]);
  });
  // 创建script标签
  const $script = document.createElement('script');
  $script.src = `${url}?${urlSearchParams.toString()}`;

  // 设置临时回调函数,触发callback并删除临时回调函数本身
  window[callbackName] = data => {
    cb(data);
    window[callbackName] = null;
    delete window[callbackName];
  };

  // 插入dom中,利用html资源能力发送
  document.body.append($script);
};

request('localhost:9999/info', { name: 'guy', id: '2021' }, data => {
  console.log('获取数据:', data);
});

利用script标签绕过跨域问题发送出请求,最后需要服务端响应这次请求并且返回一个 js 可执行代码,触发我们定义好的临时回调函数并将数据插入:

// 服务端
const express = require('express');
const app = express();

app.get('/info', (req, res) => {
  const id = req.query.id;
  const name = req.query.name;
  const callbackName = req.query.callbackName;
  // 返回可执行代码,触发临时回调函数,并将数据传入
  res.end(`${callbackName}(${id},${name})`);
});

这样就是简单的不使用 ajax 发送请求方法了,这也是大名鼎鼎的JSONP(JSON with Padding),这个方法在早期 CORS 规范不完整时非常常用,但是弊端也很明显:只能使用 GET 这个 method,那如何发送其他 method 的请求呢?


其实可以利用到 form 表单,form 本身无跨域问题,也支持多种 methods,只是提交后会刷新,那我们只需创建一个 iframe,并且让 form 提交后刷新这个 iframe 而非主页面即可。

const request = (url, data, cb) => {
  const $iframe = document.createElement('iframe');
  $iframe.name = 'request';
  $iframe.style.display = 'none';
  // 注册iframe的load事件,并且触发回调
  $iframe.addEventListener('load', cb);

  document.body.appendChild($iframe);

  const $form = document.createElement('form');

  $form.action = url;
  // 在指定的iframe中执行form
  $form.target = $iframe.name;
  $form.method = 'post';
  $form.style.display = 'none';

  Object.keys(data).forEach(key => {
    const node = document.createElement('input');
    node.name = key;
    node.value = data[key];
    $form.appendChild(node);
  });

  document.body.appendChild($form);
  $form.submit();

  document.body.removeChild($form);
};

这种方法弊端也很明显:就是无法获取返回数据。

4、代理、转发

这种方法较为正规,利用了服务端无这种限制的优势,只要代理或者转发的服务域名跟前端域名同源即可:常见的可以前端自行启动服务转发,也可以中间件返回 CORS 配置好的 headers,也可以 nginx 反向代理。

弊端嘛,就是多了一层服务,增加服务器压力和多多少少减慢了返回速度。

5、服务端返回 CORS 响应头

这个才是最正确且常规的做法,也是从根本上一劳永逸地解决了跨域问题,因为这个方案将权限完全交给服务端,服务端来控制是否允许前端获取请求响应。

弊端的话,可能就是浏览器兼容问题,主要需要 IE10 以上。

下一篇会详细讲解 CORS。