版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/f641385712/article/details/100999550
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,表示请求被服务端正常处理了),形如这样:
看似相悖的结果,这到底怎么回事???本文就告诉你答案
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。该策略是浏览器最核心也最基本的安全功能,同源指的是:同协议、同域名、同端口。
它的核心思想可以理解为:我只相信我同一个域的资源,来自于其它域的我都不可信,所以同源策略主要还是出于安全考虑的~
JavaScript
或Cookie
只能访问同源(同协议、同域名、同端口下的内容。
CORS
它是W3C(万维网联盟)的标准,它定义了在跨域访问资源时浏览器和服务器之间如何通信。它是为突破同源策略的限制而出现的一种官方标准的跨域解决方案。在实战场景中,跨域场景太为常见了(特别是当下前后端分离的开发模式),因此深入理解CORS
变得就异常的重要了(反倒前端工程师不用太了解)。
若想实现CORS
机制的跨域请求,是需要浏览器和服务器同时支持的。关于浏览器对CORS
的支持情况:现在都9012年了,so可以认为100%的浏览器都是支持的,再加上CORS
的整个过程都由浏览器自动完成,前端无需做任何设置,所以你的ajax
原来怎么用现在还是怎么用,它对前段开发人员是完全透明的。
所以呢,让此种机制生效的关键就在于服务器端,so作为服务端开发的我们,必须要玩转CORS
才可正常实现跨域通信。
CORS
机制的指导思想:自定义的HTTP头部允许浏览器和服务器相互了解对方,从而决定请求或响应成功与否
这是跨域请求产生的背景,最主要是随着互联网的发展,忘了改善网络应用程序的环境增强其功能,开发人员要求浏览器供应商允许跨域请求,能带来如下好处:
@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
发送出来的请求分为两种:
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等等
一些内置的头之外的头都叫自定义)OPTIONS
请求)。很显然,不满足简单请求三大要求的便都是非简单请求喽。在实际生产应用场景中我们最为常见的非简单请求场景大致有如下三种case:
Content-Type
为application/json
)Authorization
)对于这种请求,浏览器是直接发出请求,它的特点是:浏览器自动给加上一个Origin
的请求头,表示这个请求的来源(来自哪个源)。
比如上面案例的请求,它完全符合简单的请求的三大要求,所以它是一个简单请求,浏览器自动给它加上的头是:Origin: http://localhost:63342
。
服务端可拿到这个Origin
源,然后判断服务端是否能够接受这个源从而决定是否同意这次请求(不同意or同意):
Access-Control-Allow-Origin
这个头),浏览器发现木有这个头,就抛出一个错误XMLHttpRequest
,进而进入ajax的onerror
回到方法里(这就是为何你明明看到http状态码是200,response也有返回值,但偏偏你ajax里就是进入的error的原因~),它的现象是:服务器正常返回了资源,但浏览器拒绝接收了。和简单请求相关的3个响应头如下:
该响应头是服务器必须返回的。它的值要么是请求时Origin
的值(可从request里获取),要么是*
这样浏览器才会接受服务器的返回结果。
该响应头非必须,值是bool类型,表示是否允许发送Cookie
cookie
,不要设置此响应头即可Tips:浏览器端默认情况下,
Cookie
不包括在CORS请求之中,若你想让浏览器带上Cookie
,有需要的请自行研究一番吧~
该响应头非必须。顾名思义它要把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请求都属于"非简单请求"(比如请求方法是PUT
或DELETE
)。它最大的一个特点是:在发送正式请求通信之前,增加一次HTTP OPTIONS
请求,这个请求称之为预检(preflight)请求
。
发送
OPTIONS
预检请求的过程完全由浏览器自动完成,开发者无需关心。
预检请求:它的作用是试探服务端是否能接受真正的请求,若服务器返回的状态码不是2xx
而是4xx/5xx
的话,那么浏览器将停止发送真正的请求。OPTIONS
请求它具有如下特征:
Invalid CORS request
)下面先看一个非简单请求的例子,只需要把上例的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-Request-Headers
,那响应头中这个头就是必须的,否则是非必须的。它的值是逗号分隔的字符串,表示我服务器支持的所有头字段,不限于预检请求中的头字段(但请包含它~)。可以用*代替
说明:若请求头中有
Access-Control-Request-Headers
,但是没有此响应头/响应头中的值不包含请求头的值。那么出现的奇异现象便是:OPTIONS
请求正常200返回,但是真实请求就不会发送了。所以使用时请务必注意~
非必须。它表示需要缓存预检结果多长时间,单位是秒。比如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
这个响应头形同虚设了~
改进方案:默认动态查询参数不要精确到毫秒,绝大多数情况下精确到当前小时、天是足够了的,最不济分钟级别也够了吧~~~
最终一个小知识点补充。JSONP
是一个相对比较古老的用于解决跨域问题的技术了,对于新生代的程序员来说几乎可以忽略掉它,因为已经完全被新时代的CORS
所代替,把前浪拍死在沙滩上。
它哥俩都能解决浏览器Ajax请求资源的跨域问题,有些不同的点总结如下:
JSONP
只能实现GET请求(让支持其余请求将非常麻烦),CORS
支持所有类型的HTTP请求CORS
,我们可以通过XMLHttpRequest
直接完成请求发起和获取数据,因为都是这一个对象,所以处理错误更加方便JSONP
的唯一优势:支持更老的浏览器(现在都9012年了,相信木有了)。CORS
现已是官方的标准实现规范,几乎所有浏览器都支持得很好~withCredentials
的cookie认证)OPTIONS
预检请求,无疑增加了系统的开销(本一个请求搞定的变成了需要两个请求),所以需要做好缓存策略以及确保缓存能够生效OPTIONS
请求和实际请求的发送时间间隔非常短,此时若你限流如:同一IP每秒只能访问1次,那真实请求就会被拒绝了,因此此时就要排除掉OPTIONS
这种预检请求的影响OPTIONS
方法进行特殊处理的,否则可能就会执行多次造成一些麻烦CORS
(跨域资源共享)是一种浏览器端的机制,它在现在前后端完全分离开发主流的今天还是蛮重要的概念,即使它比较简单。
需要注意的是:既然它是浏览器端的一种机制,所以它是可以被浏览器关闭这种机制的,至于如何do,有兴趣的可自行度娘~
在实战场景中:能控制服务器的情况下,一般都是服务器上正确配置CORS。可以在服务器API层(Controller
层)进行精细化控制配置,也可以在nginx
层进行统一配置(这样后端新加服务器不用再配置),最好配置上白名单而不是简单的粗暴的全是*
。
本文主要以介绍CORS
概念为主,然后结合一个实例介绍了它的使用和结果分析。但至少看完本文后你应该留有如下疑问待解决:
Spring MVC
对CORS
的支持原理、使用方式是怎样的?OPTIONS
请求就不进入Handler方法进行处理呢