请求成功案例
为了写出一个完全正确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请求它具有如下特征:
- 没有请求body体
- 可以有响应body体(比如我们熟悉的: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-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请求,结果截图如下:
非简单请求跨域成功。从截图的结果上还能看到Access-Control-Max-Age它的功效,它能够减少OPTIONS请求的发送,从而减轻对服务端的访问压力。
如何理解Access-Control-Max-Age对相同URL生效???
为了更好理解这个响应头的作用,我针对性的做出如下试验:
为了测试,我把Access-Control-Max-Age设为了24小时,以保证缓存“永不过期”(控制变量法)






