对跨域有个全面的认知
🔀对跨域有个全面的认知
2021-10-10
| 2023-7-10
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password

前言

本文记录了我探索跨域问题的过程,在将其写成博文的过程中亦遇到新的问题,便停下笔去解决,所以整篇文章有明显的思考路径在。但是这样的写作方式也会导致绝大部分人无法跟着思考,因为大家都是有问题再来查找答案的,有明确的目的,而不会有很多耐心这样一步一步跟下去。所以,我也要探索更好的写作方式。
本文以跨域请求服务器端是如何处理的,是否会执行业务代码,浏览器又是如何处理的这个疑问为出发点,以实验的形式探索了跨域在B/S端的处理方式,实验首先以自己实现CORS基本规范来得出结果,随后又结合标准探索了Spring的@CrossOrigin注解的实现以及源码解读。在这个过程中发现了很多支线问题,并一一解决并记录。\
 

正文

怎么算跨域

URL中的协议、域名、端口一致则称为同源,不一致则不同源,也就是跨域。

浏览器对跨域的默认策略

浏览器会限制:
  1. Cookie、LocalStorage 和 IndexDB 无法读取
  1. DOM 和 JS 对象无法获得
  1. AJAX 请求的响应不能接收到
本文只讨论第三种情况。

浏览器具体是如何做到的

具体实现方式就是,XMLHttpRequestFetch这两个API都部署了同源限制。具体可以去看这两个API的实现。

跨域有哪些解决办法

  1. CORS 跨域资源共享 ——是一套机制/规范。 它由一系列 请求/响应 头(主要是响应头,也就是说,CORS主要是服务器方来实施的,也就是让服务器告知浏览器,这个域名是否被允许访问我的资源)中包含的特定HTTP头组成。浏览器通过读取这些头信息可以了解到这个跨源资源是不是应该被读取。
    1. 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源通信没有差别,代码完全一样。浏览器一旦发现请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
      CORS包含的HTTP头
2. 可以通过修改 document.domain,其允许将同源检测的目标提升到父域。
3. JSONP
 
4. WebSocket
 
5. 服务器代理 跨域限制是被浏览器限制的,因此如果用服务器作为代理来请求跨域资源,就不会有这个限制了。

[跨域基础知识]简单请求和复杂请求(是否触发预检)

日常开发中,可以认为满足以下三点的请求为简单请求。
  1. 请求方式为
    1. get
    2. post
    3. head
  1. 只有最常见的请求头
  1. 比较原始的 Content-Type(像 application/json 就不行)
其余的都是复杂请求。当然这并不全面,想要知道所有条件,请去 MDN 中查阅。
 
浏览器对于简单请求的处理过程:
 
  • 浏览器正常发送跨域请求,并在请求头中附加Origin字段标明请求来源。
  • 如果服务器不做出跨域的正确响应(添加Access-Control-Allow-Origin等字段),则浏览器会拦截返回的内容。
  • 服务器正确响应,则一切都照常。
对与复杂请求,上述基础依然适用于第一次发出的预检请求,而如果预检请求没有收到服务器的允许信号的话,则正式请求会浏览器在发送之前拦截
 

验证无效跨域访问得不到响应数据是被浏览器拦截,而非服务端未执行

 

实验环境搭建

实验后端接口如图,分为简单请求预检请求,各组 API分 为三个,分别为 无任何处理、只添加Access-Control-Allow-Origin 、都添加:
简单请求
notion image
预检请求
notion image

开始实验

简单请求
所有简单请求一次性发送:
 
notion image
 
结果分析:
通过控制台能发现,简单请求只需要携带至少一个Access-Control-Allow-Origin字段即可运作。而show接口在浏览器中发生CORS错误是可以预料到的。那么他在后台执行了吗?通过查看后端运行日志:
notion image
可以发现show也被执行了(对应第一条:’执行了‘)。回到前端,show的http头信息:
notion image
可以看到,状态码是200。但是通过响应一栏就能看出,结果被浏览器拦截了:
notion image
为了缩短篇幅,其他一些情况我就直接说实验结论,不贴过程了:
如果将接口改为这样,又会如何呢?
答案是,前端无法获得响应数据,但是接口会被执行。
实验结论:
综上可以得出:如果服务器没有配置CORS,则==简单==跨域请求可以成功执行,但是返回的内容会被浏览器拦截!
 
预检请求
notion image
 
http头信息
详情
不带CORS处理
notion image
 
notion image
只添加Access-Control-Allow-Origin
notion image
预览也无法显示,其在状态一栏显示错误提示:MethodDisallowedByPreflightResponse
都添加(预检)
notion image
符合预期
都添加(正式)
 
notion image
符合预期
 
结合后端运行日志:
notion image
结果分析:
三个预检请求对应前三条输出。但是这里其实有几个问题。
notion image
  1. 预检请求为什么会被分发到@PutMapping
    1. 预检请求是用的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默认是可以处理HEADOPTIONS方法的请求的。默认OPTIONS方法不会被DispatchServlet单独分发,不过也可以通过配置达到让RequestMapping可以声明method=options,详见:
  1. 预检请求会带参数吗
    1. 我修改了其中一条进行测试:
预检
notion image
正式
notion image
 
得出结论,预检请求不携带查询参数,正式请求会携带。
 
  1. 方法不被允许会怎么样
    1. 只添加Access-Control-Allow-Origin 这一组结果中,在状态一栏显示错误提示:MethodDisallowedByPreflightResponse。即表示方法不被允许,实际后端根本没有配置Access-Control-Allow-Methods。这个是带预检请求的情况,可以在回顾一下简单请求对于方法不支持的处理:
      notion image
      发现简单请求,即使响应方法不支持,依然还会执行,且能拿到返回值。 由此可以判断,简单请求的跨域只关心Access-Control-Allow-Origin 字段,并忽略Access-Control-Allow-Methods 字段。
      回到上面的实验结果分析,服务器日志的最后一条输出对应前端发出的那个正式的showWithPreflightAndAC请求。
      剩下的两条发生CORS错误的请求,后端并没有执行!
      实验结论:
      相比简单请求,预检请求是不带参数的,如果服务器并没有配置CORS,则OPTIONS请求方法默认也能被任意的URL匹配的RequestMapping捕获执行,且返回内容一样被浏览器拦截,之后并不会发送正式请求。

总结

综上,在一个简单的前后端请求响应模型中,对没有配置CORS的服务器发送 跨域简单请求 ,能够执行接口代码,但是返回数据会被浏览器拦截丢弃!
如果对正确配置了CORS的服务器发送了跨域简单请求的话,则一切正常!
而预检请求有细微不同,预检请求会被对应接口代码执行,仅当响应头中同时包含正确的Access-Control-Allow-OriginAccess-Control-Allow-Methods 时,正式请求才会决定发送,否则,就不是简单请求那样,接口照样调用,只是无法获取返回值这么便宜了,而是接口代码直接无法执行了(其实也会被预检请求执行一次)。 至于这个正式请求是被浏览器拦截的还是被服务器拦截的,根据网上的描述,应该是被浏览器。根据 CORS 通信 - JavaScript 教程 - 网道 (wangdoc.com) 中的描述:
当预检响应后,如果不符合跨域条件,XMLHttpRequest就会触发一个onerror事件。而为什么浏览器网络中会显示那条错误的CORS通信呢?回到那个图:
notion image
查看一下关于临时标头的解释:
notion image
目前的情况应该是匹配第二条,也就是说,这个网络资源被浏览器判定为无效。结合上面wangdoc文章的描述能更加印证这一点。所以是浏览器选择了不发送正式请求。

(额外)Fetch请求

上文都是对XHR请求的实验,而本篇博客诞生的原因其实是因为Fetch。 博主在第一次接触跨域的时候,用fetch接口成功在服务端无配置的情况下,通过浏览器控制台观察到了响应结果:
notion image
notion image
notion image
notion image
但是虽然能看到,但是代码却无法取到数据!由此可见fetch在跨域方面是具有一定的特殊性的。所以下面也做一个简单的分析。
notion image

思考

到这里就完整的验证了实验。但是是不是有些不符合预期?因为像现在这样肯定是不行的,如果接口触发了业务逻辑,那么预检请求就已经触发了,如果服务端的意思本来是不允许跨域请求,那依然执行了完整代码,不仅有业务风险,更是浪费算力,浪费带宽。所以后端针对预检请求应该有个专门的地方,只设置响应头信息,不做其他处理。
关于 CORS 的一个问题,大家怎么看 - V2EX —— 我一开始以为跨域就是这么配置的,所以上面的问题困扰了我很久,直到该贴18楼的解释将我带出了胡同
一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
对应到我们的demo中来,如果响应的设置被放到了一个单独的位置,@PutMapping中仅有逻辑代码,那服务器如果不做更多处理,会自动添加Access-Control-Allow-Origin字段么?
所以正确的跨域后端应该如何编写呢?这个问题的答案等下随我们探究Spring提供的跨域方案揭开。

@CrossOrigin注解实现CORS

 
以上都是我们自己实现CORS,来看看Spring是如何处理的:
Spring提供了@CrossOrigin注解来实现跨域访问,注解可以加载类或方法上,且支持丰富的跨域选项配置。
 
@CrossOrigin实现效果
自己简陋实现的效果
预检
notion image
notion image
正式
notion image
notion image
通过对比可以看出,@CrossOrigin的做法更符合网上说明的标准:
  • 预检中,针对性地在响应头中设置Access-Control-Allow-Methods 并为了减少预检次数,默认添加了30分钟的Access-Control-Max-Age
  • 正式响应中,只添加Access-Control-Allow-Origin 字段,以及Vary字段。
还需要注意的是,我们看看后端日志信息:
@CrossOrigin实现效果
notion image
自己简陋实现的效果
notion image
 
自己实现的由于比较粗糙,并没有做预检请求区分,所以自然而然调用了业务逻辑。而注解则避免了请求接口逻辑,这点就很符合预期了,然后自然而然地,就很想知道这是如何实现的。别急,先多了解一点@CrossOrigin,等下去探究源码。
如果注解是加在简单请求中的话,那响应头的信息又是如何呢?
@CrossOrigin实现效果
notion image
自己简陋实现的效果
notion image
发现只是多了三个Vary字段。
对于这几个不熟悉的字段,去了解一下:
  • Content-Length
    • 消息长度,请求头和响应头中都有可能出现。 最简单的例子,比如一个ajax请求,发送了一个携带 name=aaa 的POST请求,则请求头的Content-Length为8,也就是那个字符串的长度, 如果有用压缩的话,则是压缩后的长度。响应也是如此。 那些请求头、响应头数据不计算在其中。结合前面的例子,其实可以发现。 那些被浏览器拦截掉了的CORS请求,其实响应头中的Content-Length和服务器设置的返回内容长度是一致的。
  • Allow
    • 服务器允许的该资源的所有请求方式
接下来来看看源码,此时的我有个疑问,是Spring单方面遵从标准进行的部署还是利用接口调用了某个更底层特性实现? 看完源码将一切开朗。

(源码解读)Spring 是如何实现跨域的配置的

How does it work?

CORS requests (including preflight ones with an OPTIONS method) are automatically dispatched to the various HandlerMappings 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 (like Access-Control-Allow-Origin).
这篇文章中提到了CORS预检,接下来看看DefaultCorsProcessor是如何做的:
因为不能把所有代码都贴出来,如果有看不懂的,可以自己去源码那看,这里附上路径:org/springframework/web/cors/DefaultCorsProcessor.java
这个文件顶部有一句说明:
The default implementation of CorsProcessor, as defined by the CORS W3C recommendation .
表示这个实现是根据W3C推荐来定义的。

结尾

这篇博客是我截至目前写的最长的博客了。 同时也暴露了很多问题,他其实更适合于我自己阅读,记忆,而对其他读者并不是很友好。既然意识到的,之后就要针对注意一下了,比如我接下来就能改进的是,先写大纲,把段落分好再填充,本文其实并没有这样实践,导致后续光调整段落顺序就花费了大量时间。
  • 跨域
  • 前端
  • 后端
  • spring
  • https
  • ajax
  • js
  • 浏览器
  • CORS
  • 源码
  • div之间横竖方向的5px间距搜不到iPhone开的热点的解决办法
    Loading...
    目录