前言
跨域
问题是前端开发绕不过去的一个问题,常常让开发者很头疼,下面我们来聊聊跨域问题的由来,以及该怎么正确的解决跨域问题!
什么是跨域
首先思考一个问题,为什么会出现跨域?
在回答这个问题之前,我们先来看两个例子:
csrf
1、假如我用浏览器访问了站点A,站点A将一些私密信息存在了站点A域名的cookie下面
2、此时我用浏览器访问站点B,站点B的脚本获取到站点Acookie下面的信息,然后伪造了一个ajax请求,修改了用户的信息,这是一个非常危险的操作!
3、以上的整个过程就称为-跨域请求伪造(csrf),过程如下图所示:
跨域dom访问
1、假如我做了一个银行网站(假的),通过iframe将真正的银行网站链接到了我自己的网站中
2、此时银行用户在不知情的情况下,访问了我的银行网站,但是在真的银行站点中输入了自己的账户和密码
3、此时我通过自己的假网站访问真网站的DOM结构,获取用户的账号和密码,这也是很危险的
通过以上两个例子,我们知道,如果浏览器没有跨域策略存在,会存在许许多多的安全问题(不局限以上例子),这些问题影响很大,会造成不可预料的后果!
正是基于此,浏览器在1995年
由 Netscape 公司
引入浏览器提出了同源策略,借此来限制不同域之间资源的相互访问,提升网站的安全性。正是由于这个同源策略的存在,引入了浏览器跨域访问的问题。
那么什么是同源策略呢?它又是做到提升安全性的呢?我们接着往下看。
what is 同源策略
一直说同源,那么什么是同源呢?同源跟跨域有什么联系呢?
可以用一句话总结跨域与同源的关系--同源策略引入了跨域问题
那么浏览器是怎么定义同源策略的呢?很简单,我们先看下面一张图:
我们都知道,一个url地址,基本上由协议、域名、端口
和路径组成,在定义同源时只用到了协议、域名和端口,顾名思义,判断一个两个站点是否同源,就只需要看他们的协议
、域名
和端口
是否一样就可以了。图中有个简单的练习,大家可以看一下自己的掌握情况!
通过学习我们了解了浏览器的同源策略,它是浏览器的基本规则
,定义了跨域站点之间的资源交换协议
,规定跨域站点之间哪些资源可以交换,哪些资源不能交换。
在了解了同源策略以后,我们来看一下,同源策略对站点之间的资源访问都做了哪些限制:
同源策略规定:
(1) 跨域站点之间Cookie、LocalStorage 和 IndexDB
无法读取。
(2) 跨域站点之间DOM
无法获得。
(3) 限制跨域站点之间AJAX
请求。
通过学习同源策略,我们知道了不同站点(跨域,不同源)之间不能相互访问cookie、不能相互访问DOM,这也就解决了我们文章开头所遇到的安全问题。但跨域策略也是最基本的安全策略,并不能完全限制csrf等安全问题,这点需要注意!
同源策略是浏览器的一个安全策略,为了提高浏览器的安全性,但是它同时带来了跨域问题,所以同源策略是浏览器安全性和可用性之间的一个平衡点,有得也有舍(提高了安全性,牺牲了可用性),取舍之道也!
跨域例子
了解了跨域的由来之后,下面我们来看一下跨域的实际例子:
图一,我们从域https://test.dev.com.cn:4010
访问到域https://vg-config-pre.vmic.xyz
,被浏览器检查出ajax跨域请求,所以报错了!符合同源策略第三条!
图二与图三,我们从域https://test.dev.com.cn:4010
访问到域https://vg-config-pre.vmic.xyz
,我们看这个请求,发现请求与同源ajax之间的区别是它发送请求之时,没有携带cookie信息!这符合同源策略第一条!
至于同源策略第二条,我们可以通过内嵌iframe的例子,轻松想象!
跨域策略
浏览器出台了同源策略,提升了安全性,牺牲了可用性。为了提升不同域名之间的通信能力,浏览器给出了跨域的解决方案,那就是cors--跨域策略
!
下面我们来了解一下什么是cors
对于cors,用官方的话来说是这样的:跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种基于HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它origin(域,协议和端口),这样浏览器可以访问加载这些资源。
我们来翻译一下这句话,cors是浏览器通过http头
实现的一种跨域策略,浏览器可以通过http响应头规定允许除自己外的哪些域访问,并将跨域信息返回给浏览器。浏览器则可以http请求头携带自己的身份信息,方便服务器根据信息判断是否允许其跨域。
通过学习cors规则,我们了解到两个重点信息:
第一,跨域策略是通过http请求头
与http响应头
告诉浏览器允不允许实现的
第二,跨域策略其实就是一问一答
,浏览器通过http请求头问浏览器允不允许跨域访问,服务器通过http响应头告诉浏览器允不允许跨域访问
那跨域策略究竟规定了哪些请求头(requeset headers)
和响应头(response headers)
呢?
请求头:
Origin: 表明请求的来源域名
header: 表明请求携带哪些请求头
method: 表明请求的方法
响应头:
Access-Control-Request-Method:表明服务端允许哪些方法跨域
Access-Control-Request-Headers:表明服务端允许携带哪些请求头跨域
Access-Control-Allow-origin:表明服务端允许哪些域跨域
Access-Control-Max-Age: 86400 :对于复杂请求来说,规定了预检请求的有效期,后面再说什么是复杂请求!
可以看到跨域请求头和响应头是一一对应的,如果请求头携带的信息都完全符合响应头的规则,则允许当次请求进行跨域访问。注意,是完全符合所有的条件!
以上就是cors跨域策略的主要内容。除了这些,cors还根据一些规则,对http请求进行了区分,分为简单请求
和复杂请求
简单请求与复杂请求
简单请求和复杂请求的跨域流程是不一样的,复杂请求
比简单请求
多了一道预检请求
。我们可以通过下图来看一下什么是简单请求:
可以看到,完全符合
上面五个
条件的被称为简单请求
,其余的都是复杂请求
。
简单请求跨域流程
简单请求的跨域流程是什么呢?直接发送跨域请求
,判断请求头携带的跨域信息和是否符合响应头规定的跨域规则,如下图所示,请求的来源为Origin:foo.example
,响应头Access-Control-Allow-origin:*
规定所有域都可以跨域访问,完成整个跨域访问的流程。
复杂请求跨域流程
不符合上面五个条件的被称为复杂请求
,下面的代码使用 POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-PINGOTHER: pingpong)。另外,该请求的 Content-Type 为 application/xml。因此,该请求的跨域流程需要首先发起“预检请求”。
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
function callOtherDomain(){
if(invocation)
{
invocation.open('POST', url, true);
invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
invocation.setRequestHeader('Content-Type', 'application/xml');
invocation.onreadystatechange = handler;
invocation.send(body);
}
}
我们来看一下复杂请求的跨域流程,复杂请求会发送一个请求方法为options的预检请求
,判断预检请求的请求头是否符合服务端响应头的跨域规则,如果符合才会继续发送正式请求,完成跨域流程。如下图所示,请求的来源为Origin:foo.example
,响应头Access-Control-Allow-origin:http://foo.example
规定foo.example可以跨域访问,浏览器接收到允许跨域的消息以后,再发送正式请求,完成跨域访问。复杂请求要增加预检请求的策略,可能是处于安全方面的考虑。
不管是复杂请求还是简单请求,最终都是判断请求头和是否符合响应头设置的跨域规则!
,这也决定了解决跨域问题需要前端与服务端配合。
前文提到,跨域策略cors中还有Access-Control-Max-Age: 86400 这样一个请求头,它规定了预检请求的有效期,如果你在响应头中将该值设定为86400,那么预检请求的有效期就是86400ms,换算成一天24小时,24小时内跨域访问免预检,超过24小时需要重新发送预检请求。
这就是cors!
解决跨域
了解了同源和跨域策略之后,我们来了解一下该如何解决跨域问题!
思路有以下两种:
1、直接按照跨域需求,在服务端配置,多应用于测试环境、预发环境和线上环境
2、通过代理将不同域转化为相同域,开发环境使用居多
服务端配置解决跨域问题的方案,我们不在多说,属于是沟通问题。我们来看看怎么通过代理的方式,实现前端自己跨域访问。
开发环境--webpack devServer
直接利用webpack提供的代理,将ajax请求转换为和服务端站点一样的域名(消除跨域),具体实现如下,一般用于开发环境:
devServer: {
host: 'vg-config-dev.vmic.xyz',
proxy: {
'/api-v0': {
target: 'https://vg-palace-test.vivo.com.cn',
changeOrigin: true,
secure: true,
logLevel: 'debug'
}
}
}
假设我本地应用的域名为http://vg-config-dev.vmic.xyz:8080
,我要访问的服务端接口为https://vg-palace-test.vivo.com.cn/api-v0/getConfig?id=20
,这时出现了跨域访问,通过对/api-v0
规则的代理,将http://vg-config-dev.vmic.xyz:8080
代理到https://vg-palace-test.vivo.com.cn
,当我访问http://vg-config-dev.vmic.xyz:8080/api-v0/getConfig?id=20
时,实际上就是访问https://vg-palace-test.vivo.com.cn/api-v0/getConfig?id=20
,而http://vg-config-dev.vmic.xyz:8080/api-v0/getConfig?id=20
与我站点的域名http://vg-config-dev.vmic.xyz:8080
同源,避免跨域访问,实现接口的正常访问。
开发环境--nginx
Nginx(发音同“engine X”)负载均衡,提供代理服务。
该软件由伊戈尔·赛索耶夫(Игорь Сысоев)创建并于2004年首次公开发布,俄罗斯。
2019年3月11日,Nginx公司被F5网络公司以6.7亿美元收购。
俄罗斯研发的产品,被美元收购,可以看到科学是没有国界的(玩笑)!!!
Nginx可以实现正向代理(代理客户端)
和反向代理(代理服务端)
,我们都知道vpn,我们通过vpn访问外国网站,就是正向代理的一个实际例子。我们就是利用Nginx的正向代理功能解决跨域问题,下面来介绍nginx实现跨域的具体流程。
niginx服务器 下载(mac)
1、先下载 homebrew,mac软件下载管理工具
2、使用homebrew
下载 nginx, 命令行执行 brew install nginx 下载 nginx
3、下载switchhost
nginx-正向代理实现跨域
假设本地项目跑在http://10.15.51.236:8081
上面,后端的跨域策略为:Access-Control-Allow-origin:.vivo.com.cn
我们使用 nginx 将本地项目http://10.15.51.236:8081
代理到10.15.51.236:80
,具体实现方式如下:
编辑 nginx 配置,命令行执行vim /usr/local/etc/nginx/nginx.conf
,然后编辑文本:
然后开启 nginx 代理:命令行执行nginx
命令
开启switchhost
代理,将http://game-bbs.vivo.com.cn
代理到http://10.15.51.236
我们知道,当直接访问域名不带端口号的时候,访问的是默认端口80
,所以当我们访问http://game-bbs.vivo.com.cn
时,通 switchhost 代理,实际上访问的是http://10.15.51.236
,也就是就是访问http://10.15.51.236:80
,再通过 nginx 代理,实际上就是访问http://10.15.51.236:8081
。
此时我们本地项目的域名变成了http://game-bbs.vivo.com.cn
,符合了我们预设的接口跨域策略.vivo.com.cn
,成功实现跨域!
nginx-反向代理实现跨域
代理服务端:该方案一般用于测乎环境、预发环境和正式环境。
反向代理的原理很简单,nginx代替服务端,设置了允许跨域的域名、headers和method,符合这些的条件的请求都可以跨域!
总结
跨域是浏览器同源策略带来的问题,同时浏览器也给出了跨域解决方案!
跨域问题是前端的事情(前端引入),但是解决跨域需要服务端或者运维配合!
参考文献
1、developer.mozilla.org/zh-CN/docs/…