前言:
跨域,在前后端分离的项目中是很常见的一个问题,跨域是对于浏览器操作来说的,脱离了浏览器来谈跨域是没有任何意义的,笔者目前所在的项目组就是一个前后端分离的项目,也出现过跨域问题,这里对跨域以及跨域涉及到的CORS、OPTIONS请求、Referer字段、Origin字段等信息做个总结。这篇文章分三个步骤去总结跨域问题,第一部分介绍什么是跨域,以及跨域相关的一些概念;第二部分介绍跨域带来的问题有哪些。第三部分总结解决跨域问题的方式。
一、跨域
1.什么是跨域?
跨域这个概念是对于前端来说的,准确的说是对于浏览器来说的,因为安全问题,浏览器会禁止js访问非同源的资源,访问非同源的资源就是跨域。那什么样的资源才算是同源呢?同源需要同时满足以下三点:
1)通讯协议相同
2)域名或者ip相同
3)端口相同
当浏览器发出的请求,更具体点的说是js发出的资源请求,对于以上三者有任何一个不满足,都属于跨域,假如请求发起地址是这个:http://huawei.one.two/,当访问如下地址时的跨域情况如下:
1)https://huawei.one.two/ 通讯协议不同,这里访问的是https,属于跨域
2)http://huawei.one.three/ 域名不同,也属于跨域
3)http://huawei.one.two:8080/ 原端口是默认的80端口,端口不同也属于跨域。
4)http://huawei.one.two/supplier/queryDetails 这里访问的是相同的通讯协议、相同的域名、相同的端口,虽然地址有区别,但是不是跨域。
2.浏览器为什么要禁止跨域?允许跨域有什么隐患?
前面简略的说了下,因为安全问题,浏览器禁止了跨域的请求,跨域的最典型的安全问题体现在Cookie中,我们知道Cookie中会缓存登录信息,跨域时Cookie信息是不会共享的,假如Cookie中的登录信息可以共享,这很明显会有极大的安全隐患。举个例子:A用户登录了中国银行账户系统,在未退出的情况下,又访问了某动作片网站,动作片网站中电影图片链接的不是影片地址,而是中国银行的转账支付链接,此时A用户点击了这个电影。那么此时用户点动作片电影时就变成了进入中国银行去转账了,因为浏览器也允许跨域,所以此时Cookie信息共享,A用户访问的转账地址携带了Cookie中的登录信息。银行一看是用户A的正常操作,转账就成功了。这就是允许跨域的最典型的问题,所以浏览器会禁止跨域访问,禁止跨域请求,跨域时Cookie信息不共享。所以总结来说,禁止跨域就是为了保护用户的信息安全,就是为了Cookie、localStorage等中的信息不回被非同源的网站窃取。
3.跨域时有什么体现
说到跨域的体现就必须要说OPTIONS请求了,因为跨域总是伴随着OPTIONS请求的,那什么是OPTIONS请求呢?下面先来介绍下OPTIONS请求。
3.1 什么是OPTIONS请求?
Http请求总共有8种,OPTIONS,就是其中一种;这八种请求方式分别是:GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE、CONNECT。这把八种请求中OPTIONS之前的三个请求GET、POST、HEAD是简单请求类型,OPTIONS之后的四种就是复杂请求了,也可叫重请求。
3.2 OPTIONS请求有什么作用?
已经知道OPTIONS请求是八种Http请求的一种,那他有什么作用呢?他的作用官方给出的解释有两种:
- 1)获取服务器支持的HTTP方法。
- 2)检查服务器性能。
这就是OPTIONS的作用了,在跨域时,浏览器会先向服务器发送一个OPTIONS请求,用来判断服务器是否支持跨域的操作,假如服务支持跨域的话服务器需要在响应的头信息中加入Access-Control-Allow-Origin来声明允许跨域的域名;加入Access-Control-Allow-Methods来声明允许的请求方式。下面就是一个跨域时的OPTIONS请求案例:
如果服务器不支持跨域的话,会响应一个错误码过来,此时第二次的真实请求是不会发送出去的。
3.3 哪些场景会触发OPTIONS请求
前面说跨域会触发OPTIONS请求,事实上不止跨域会触发OPTIONS请求,这只是OPTIONS被触发的一种情况,此外还有两种情况会触发OPTIONS请求,这里一起总结下三个场景。
1)跨域时会触发OPTIONS请求,用以判断服务器是否支持跨域操作,上面已经说了允许跨域时需要在Access-Control-Allow-Origin、Access-Control-Allow-Methods的这两个字段进行声明允许的跨域的域名和允许的请求方式,如下图:
2)简单请求时(GET、POST、HEAD)添加了自定义的头部信息、Content-type不是这几种类型application/x-www-form-urlencoded、multipart/form-data、text/plain此时会触发OPTIONS请求。此时需要去服务器验证是否支持自定义请求头,如果不支持会返回相应状态码,如果支持,则会在返回的响应头中的这个Access-Control-Allow-Headers:中进行声明,如果支持自定义头部的话,这个字段的值要么是列出支持的字段,要么是*,如下图所示:
3)复杂请求(PUT、DELETE、TRACE、CONNECT)都会触发OPTIONS请求。但是这些请求一般不会使用,正常的服务都会禁用这些请求方式。
二、跨域导致了什么问题
1.请求无响应
前面已经说过如果服务器允许跨域,会在响应头中加入Access-Control-Allow-Origin这个字段标注允许跨域的域名,这个域名还必须与OPTIONS请求发起时Origin中的值相同,当浏览器收服务器响应的信息时,会在头中检查Access-Control-Allow-Origin这个字段的值,若是与Origin不匹配,则不会将请求信息返回给用户看到,此时体现的就是请求无响应。这里解释下Origin字段,这个字段是只有在POST请求中才会出现,作用是标注请求发起的源或者说位置。Origin与另一个字段Referer有些类似,Referer作用也是标注请求发起的位置,只不过所有的请求中都会有Referer。Origin和Referer通常被用作预防CSRF攻击的判断,下面第一张图是GET请求,只有Referer没有Origin。第二张图是POST,两者则都有。如果对于CSRF有些疑问可以看这里:一文从容应对CSRF。
2.请求发送不出去
有时候就会出现这种情况,笔者也是碰到过:浏览器访问的网页正常,但是点击功能时一直都没有响应,然后打开F12查看network时发现浏览器压根就没有将请求发送出去,这是为何?当时也是百思不得琦姐,后来才知道这个也是跨域引起的问题,当时运维反映说是变更了网略策略导致的这种情况,具体操作,笔者也是不清楚,但是这个问题的根本原因是跨域引起的,若是碰到类似问题,解决跨域问题这个问题也就解决了。
3.哪些项目会有跨域问题
上面已经说了跨域的条件,仔细一想便可以发现,只有前后端分离的项目才会碰到跨域问题,而且一定会碰到。因为前后端分离时,前端是一个独立的服务,后端是一个独立的服务或者集群。若是前后端服务部署在同一服务器,那他们接口肯定不同,部署在不同服务器他们ip不同,所以前后端分离的项目肯定会碰到跨域问题,而前后端不分离的项目则不会有这种问题,因为请求的都是同源的资源。那么如果碰到了跨域问题怎么解决呢?
三、如何解决跨域的问题
1.通过服务器代理解决跨域(推荐)
对于前后端分离的项目一定是有跨域问题存在的,目前主流的解决方案就是使用NGINX的反向代理,前端配置相对路径,根据NGINX的配置代理后端地址,这样就解决了跨域的问题,与前端交互的是NGINX服务器,就不会有跨域问题,NGINX服务器与后台交互也不会有跨域,这样就可以解决跨域的问题,这种方式笔者认为还是比较简单的,若是对于NGINX的配置不太清楚,可以去b站上搜搜教学视频,NGINX的使用还是很简单的。
2.CORS解决跨域(推荐)
CORS(跨域资源共享:Cross-origin resource sharing)是W3C的标准,这个标准允许浏览器跨域对服务器的资源进行访问,只不过前端在发起请求是需要使用XMLHttpRequest进行发起,目前主流的浏览器都支持CORS,所以这是一种行之有效的解决方案。在使用这种方案解决跨域时,需要服务端的同时支持。服务端要如何改动呢,这里以SpringBoot(SpringCloud类似)构建的项目为例来进行演示服务端如何允许跨域的访问。这里可以细分为两种方案,一种是全局式的解决方案,一种是方法级别的解决方案。
2.1 服务端全局解决方案
我们需要在前端配置适配器(WebMvcConfigurerAdapter)中来进行相关的配置,代码如下所示,这样就完成了CORS的后端支持,前端则不需要配置什么,因为前端默认就是支持的。这样服务端的跨域问题就解决了。
@Configuration public class WebInterceptorConfiguration extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**")//允许被跨域访问的地址 .allowedOrigins("http://sworkp.esgcc.com.cn")//允许跨域的域名,对应响应头中的:Access-Control-Allow-Origin .allowCredentials(true)//跨域是否允许使用Cookie信息,对应响应头中的:Access-Control-Allow-Credentials .allowedHeaders("*")//头部允许的自定义参数,对应响应头中的:Access-Control-Allow-Headers .allowedMethods("POST","GET","OPTIONS")//允许的请求方法,对应响应头中的:Access-Control-Allow-Methods .maxAge(1800);//配置客户端缓存预检请求的响应的时间(以秒为单位)。默认设置为1800秒(30分钟)。 } }
(注意:SpringBoot2.3 通过继承WebMvcConfigurerAdapter该类,2.4以后的版本需要实现WebMvcConfigurer该接口,因为SpringBoot2.4以后废弃了WebMvcConfigurerAdapter该类,不过方法内部的操作没有区别。)
2.2 服务端方法级解决方案
在方法层面控制是否跨域我们只需要一个注解CrossOrigin即可,该注解就是专门用于控制跨域访问的注解。CrossOrigin使用在类层面上代表该类下所有接口均可被跨域访问,使用在方法上则只有被该注解标注的方法才可以被跨域访问。该注解支持的属性与第一种通过配置的没有什么区别,所以建议还是使用全局配置更为方便,但要是追求灵活性还是需要使用注解,注解的使用形式如下:
@CrossOrigin(origins = "http://www.baidu.com" ,allowCredentials = "false" ,methods = RequestMethod.POST ,maxAge = 1800,allowedHeaders = "*") @RestController("/supplier") public class SupplierController { @PostMapping("/query") public void queryDetail(String oid){ } }
3.JSONP解决和webSocket解决(不推荐)
这两种方法都是已经过时的解决方案,当下主流的肯定不会使用,CORS已经完美的取代了JSONP,webSocket使用场景也很少,笔者认为没有必要去研究这两种策略了,这里给出一位作者关于这两点的解释,供有需要的人作为参考:点击这里查看
四、总结
这里介绍了什么是跨域,跨域的危害、哪些情况会有跨域、怎么解决跨域,尤其是前后端分离的项目,跨域是必须解决的问题,笔者也是碰到过类似的问题,希望这里的微薄分享可以对路过的你有所启发。