CORS跨域资源共享(一):模拟跨域请求以及结果分析,理解同源策略【享学Spring MVC】

每篇一句

talk is cheap,show me the money.

前言

CORS的全称是:跨域资源共享(Cross-origin resource sharing),它是浏览器的一个技术规范。
浏览器自己是可以发起跨域请求的(比如你可以外链一个外域的图片或者视频),但是Javascript脚本是不能跨域去获取这些资源的内容的。传统的ajax请求只能获取在同一个域名下的资源,但是Html5打破了这个限制:允许ajax发起跨域请求。跨域的解决方案有多种:JSONP、Flash、IFrame等,当然还有今天的主菜CORS

我有理由相信若你在前端使用过Ajax,你100%遇见过如下图这样的报错:
在这里插入图片描述
若你看到这样的报错,那么此次你的请求
返回数据是失败的(请务必理解这句话)
。但是,但是,但是若你查看调试工具的Network栏,发现这个URL请求的response是有返回值的(并且http状态码是200,表示请求被服务端正常处理了),形如这样:
在这里插入图片描述
在这里插入图片描述
看似相悖的结果,这到底怎么回事???本文就告诉你答案




同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。该策略是浏览器最核心也最基本的安全功能,同源指的是:同协议、同域名、同端口。

它的核心思想可以理解为:我只相信我同一个域的资源,来自于其它域的我都不可信,所以同源策略主要还是出于安全考虑的~

JavaScriptCookie只能访问同源(同协议、同域名、同端口下的内容。

CORS

CORS它是W3C(万维网联盟)的标准,它定义了在跨域访问资源时浏览器和服务器之间如何通信。它是为突破同源策略的限制而出现的一种官方标准的跨域解决方案。在实战场景中,跨域场景太为常见了(特别是当下前后端分离的开发模式),因此深入理解CORS变得就异常的重要了(反倒前端工程师不用太了解)。

若想实现CORS机制的跨域请求,是需要浏览器和服务器同时支持的。关于浏览器对CORS的支持情况:现在都9012年了,so可以认为100%的浏览器都是支持的,再加上CORS的整个过程都由浏览器自动完成,前端无需做任何设置,所以你的ajax原来怎么用现在还是怎么用,它对前段开发人员是完全透明的。
所以呢,让此种机制生效的关键就在于服务器端,so作为服务端开发的我们,必须要玩转CORS才可正常实现跨域通信。

CORS机制的指导思想:自定义的HTTP头部允许浏览器和服务器相互了解对方,从而决定请求或响应成功与否

为何需要跨域请求???

这是跨域请求产生的背景,最主要是随着互联网的发展,忘了改善网络应用程序的环境增强其功能,开发人员要求浏览器供应商允许跨域请求,能带来如下好处:

  • javascript可以使用ajax方式跨域访问资源
  • CSS可以使用@font-face跨域调用字体
  • 通过canvas标签,绘制图表和视频

由此可见:跨域不仅仅是ajax的专属

本地模拟跨域请求以及结果分析

上面都是成套成套的理论知识,过于抽象。那接下来我就是要通过本地的实例来模拟出跨域请求,从而依托于案例分析CORS各种不同的case情况下的结果分析。

1、写一个前端HTML页面放于idea(idea可充当静态web服务器)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>CORS跨域</title>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
<div style="text-align:center;margin-top: 100px;font-size: 60px;color: brown;cursor: pointer;">
    <span onclick="sendAjaxReq()">发送Ajax请求</span>
</div>
<script type="text/javascript">
    function sendAjaxReq() {
        $.ajax({
            type: "GET",
            // contentType: "application/json",
            url: "http://localhost:8080/demo_war_war/test/cors,
            success: function (message) {
                console.log("成功!" + message);
            },
            error: function (a, b, c) {
                console.log("失败!" + a.statusText);
            }
        });
    }
</script>
</body>
</html>

2、写一个控制器Controller处理页面发送的ajax请求

@RestController
public class CorsController {

    @GetMapping("/test/cors")
    public Object testCors() {
        return "hello cors";
    }
}

3、利用idea的web服务器能力运行html页面,如下截图(本例使用的是标准的63342静态web端口)
在这里插入图片描述
请注意这个页面的访问地址的是http://localhost:63342...,而点击这个"发送Ajax请求"按钮要发送的地址是http://localhost:8080...两者端口号不一样说明是不同的域,因此此ajax请求它必定属于跨域请求(CORS请求)。

4、点击发送按钮,查看控制台的结果
这个case的结果请完全参照文首的几张截图,此处就省略了

Tips:如果域名连不上服务端(比如服务端木有启动),它的报错一般都会是网络连接方面的问题,形如:GET http://localhost:8080/demo_war_war/test/cors net::ERR_CONNECTION_REFUSED,请注意区分~

如上结果,命名返回了200但浏览器偏偏还是报跨域异常,我相信这个让你感觉到十分的诧异和不解,那么接下来就围绕它来解释通这个问题。
但在我解释此现象之前,必须先要弄明白两个非常重要的CORS请求类型:简单请求,非简单请求(说明:这两种请求都属于CORS请求,这是大前提)。


简单请求、非简单请求

CORS发送出来的请求分为两种:

  1. 简单请求。需要同时满足下面三个要求
    1. 请求方法只能是GET、POST、HEAD
    2. Content-Type只能是三个值的任意一个application/x-www-form-urlencoded、multipart/form-data、text/plain(备注:若使用jquery的ajax发送请求,没指定Content-Type的情况下,默认它的值是application/x-www-form-urlencoded。源生的ajax请求请手动显示指定)
    3. 无自定义请求头(除了Accept、Content-Type等等一些内置的头之外的头都叫自定义)
  2. 非简单请求。除了简单请求之外都是它(带预检,也就是我们常见的OPTIONS请求)。

很显然,不满足简单请求三大要求的便都是非简单请求喽。在实际生产应用场景中我们最为常见的非简单请求场景大致有如下三种case:

  1. ajax发送put、delete请求
  2. 发送json格式数据(Content-Typeapplication/json
  3. 自定义请求头(比如自定义鉴权请求头Authorization
简单请求

对于这种请求,浏览器是直接发出请求,它的特点是:浏览器自动给加上一个Origin的请求头,表示这个请求的来源(来自哪个源)。
比如上面案例的请求,它完全符合简单的请求的三大要求,所以它是一个简单请求,浏览器自动给它加上的头是:Origin: http://localhost:63342
服务端可拿到这个Origin源,然后判断服务端是否能够接受这个源从而决定是否同意这次请求(不同意or同意):

  • 不同意:服务器会返回一个正常的HTTP回应(响应头里木有Access-Control-Allow-Origin这个头),浏览器发现木有这个头,就抛出一个错误XMLHttpRequest,进而进入ajax的onerror回到方法里(这就是为何你明明看到http状态码是200,response也有返回值,但偏偏你ajax里就是进入的error的原因~),它的现象是:服务器正常返回了资源,但浏览器拒绝接收了。
  • 同意:服务器的响应里会多出下面详解的几个响应头,从而回调ajax的onsuccess方法,这就是真正意义上的成功了,浏览器也接收了这个返回结果。
和简单请求相关的3个响应头如下:
Access-Control-Allow-Origin

该响应头是服务器必须返回的。它的值要么是请求时Origin的值(可从request里获取),要么是*这样浏览器才会接受服务器的返回结果。

Access-Control-Allow-Credentials

该响应头非必须,值是bool类型,表示是否允许发送Cookie

  • true:表示服务器允许你浏览器把cookie发给我(若服务器想获取Cookie的,请务必设置此值)
  • false :请注意此字段只能设置为true,若不允许发送cookie,不要设置此响应头即可

Tips:浏览器端默认情况下,Cookie不包括在CORS请求之中,若你想让浏览器带上Cookie,有需要的请自行研究一番吧~

Access-Control-Expose-Headers

该响应头非必须。顾名思义它要把response中的哪些头暴露给浏览器,让它可以获取到(默认情况下浏览器的XMLHttpRequest对象的getResponseHeader()方法只能获取到那些Cache-Control、Expires等等几个标准的响应头,若需要拿其它key,需要在这里指定)

请求成功案例

为了写出一个完全正确CORS简单请求,基于本例我只需要加一句代码即可:

@GetMapping("/test/cors")
public Object testCors(HttpServerResponse response) {
	// HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN
    response.addHeader("Access-Control-Allow-Origin", "http://localhost:63342");
    return "hello cors";
}

再次点击按钮发送ajax请求结果如下:
在这里插入图片描述
大功告成。服务端不仅仅正常处理了请求,浏览器也接受了返回值。

对于简单请求请务必杜绝这种case:返回的状态码是200(服务端逻辑正常执行且正常返回了),浏览器不会接收结果,而是回调到error方法去~

非简单请求

顾名思义,它比简单请求就要复杂些,不是简单请求的CORS请求都属于"非简单请求"(比如请求方法是PUTDELETE)。它最大的一个特点是:在发送正式请求通信之前,增加一次HTTP OPTIONS请求,这个请求称之为预检(preflight)请求

发送OPTIONS预检请求的过程完全由浏览器自动完成,开发者无需关心。

预检请求:它的作用是试探服务端是否能接受真正的请求,若服务器返回的状态码不是2xx而是4xx/5xx的话,那么浏览器将停止发送真正的请求。OPTIONS请求它具有如下特征:

  1. 没有请求body体
  2. 可以有响应body体(比如我们熟悉的:Invalid CORS request
  3. 安全
  4. 幂等性
  5. 不能缓存,不能在表单里使用

下面先看一个非简单请求的例子,只需要把上例的Ajax注释的contentType放开即可,它便轻松成为了一个非简单请求了:

...
contentType: "application/json",
...

点击发送按钮,结果截图如下:
在这里插入图片描述
在这里插入图片描述
OPTIONS请求返回的状态码是403,所以真实的请求并未发送(network栏只有一个请求~)。浏览器自动添加的请求头中,最重要的仍然是Origin这个头,例如我们生产环境的请求头如下:
在这里插入图片描述
另外两个请求头解释如下(虽然不是十分重要,但也是必须了解的):

  • Access-Control-Request-Method:该请求头是必须的。发给服务器告知我接下来的真实方法是啥,本例是GET;
  • Access-Control-Request-Headers:非必须(因为可能无自定义的请求头嘛)。若有多个是逗号分隔,告诉服务器我真实请求即将携带的请求头是哪些,本例是content-type这一个请求头;

这些请求头最终都发送给服务器,服务器收到这个预检请求后判断,检查这些头,确认允许跨域与否就可以做出相应的回应了(本例回应403:Forbidden)。

和非简单请求相关的5个响应头如下:
Access-Control-Allow-Origin和Access-Control-Allow-Credentials

同上

Access-Control-Allow-Methods

必须的相应头,值是逗号分隔的字符串。表明我服务器可以支持的所有跨域请求的方法~可以用*代替

注:为何返回的不单单是马上要发真实请求的那个方法,而是多个呢???这是为了避免多次"预检"请求,提高效率。后面你可以看到它的功效

Access-Control-Allow-Headers

若请求头中包含Access-Control-Request-Headers,那响应头中这个头就是必须的,否则是非必须的。它的值是逗号分隔的字符串,表示我服务器支持的所有头字段,不限于预检请求中的头字段(但请包含它~)。可以用*代替

说明:若请求头中有Access-Control-Request-Headers,但是没有此响应头/响应头中的值不包含请求头的值。那么出现的奇异现象便是:OPTIONS请求正常200返回,但是真实请求就不会发送了。所以使用时请务必注意~

Access-Control-Max-Age(重要)

非必须。它表示需要缓存预检结果多长时间,单位是秒。比如Access-Control-Max-Age: 600表示将预检结果缓存10分钟,即表示10分钟之内同样的URL将不再发送预检请求。如果值是0表示不用缓存~

Tips:因为它对url生效,所以对那些默认的查询条件取当前时间戳的可千万别这么干了,一般我相信你精确到日期就够了而不用精确到毫秒吧,否则age就不生效了(每次都还得发送预检请求)

当然,你的浏览器也是可以禁用掉这种缓存的。
在这里插入图片描述

请求成功案例

它和简单请求的处理方式是不一样的,因为OPTIONS请求进入不了Handler方法,所以在Controller里向HttpServletResponse里设置请求头是无效的。
因此我们应该把设置相应头信息放在Filter/HandlerInterceptor上才行,本例以Spring MVC的拦截器为例(生产上推荐使用Filter):

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	// 这几个响应头都是可以用*来表示所有的
    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "*");
    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
    response.addHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "60");
    return true;
}

请注意:这些添加header只能放在preHandle,放在postHandle/afterCompletion里都将不生效(network里能看到这个头,但是无效果

配置好后,点击按钮发送Ajax请求,结果截图如下:
在这里插入图片描述
在这里插入图片描述
从这张截图可以看到:我点击了3此发送切都成功了,再回头看看network:
在这里插入图片描述
非简单请求跨域成功。从截图的结果上还能看到Access-Control-Max-Age它的功效,它能够减少OPTIONS请求的发送,从而减轻对服务端的访问压力。

如何理解Access-Control-Max-Age对相同URL生效???

为了更好理解这个响应头的作用,我针对性的做出如下试验:

为了测试,我把Access-Control-Max-Age设为了24小时,以保证缓存“永不过期”(控制变量法)

1、相同URL,不同的请求Method
页面改造如下,以保证先后发送一个GET请求和一个POST请求,同时Controller也增加对POST请求的支持

<script type="text/javascript">
    var url = "http://localhost:8080/demo_war_war/test/cors";
    function sendAjaxReq() {
        $.ajax({
            type: "GET",
            contentType: "application/json",
            url: url,
            success: function (message) {
                console.log("成功!" + message);

                // 成功里立马再发一次请求:url一样 但是POST请求
                $.ajax({
                    type: "POST",
                    contentType: "application/json",
                    url: url,
                    success: function (message) {
                        console.log("成功!" + message);
                    }
                });
            },
            error: function (a, b, c, d) {
                console.log("失败!");
            }
        });
    }
</script>
// 支持GET和POST请求的处理
@RequestMapping(value = "/test/cors", method = {GET, POST})
public Object testCors(HttpServletResponse response) {
    return "hello cors";
}

答案:发送一次OPTIONS请求

2、相同URL,相同的请求Method(POST请求为例),不同的请求body体
答案:发送一次OPTIONS请求
3、相同的URL,不同Method、不同body体
答案:发送一次OPTIONS请求
4、不同的URL
答案:发送两次OPTIONS请求

实验证明:在缓存还生效的情况下,是否再次发送OPTIONS请求只和URL有关,只要URL不变,都不会再次发送OPTIONS请求了~
这就警示我们:那些URL中有默认动态查询参数的(如当前时间戳)请务必注意了,如果每次都获取当前时间戳,那就导致每次URL都是不一样的,那就让Access-Control-Max-Age这个响应头形同虚设了~

改进方案:默认动态查询参数不要精确到毫秒,绝大多数情况下精确到当前小时、天是足够了的,最不济分钟级别也够了吧~~~


CORS和JSONP对比

最终一个小知识点补充。JSONP是一个相对比较古老的用于解决跨域问题的技术了,对于新生代的程序员来说几乎可以忽略掉它,因为已经完全被新时代的CORS所代替,把前浪拍死在沙滩上。

它哥俩都能解决浏览器Ajax请求资源的跨域问题,有些不同的点总结如下:

  1. JSONP只能实现GET请求(让支持其余请求将非常麻烦),CORS支持所有类型的HTTP请求
  2. 使用CORS,我们可以通过XMLHttpRequest直接完成请求发起和获取数据,因为都是这一个对象,所以处理错误更加方便
  3. JSONP的唯一优势:支持更老的浏览器(现在都9012年了,相信木有了)。CORS现已是官方的标准实现规范,几乎所有浏览器都支持得很好~
CORS带来的问题
  1. 带来的安全隐患,最主要的便是著名的跨站请求伪造CSRF(Cross-site request forgery),所以要做好这块的安全工作(建议可开启withCredentials的cookie认证)
  2. 因为增加了OPTIONS预检请求,无疑增加了系统的开销(本一个请求搞定的变成了需要两个请求),所以需要做好缓存策略以及确保缓存能够生效
  3. 可能影响到你的限流,需要特殊处理。由于OPTIONS请求和实际请求的发送时间间隔非常短,此时若你限流如:同一IP每秒只能访问1次,那真实请求就会被拒绝了,因此此时就要排除掉OPTIONS这种预检请求的影响
  4. 同样的,若你的Filter/拦截器里,若有需要也是要对OPTIONS方法进行特殊处理的,否则可能就会执行多次造成一些麻烦
相关阅读

CORS跨域资源共享(一):模拟跨域请求以及结果分析,理解同源策略【享学Spring MVC】
CORS跨域资源共享(二):详解Spring MVC对CORS支持的相关类和API【享学Spring MVC】
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例,原理分析【享学Spring MVC】

总结

CORS(跨域资源共享)是一种浏览器端的机制,它在现在前后端完全分离开发主流的今天还是蛮重要的概念,即使它比较简单。
需要注意的是:既然它是浏览器端的一种机制,所以它是可以被浏览器关闭这种机制的,至于如何do,有兴趣的可自行度娘~

在实战场景中:能控制服务器的情况下,一般都是服务器上正确配置CORS。可以在服务器API层(Controller层)进行精细化控制配置,也可以在nginx层进行统一配置(这样后端新加服务器不用再配置),最好配置上白名单而不是简单的粗暴的全是*

本文主要以介绍CORS概念为主,然后结合一个实例介绍了它的使用和结果分析。但至少看完本文后你应该留有如下疑问待解决:

  • 有没有通用的跨域解决方案?
  • Spring MVCCORS的支持原理、使用方式是怎样的?
  • 为何OPTIONS请求就不进入Handler方法进行处理呢

显然这是下文讲述的内容,敬请关注~


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

  • 19
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值