作为前端开发,从入行起,应该就接触过跨域的概念。工作中,在与服务端配合时,也经常需要处理跨域相关问题。如果你不能理解到底什么是跨域,那在与服务端配合解决跨域问题时,你可能就要落入对方的掌控之中了(哈哈,开个玩笑)。
为了避免这一尴尬的境地,今天我来带着大家一起重温并巩固一下以下内容:什么是跨域,跨域有哪些实际的开发场景,有哪些方式可以快速的处理跨域问题。
整篇内容没有涵盖网上所有的与跨域“沾边”的知识,而是从实际场景出发,旨在解决工作中经常遇到的跨域问题。
一、什么是跨域?
1.1 同源策略 VS 跨域
如果让你直接定义什么是跨域,你可能会发现很难定义。所以需要借助与之对立的概念——同源策略(SOP,Same-Origin Policy)。只要不是同源的,那就是跨域的。
同源策略是浏览器中一个极为至关重要的安全机制,可以用来限制某一源内的文档或脚本与另一源内的资源如何进行交互。其目的在于隔离潜在的恶意文档,减少可能存在的攻击。
而对于同源的定义是:协议、主机名(host)以及端口三者均相同。
维基百科中对于URI的结构组成说明如下:
[协议名]://[用户名]:[密码]@[主机名]:[端口]/[路径]?[查询参数]#[片段ID]
常规情况下,我们无需在URI中带有用户名密码等信息用于验证。这只是一个完整的URI组成示例。
所以对于同源,只要URI中协议名、主机名、端口三者有其中一条不同,则视为不同源。不同源之间请求资源,则为跨域。其中主机名部分,主域和子域视为不同、域名与其对应的IP也视为不同,这就是说看着必须得一样。
1.2 跨域的限制
当存在跨域问题时,浏览器会做出一定的限制措施。主要包括以下三点:
Cookie、LocalStorage 和 IndexDB 无法读取。
DOM 无法获得。
AJAX 请求被拦截。
注:Cookie获取不检测端口
二、常见的跨域开发场景/业务场景
撇开场景谈概念,一定是晦涩难懂的,开始说了,本文旨在解决实际工作中遇到的跨域问题。下面我们来一起看看工作过程中比较常见的跨域场景。
2.1 前后端分离:纯前端 + 接口层 (开发模式)
在前后端分离的开发模式下,开发环境应该用webpack的居多(当然有的可能不是,以此为例),与之相应的web服务器就是webpack-dev-server。这类开发模式的架构一般如下:
这一架构下,dev-server中的页面如果通过ajax直接调用服务端的API会存在跨域问题。
2.2 前后端分离:纯前端 + 接口层 (生产模式)
与第一种方式相似,前后端分离的项目在开发完成后,往往通过nginx等作为静态资源服务器,前端页面直接通过ajax发送请求,依然存在跨域请求问题。
架构如下:
2.3 前后端分离:纯前端 + BFF + 接口层
有时候,我们所要调用的接口层可能并不只是给我们提供服务,他们只会提供一些通用的数据,我们需要对数据进行一定程度的二次加工;也可能我们需要自己给前端页面提供一些通用的功能,如图片上传等。这时,就需要在前端页面和接口层之间增加一个BFF层(Backends For Frontends)。
BFF层一般由前端维护,所以使用Node.js居多。
这一架构如下:
使用这种架构其实本身已经解决了跨域问题,是一种跨域解决方式,后面我们再细说。
2.4 服务端渲染 + web服务器(不跨域)
最后一种是最原始的web服务架构,html页面以及其他静态资源都直接从服务器获取,接口也直接由所在服务器处理。这种方式不存在跨域问题。前端和服务端逻辑完全绑定,互相支撑提供服务。
三、跨域的解决方式
前面我们提到,跨域是浏览器的限制。所以我们想解决跨域问题可以有两个方向,第一是绕开浏览器限制,第二是通过浏览器支持的方式来允许跨域。
下面我们分别会介绍三种绕开浏览器限制的解决方式,分别为webpack-dev-server代理/Nginx代理转发/服务器代理,以及浏览器本身支持的CORS方式。
没有大家耳熟能详的JSONP,大家自行科普一下吧。
3.1 webpack-dev-server代理
对于上面说到的“前后端分离:纯前端 + 接口层 (开发模式)”这一场景,当我们在http://co.com的页面上直接调用http://api.co.com的接口时,会出现跨域问题。
我们可以将所有的接口请求都从http://co.com发出,如http://co.com/api/getSomeData(额外加了/api,方便统一转发),最后通过proxy配置代理,转发到最终的接口服务器http://api.co.com/getSomeData。
proxy配置如下:
devServer: { proxy: { '/api': { target: 'http://api.co.com', // 如果转发后的pathname需要改变,可以通过以下方式重写 // 下面是把api前缀去掉 pathRewrite: { '^/api/': '', }, }, }}
通过上述方式,我们可以在接口请求发起的时候,统一从当前所在源发起,最后通过proxy代理的方式转发到真正的接口层。这样就绕开了调用接口时浏览器的同源限制。
3.2 Nginx代理转发
针对第二部分提到的“前后端分离:纯前端 + 接口层(生产模式)”这一场景,这时我们没有webpack-dev-server可用了,不过没关系,我们在使用nginx作为静态资源服务器时,也可以做一些代理转发。可以将接口请求全部转发到对应接口服务器。
配置如下:
location /api { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-NginX-Proxy true; # 转发时重写地址 rewrite ^/api/(.*)$ /$1 break; # 转发目的地 proxy_pass http://api.co.com;}
这种方式其实与第一种类似,只不过是通过不同的方式进行代理。这种方式也是通过绕过浏览器限制的方式解决跨域的。
3.3 服务器转发
第二部分的第三种架构“前后端分离:纯前端 + BFF + 接口层”,这种架构其实就已经解决了跨域的问题,前端页面的所有接口都由BFF层进行管理。
对于BFF层,可以通过添加中间件或者其他的方式对于接口进行拦截。如果是静态资源或者是当前服务所提供的接口,则直接处理。如果是调用api的接口请求,将其转发到对应的服务即可。
不同的框架有不同的方式来处理接口拦截与转发,所以此处没有代码。
3.4 CORS 跨域资源共享
跨域资源共享(CORS) 是一种机制,服务端可以通过额外的 HTTP 头来告诉浏览器允许某一源内的Web应用访问不同源服务器上的指定资源。
CORS使用通用的跨域解决方式,需要服务端配合进行实现。
这里面会涉及到简单请求以及预检请求的概念。关于什么是简单请求,大家可以移步MDN1看下详细的定义,这里不再详述了。
这两种请求的区别在于,对于预检请求,浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的HTTP请求。
为什么要区分简单请求和预检请求可以参考贺老的这篇文章2
IE8/9不支持CORS,通过XDomainRequest来实现
3.4.1 简单请求
对于简单请求,服务端通过简单的设置Access-Control-Allow-Origin: *即可允许任意来源进行跨域请求。如果只想允许来自http://co.com的访问,可以设置Access-Control-Allow-Origin: http://co.com。
通信过程示意图如下:
注:在发起跨域请求时,浏览器会在请求头字段中自动带上Origin字段,值为当前所在域。
3.4.2 预检请求
对于预检请求,服务端需要额外再多做一些事情。如下步骤:
首先,发起预检请求,带上真实请求的Method。
服务端判断是否允许跨域请求,如果允许则返回允许的来源、允许的请求Methods以及预检请求的有效时长(有效时间内,同一请求无需再次发送预检请求,不过不可以任意设置,浏览器有最大时长限制)。
客户端发起真实的跨域请求。
服务端返回。
通信过程示意图如下:
需要注意的是,服务端在处理预检请求时,如果允许跨域,服务端只需要设置对应的响应头,然后直接返回即可,无需其他处理。
3.4.3 附带身份凭证的请求
常规来说,我们的请求都需要带有身份凭证(如Cookie),这时服务器端的响应中需要额外设置Access-Control-Allow-Credentials: true,如果未设置,浏览器将不会把响应内容返回给请求的发送者。
还有个别不是很常用的请求头和响应头字段,大家可前往MDN3查看完整的列表。
总结
如上,我们从以下三个方面介绍了跨域:什么是跨域,跨域有哪些实际的开发场景,有哪些方式可以快速的处理跨域问题。
大家在遇到跨域问题时,可以根据具体的场景选择绕过跨域问题,还是选择通用的CORS模式来解决。