前言
CORS的全称是:跨域资源共享(Cross-origin resource sharing),它是浏览器的一个技术规范。
浏览器自己是可以发起跨域请求的(比如你可以外链一个外域的图片或者视频),但是Javascript脚本是不能跨域去获取这些资源的内容的。传统的ajax请求只能获取在同一个域名下的资源,但是Html5打破了这个限制:允许ajax发起跨域请求。跨域的解决方案有多种:JSONP、Flash、IFrame等,当然还有今天的主菜CORS。
我有理由相信若你在前端使用过Ajax,你100%遇见过如下图这样的报错:
若你看到这样的报错,那么此次你的请求返回数据是失败的(请务必理解这句话)。但是,但是,但是若你查看调试工具的Network栏,发现这个URL请求的response是有返回值的(并且http状态码是200,表示请求被服务端正常处理了),形如这样:
看似相悖的结果,这到底怎么回事???本文就告诉你答案
同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。该策略是浏览器最核心也最基本的安全功能,同源指的是:同协议、同域名、同端口。
它的核心思想可以理解为:我只相信我同一个域的资源,来自于其它域的我都不可信,所以同源策略主要还是出于安全考虑的~
JavaScript或Cookie只能访问同源(同协议、同域名、同端口下的内容。
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. 请求方法只能是GET、POST、HEAD2. 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:
- ajax发送put、delete请求
- 发送json格式数据(Content-Type为application/json)
- 自定义请求头(比如自定义鉴权请求头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,需要在这里指定)