type
status
date
slug
summary
tags
category
icon
password
前言
本文记录了我探索跨域问题的过程,在将其写成博文的过程中亦遇到新的问题,便停下笔去解决,所以整篇文章有明显的思考路径在。但是这样的写作方式也会导致绝大部分人无法跟着思考,因为大家都是有问题再来查找答案的,有明确的目的,而不会有很多耐心这样一步一步跟下去。所以,我也要探索更好的写作方式。
本文以跨域请求服务器端是如何处理的,是否会执行业务代码,浏览器又是如何处理的这个疑问为出发点,以实验的形式探索了跨域在B/S端的处理方式,实验首先以自己实现CORS基本规范来得出结果,随后又结合标准探索了Spring的
@CrossOrigin
注解的实现以及源码解读。在这个过程中发现了很多支线问题,并一一解决并记录。\正文
怎么算跨域
URL中的协议、域名、端口一致则称为同源,不一致则不同源,也就是跨域。
浏览器对跨域的默认策略
浏览器会限制:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获得
- AJAX 请求的响应不能接收到
本文只讨论第三种情况。
浏览器具体是如何做到的
具体实现方式就是,
XMLHttpRequest
和Fetch
这两个API都部署了同源限制。具体可以去看这两个API的实现。跨域有哪些解决办法
CORS 跨域资源共享
——是一套机制/规范。 它由一系列请求/响应
头(主要是响应头,也就是说,CORS主要是服务器方来实施的,也就是让服务器告知浏览器,这个域名是否被允许访问我的资源)中包含的特定HTTP头组成。浏览器通过读取这些头信息可以了解到这个跨源资源是不是应该被读取。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源通信没有差别,代码完全一样。浏览器一旦发现请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
CORS包含的HTTP头
2. 可以通过修改
document.domain
,其允许将同源检测的目标提升到父域。
3. JSONP
4. WebSocket
5. 服务器代理
跨域限制是被浏览器限制的,因此如果用服务器作为代理来请求跨域资源,就不会有这个限制了。
[跨域基础知识]简单请求和复杂请求(是否触发预检)
日常开发中,可以认为满足以下三点的请求为简单请求。
- 请求方式为
- get
- post
- head
- 只有最常见的请求头
- 比较原始的 Content-Type(像 application/json 就不行)
其余的都是复杂请求。当然这并不全面,想要知道所有条件,请去 MDN 中查阅。
浏览器对于简单请求的处理过程:
- 浏览器正常发送跨域请求,并在请求头中附加Origin字段标明请求来源。
- 如果服务器不做出跨域的正确响应(添加
Access-Control-Allow-Origin
等字段),则浏览器会拦截返回的内容。
- 服务器正确响应,则一切都照常。
对与复杂请求,上述基础依然适用于第一次发出的预检请求,而如果预检请求没有收到服务器的允许信号的话,则正式请求会被浏览器在发送之前拦截。
验证无效跨域访问得不到响应数据是被浏览器拦截,而非服务端未执行
实验环境搭建
实验后端接口如图,分为简单请求和预检请求,各组 API分 为三个,分别为 无任何处理、只添加
Access-Control-Allow-Origin
、都添加: 简单请求
预检请求
开始实验
简单请求
所有简单请求一次性发送:
结果分析:
通过控制台能发现,简单请求只需要携带至少一个
Access-Control-Allow-Origin
字段即可运作。而show接口在浏览器中发生CORS错误是可以预料到的。那么他在后台执行了吗?通过查看后端运行日志:可以发现show也被执行了(对应第一条:’执行了‘)。回到前端,show的http头信息:
可以看到,状态码是200。但是通过响应一栏就能看出,结果被浏览器拦截了:
为了缩短篇幅,其他一些情况我就直接说实验结论,不贴过程了:如果将接口改为这样,又会如何呢?答案是,前端无法获得响应数据,但是接口会被执行。
实验结论:
综上可以得出:如果服务器没有配置CORS,则==简单==跨域请求可以成功执行,但是返回的内容会被浏览器拦截!
预检请求
http头信息
详情
不带CORS处理
只添加
Access-Control-Allow-Origin
预览也无法显示,其在状态一栏显示错误提示:
MethodDisallowedByPreflightResponse
都添加(预检)
符合预期
都添加(正式)
符合预期
结合后端运行日志:
结果分析:
三个预检请求对应前三条输出。但是这里其实有几个问题。
- 预检请求为什么会被分发到
@PutMapping
预检请求是用的
OPTIONS
请求方法,但是我后端根本没配置OPTIONS
啊!通过积极地查阅文档,我终于发现了关于这个的说明:22. Web MVC framework (spring.io)HTTP HEAD and HTTP OPTIONS
@RequestMapping
methods mapped to “GET” are also implicitly mapped to “HEAD”, i.e. there is no need to have “HEAD” explicitly declared. An HTTP HEAD request is processed as if it were an HTTP GET except instead of writing the body only the number of bytes are counted and the “Content-Length” header set.@RequestMapping
methods have built-in support for HTTP OPTIONS. By default an HTTP OPTIONS request is handled by setting the “Allow” response header to the HTTP methods explicitly declared on all@RequestMapping
methods with matching URL patterns. When no HTTP methods are explicitly declared the “Allow” header is set to “GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS”. Ideally always declare the HTTP method(s) that an@RequestMapping
method is intended to handle, or alternatively use one of the dedicated composed@RequestMapping
variants (see the section called “Composed @RequestMapping Variants”).Although not necessary an@RequestMapping
method can be mapped to and handle either HTTP HEAD or HTTP OPTIONS, or both.
这里面解释了,
RequestMapping
默认是可以处理HEAD
、OPTIONS
方法的请求的。默认OPTIONS
方法不会被DispatchServlet
单独分发,不过也可以通过配置达到让RequestMapping
可以声明method=options
,详见:- 预检请求会带参数吗
我修改了其中一条进行测试:
预检
正式
得出结论,预检请求不携带查询参数,正式请求会携带。
- 方法不被允许会怎么样
只添加
Access-Control-Allow-Origin
这一组结果中,在状态一栏显示错误提示:MethodDisallowedByPreflightResponse
。即表示方法不被允许,实际后端根本没有配置Access-Control-Allow-Methods
。这个是带预检请求的情况,可以在回顾一下简单请求对于方法不支持的处理:发现简单请求,即使响应方法不支持,依然还会执行,且能拿到返回值。 由此可以判断,简单请求的跨域只关心
Access-Control-Allow-Origin
字段,并忽略Access-Control-Allow-Methods
字段。回到上面的实验结果分析,服务器日志的最后一条输出对应前端发出的那个正式的
showWithPreflightAndAC
请求。剩下的两条发生CORS错误的请求,后端并没有执行!
实验结论:
相比简单请求,预检请求是不带参数的,如果服务器并没有配置
CORS
,则OPTIONS
请求方法默认也能被任意的URL
匹配的RequestMapping
捕获执行,且返回内容一样被浏览器拦截,之后并不会发送正式请求。总结
综上,在一个简单的前后端请求响应模型中,对没有配置CORS的服务器发送 跨域简单请求 ,能够执行接口代码,但是返回数据会被浏览器拦截丢弃!
如果对正确配置了CORS的服务器发送了跨域简单请求的话,则一切正常!
而预检请求有细微不同,预检请求会被对应接口代码执行,仅当响应头中同时包含正确的
Access-Control-Allow-Origin
、Access-Control-Allow-Methods
时,正式请求才会决定发送,否则,就不是简单请求那样,接口照样调用,只是无法获取返回值这么便宜了,而是接口代码直接无法执行了(其实也会被预检请求执行一次)。 至于这个正式请求是被浏览器拦截的还是被服务器拦截的,根据网上的描述,应该是被浏览器。根据 CORS 通信 - JavaScript 教程 - 网道 (wangdoc.com) 中的描述:当预检响应后,如果不符合跨域条件,XMLHttpRequest就会触发一个onerror事件。而为什么浏览器网络中会显示那条错误的CORS通信呢?回到那个图:
查看一下关于临时标头的解释:
目前的情况应该是匹配第二条,也就是说,这个网络资源被浏览器判定为无效。结合上面
wangdoc
文章的描述能更加印证这一点。所以是浏览器选择了不发送正式请求。(额外)Fetch请求
上文都是对XHR请求的实验,而本篇博客诞生的原因其实是因为Fetch。 博主在第一次接触跨域的时候,用fetch接口成功在服务端无配置的情况下,通过浏览器控制台观察到了响应结果:
但是虽然能看到,但是代码却无法取到数据!由此可见fetch在跨域方面是具有一定的特殊性的。所以下面也做一个简单的分析。
思考
到这里就完整的验证了实验。但是是不是有些不符合预期?因为像现在这样肯定是不行的,如果接口触发了业务逻辑,那么预检请求就已经触发了,如果服务端的意思本来是不允许跨域请求,那依然执行了完整代码,不仅有业务风险,更是浪费算力,浪费带宽。所以后端针对预检请求应该有个专门的地方,只设置响应头信息,不做其他处理。
关于 CORS 的一个问题,大家怎么看 - V2EX —— 我一开始以为跨域就是这么配置的,所以上面的问题困扰了我很久,直到该贴18楼的解释将我带出了胡同
一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
对应到我们的demo中来,如果响应的设置被放到了一个单独的位置,
@PutMapping
中仅有逻辑代码,那服务器如果不做更多处理,会自动添加Access-Control-Allow-Origin
字段么?所以正确的跨域后端应该如何编写呢?这个问题的答案等下随我们探究Spring提供的跨域方案揭开。
用@CrossOrigin
注解实现CORS
以上都是我们自己实现CORS,来看看Spring是如何处理的:
Spring提供了
@CrossOrigin
注解来实现跨域访问,注解可以加载类或方法上,且支持丰富的跨域选项配置。@CrossOrigin
实现效果自己简陋实现的效果
预检
正式
通过对比可以看出,
@CrossOrigin
的做法更符合网上说明的标准:- 预检中,针对性地在响应头中设置
Access-Control-Allow-Methods
并为了减少预检次数,默认添加了30分钟的Access-Control-Max-Age
。
- 正式响应中,只添加
Access-Control-Allow-Origin
字段,以及Vary
字段。
还需要注意的是,我们看看后端日志信息:
@CrossOrigin
实现效果自己简陋实现的效果
自己实现的由于比较粗糙,并没有做预检请求区分,所以自然而然调用了业务逻辑。而注解则避免了请求接口逻辑,这点就很符合预期了,然后自然而然地,就很想知道这是如何实现的。别急,先多了解一点
@CrossOrigin
,等下去探究源码。如果注解是加在简单请求中的话,那响应头的信息又是如何呢?
@CrossOrigin
实现效果自己简陋实现的效果
发现只是多了三个Vary字段。
对于这几个不熟悉的字段,去了解一下:
- Vary
vary跟浏览器缓存有关,不配的话,可能导致配了CORS但是被缓存干扰而无效化
- Content-Length
消息长度,请求头和响应头中都有可能出现。 最简单的例子,比如一个ajax请求,发送了一个携带
name=aaa
的POST请求,则请求头的Content-Length为8,也就是那个字符串的长度, 如果有用压缩的话,则是压缩后的长度。响应也是如此。 那些请求头、响应头数据不计算在其中。结合前面的例子,其实可以发现。 那些被浏览器拦截掉了的CORS请求,其实响应头中的Content-Length和服务器设置的返回内容长度是一致的。- Allow
服务器允许的该资源的所有请求方式
接下来来看看源码,此时的我有个疑问,是Spring单方面遵从标准进行的部署还是利用接口调用了某个更底层特性实现? 看完源码将一切开朗。
<ins/>
(源码解读)Spring 是如何实现跨域的配置的
How does it work?
CORS requests (including preflight ones with anOPTIONS
method) are automatically dispatched to the variousHandlerMapping
s registered. They handle CORS preflight requests and intercept CORS simple and actual requests thanks to a CorsProcessor implementation (DefaultCorsProcessor by default) in order to add the relevant CORS response headers (likeAccess-Control-Allow-Origin
).
这篇文章中提到了CORS预检,接下来看看
DefaultCorsProcessor
是如何做的:因为不能把所有代码都贴出来,如果有看不懂的,可以自己去源码那看,这里附上路径:
org/springframework/web/cors/DefaultCorsProcessor.java
这个文件顶部有一句说明:
The default implementation of CorsProcessor, as defined by the CORS W3C recommendation .
表示这个实现是根据W3C推荐来定义的。
结尾
这篇博客是我截至目前写的最长的博客了。 同时也暴露了很多问题,他其实更适合于我自己阅读,记忆,而对其他读者并不是很友好。既然意识到的,之后就要针对注意一下了,比如我接下来就能改进的是,先写大纲,把段落分好再填充,本文其实并没有这样实践,导致后续光调整段落顺序就花费了大量时间。