跨域是怎么回事
本文最后更新于: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.domain
为aaa.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);
我们本地起个服务试试,本身会跨域的请求,加上这个参数后会打印出:
可以看到状态码为 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。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!