跨域实战解析

目录

前言

一、什么是跨域

二、跨域请求流程

简单请求

非简单请求

三、为什么需要同源策略?

四、遇见跨域

五、解决跨域

解决跨域之代理服务器(前端解决)

解决跨域之CORS头部设置(后端解决)

前端后端谁来解决跨域?

前端解决分析

后端解决分析

前端Vs后端

六、总结

关于预检请求

参考文档


前言

对于以前的我来说,跨域问题既熟悉又陌生。当有前端同事找到我,抱怨接口因为跨域访问不通时,我通常会立即使用Postman进行演示,展示我的接口是正常可用的,并且往往认为是前端调用的问题,让他自己去解决。然而,最近一次的跨域经历让我对跨域问题有了新的认识。本次分享的灵感来源于我们组的分享主题是Tcp/Ip协议相关的网络知识,同时也碰到了报价检查工具前后端分离交互时的跨域问题,最后阅读了亚哥推荐的《Wireshark网络分析的艺术》,并尝试使用Wireshark进行抓包分析的过程。

此外,在网络资源中关于什么是跨域、跨域问题怎么解决等文章层出不穷,涵盖了各种详尽的解释和解决方案,已经是一个“老掉牙”的话题。但是仔细对比阅读也可以发现网上的大多数文章之间也是互相借鉴,最大的特点就是直接了当的告诉你怎么去解决跨域,前端解决也好,后端解决也罢,每次看完资料后,总是感觉很少有文档能帮助我直观的去理解不同的解决方式之间到底有什么区别,因此决定借这个机会深入学习一下。

一、什么是跨域

通常,我们提到的跨域指:CORSCORS 是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing), 是一种基于HTTP头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。

同源是指:协议、域名、端口号完全相同。当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

情景同源情况是否跨域
同一域名下

http://grocery.corp.qunar.com/lab/a.js

 http://grocery.corp.qunar.com/script/b.js

同源
同一域名下不同文件夹

http://grocery.corp.qunar.com/a.js

 http://grocery.corp.qunar.com/b.js

同源
同一域名,不同端口

http://grocery.corp.qunar.com:8000/a.js

 http://grocery.corp.qunar.com/b.js

不同端口,跨域
同一域名,不同协议

http://grocery.corp.qunar.com/a.js

https://grocery.corp.qunar.com/b.js

不同协议,跨域
域名和域名对应IP

http://grocery.corp.qunar.com/a.js

 http://70.32.92.74/b.js

不同域,跨域
主域相同,子域不同

http://grocery.corp.qunar.com/a.js

 http://script.qunar.com/b.js

不同子域(二级域名),跨域
同一域名,不同二级域名

http://grocery.corp.qunar.com/ajs

http://qunar.com/b.js

不同二级域名,跨域(同上)

跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头(这里的特性在第6节关于预检请求介绍)。

二、跨域请求流程

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

当请求同时满足如下条件时,CORS 验证机制会使用简单请求, 否则 CORS 验证机制会使用预检请求。

  • 请求方法是以下三种方法之一:HEAD,GET,POST

  • HTTP 的头信息不超出以下几种字段:

    • Accept

    • Accept-Language

    • Content-Language

    • Last-Event-ID

    • Content-Type(只限于三个值 application/x-www-form-urlencoded、 multipart/form-data、text/plain)

接下来简单介绍一下简单请求与非简单请求的区别:

简单请求

浏览器直接发送跨域请求,并在请求头中携带 Origin 的头,表明这是一个跨域的请求。 服务器端接到请求后,会根据自己的跨域规则,通过 Access-Control-Allow-Origin 和 Access-Control-Allow-Methods 响应头,来返回验证结果。以下是浏览器验证跨域请求的流程图:从上面的流程图中可以看出,应答中携带了跨域头 Access-Control-Allow-Origin字段的请求能被浏览器返回响应。所以使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://api.training.com/com 的访问,该首部字段的内容如下:Access-Control-Allow-Origin: http://api.training.com/com ,现在除了 http://api.training.com/com ,其它外域均不能访问该资源。

非简单请求

浏览器在发现页面发出的请求非简单请求时,并不会立即执行对应的请求代码,而是会触发预先请求模式。预先请求模式会先发送 preflight request(预先验证请求),preflight request 是一个 OPTION 请求,用于询问要被跨域访问的服务器,是否允许当前域名下的页面发送跨域的请求。在得到服务器的跨域授权后才能发送真正的 HTTP 请求。

OPTIONS 请求头部中会包含以下头部:

请求头说明
Origin表明预检请求或实际请求的源站 URI 不管是否跨域,ORIGIN 字段总是被发送
Access-Control-Request-Method将实际请求所使用的 HTTP 方法告诉服务器
Access-Control-Request-Headers将实际请求所携带的首部字段告诉服务器

服务器收到 OPTIONS 请求后,设置头部与浏览器沟通来判断是否允许这个请求。

响应头说明
Access-Control-Allow-Origin指定允许访问该资源的外域 URI,对于携带身份凭证的请求不可使用通配符*
Access-Control-Expose-Headers指定 XMLHttpRequest 的 getResponseHeader 可以访问的响应头
Access-Control-Max-Age指定 preflight 请求的结果能够被缓存多久
Access-Control-Allow-Credentials是否允许浏览器读取 response 的内容; 当用在 preflight 预检请求的响应中时,指定实际的请求是否可使用 credentials
Access-Control-Allow-Methods指明实际请求所允许使用的 HTTP 方法
Access-Control-Allow-Headers指明实际请求中允许携带的首部字段

如果 preflight request 验证通过,浏览器才会发送真正的跨域请求。以下是非简单请求(触发预检请求时)的流程图:

从上图可以看出非简单请求的流程里其实已经完整的包括了简单请求的流程,只是额外增加了一次预检请求的触发与判断。以上两张图

三、为什么需要同源策略?

诞生之初就是为了数据安全。这块内容先暂时不写,因为关于跨站攻击与抓取的等知识目前还没有自己的理解,网络上参考资源也很多。接下来主要介绍四、五节的内容。

四、遇见跨域

在重构报价检查工具时,我们的系统交互图与测试接口介绍如下:

  • grocery系统是报价组的P3级排查工具类系统,web模块是排查工具页面的前端模块,使用的语言是vue,provider模块后端逻辑模块,语言是java;Sirius是报价组P1级别的酒店报价计算系统,在这里主要是给排查工具sirius提供一个报价数据接口
  • 首先,用户通过访问http://localhost:8080/#/priceTools/checkPrice接口进入前端页面,如果是未登录状态下则会被强制重定向到登陆页面,调用后端接口http://localhost:8081/login进行登陆
  • 用户登录后调用/priceTools/checkPrice页面的报价搜索按钮后会调用http://localhost:8081/priceView/result接口,而http://localhost:8081/priceView/result接口会调用http://sirius-i.corp.qunar.com/galaxy/priceView/result获取报价数据

在测试开发阶段,我们的前端页面通过vite+node.js进行构建与部署,前端页面访问urI默认为: http://localhost:8080/,端口是使用的默认的8080端口;后端通过tomcat部署,访问url为:http://localhost:8081/priceView/result ,由于默认端口8080已经被前端使用,因此部署端口只能改为8081,前后端都部署完成后,前端通过axios发起请求后端接口,后端通过TttpClient请求sirius报价接口。但是在前端点击搜索后,页面报错:Error: Network Error,查看网络请求后发现是跨域问题:

同时控制台也给了明确的报错信息:

先解释一下这个错误:

  • 来源与目标不同: 请求是从http://localhost:8080发起的,但是目标地址是http://localhost:8081,它们的域名(或端口)不同。
  • CORS 机制: 浏览器实施了同源策略,被阻止了响应。
  • 缺少 'Access-Control-Allow-Origin' 头: 目标服务器http://localhost:8081没有在响应中包含Access-Control-Allow-Origin头,该头告诉浏览器哪些来源被允许访问资源。由于缺少这个头,浏览器拒绝了跨域请求。

再看一下详细的响应头信息,确实没有包含Access-Control-Allow-Origin头。所以接下来介绍下这个问题怎么解决以及不同的解决方式之间的异同。

五、解决跨域

解决跨域的方式有很多,问了一下chatGpt,得到如下答案:

问:解决跨域的方法有哪些?

答:解决跨域(Cross-Origin Resource Sharing,CORS)的问题有多种方法,以下是一些常见的方法:

  1. CORS 头部设置:

    • 服务器端可以通过设置适当的 CORS 头部来允许或拒绝跨域请求。其中Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 包括等。
    • 例如,在 Express 框架中,可以使用中间件cors 来简化 CORS 头部的设置。
  2. JSONP(JSON with Padding):

    • JSONP 是一种绕过浏览器同源策略的方法,通过在页面中动态创建标签<script> 来实现跨域请求。
    • JSONP 仅支持 GET 请求,并要求服务端返回一个包裹在回调函数中的 JSON 数据。
  3. 代理服务器:

    • 前端可以通过在同源域名下搭建一个代理服务器,将请求发送到该代理服务器,然后由代理服务器转发请求到目标服务器。这样前端和后端的通信就在同源内了。
  4. CORS 中间件:

    • 在一些服务器端框架中,可以使用专门的 CORS 中间件来简化 CORS 头部的设置,例如 Express 中的 cors 中间件。
  5. 使用 WebSocket:

    • WebSocket 不受同源策略的限制,可以在不同域名之间进行双向通信。可以考虑使用 WebSocket 替代传统的 HTTP 请求。
  6. 反向代理:

    • 在生产环境中,可以通过配置反向代理来将前端和后端服务部署在同一域名下,避免跨域问题。

看得出来方法有很多,这里就只介绍我真正使用过的两种方式:代理服务器和CORS头部设置:

解决跨域之代理服务器(前端解决)

前端在同源域名下搭建一个代理服务器,将请求发送到该代理服务器,然后由代理服务器转发请求到目标服务器。请求被代理到目标服务器,目标服务器响应请求并将数据返回给开发服务器。开发服务器将目标服务器的响应返回给前端应用,前端应用看起来好像直接从同一域发起请求,避免了跨域问题。

 server: {
      port: 8080,
      host: '0.0.0.0',
      open: true,
      proxy: { 
        '/dev': {
          target: 'http://localhost:8081',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/dev/, '')
        }
      },
    }

这种方式的原理比较简单:

  • 中间服务和前端服务之间由于协议域名端口三者统一不存在跨域问题,可以直接发送请求 
  • 中间服务和后端服务之间由于并不经过浏览器没有同源策略的限制,可以直接发送请求
     

解决跨域之CORS头部设置(后端解决)

在Spring Boot 中配置 CORS(跨域资源共享)的类,配置允许跨域请求的相关规则(由于是后台工具类,以及测试阶段,所以跨域配置的粒度较大,请忽略)

/**
 * @author hubo.mao created on 2023-02-05 23:11
 */
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CrosConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")          // 允许所有路径都可跨域
            .allowedOriginPatterns("*")      // 允许所有来源(域)发起跨域请求。这里使用 * 表示任意来源
            .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")  //  允许的请求方法,包括 GET、HEAD、POST、PUT、DELETE、OPTIONS
            .allowCredentials(true)  // 允许携带认证信息,例如在跨域请求中使用 Cookies
            .maxAge(3600)  // 预检请求的有效期,以秒为单位。在有效期内,浏览器无需再次发起预检请求
            .allowedHeaders("*");  // 允许的请求头,这里使用 * 表示允许任意请求头
    }
}

前端后端谁来解决跨域?

这个问题前端工程师认为后端解决,后端工程师认为前端解决。

在自测阶段,我发现我通过以上两种方式都能顺利解决跨域问题,那两种方式具体对前后端的区别是什么呢?

前端解决分析

先来看一下通过代理服务器方式解决时,浏览器展示的请求头和响应头等信息:

控制台日志:

浏览器的network:

解决跨域后的浏览器network信息:

可以看到通过代理解决跨域问题,请求头中没有origin字段,原因在于当使用代理解决跨域问题时,代理服务器在客户端和目标服务器之间起到中间人的作用,将请求从客户端转发到目标服务器,并将响应从目标服务器返回给客户端。这种情况下,客户端发送的请求实际上是发往代理服务器的,而不是直接发送到目标服务器。猜测代理服务器可以根据自身的配置,通过其他方式传递跨域请求的信息给目标服务器,而不依赖于浏览器自动添加的Origin字段。

注:

  • Sec-Fetch-Site: same-site 表示同站 Sec-Fetch-Site: same-origin表示同源
    • 源:源(origin)= 协议(scheme)+ 主机名(hostname)+ 端口号(port)
    • 站:站(site)= eTLD+1 (TLD 表示顶级域名,例如 .com、.org、.cn ,TLD+1 表示顶级域名和它前面二级域名的组合,例如公司很多域名都是qunar.com结尾,所以看做是同站)
  • Access-Control-Allow-Origin: * 是 CORS(跨源资源共享)中的一个响应头部字段,它用于指示哪些源(域)被允许访问资源。在这里,* 表示允许任何源访问资源,即允许所有域进行跨域访问。
  • Host: localhost:8081:

    • Host 头部指定了请求的目标服务器的主机名和端口号。在这里,请求将被发送到 localhost 主机的 8081 端口。

  • Origin: http://localhost:8080

    • Origin 头部指示了请求的来源。它表示请求是从哪个源(协议、域名和端口)发起的。在这里,请求的来源是 http://localhost:8080

  • Referer: http://localhost:8080/:

    • Referer 头部包含了当前请求的来源页面的 URL。在这里,它指示请求是从 http://localhost:8080/ 页面发起的。

接下来用wrieshark抓包看一下整个代理过程,从前端发起请求到代理服务器然后到后端接口的整个过程

两个http请求分别向请求发起端口发送数据

各自发送完数据后的挥手过程

可以看到使用代理的情况下,总共有两个请求:

此外,还能在过滤出的抓包结果中看到三次握手与四次挥手的过程。

但是不明白的是:8181端口给8225端口发送数据,这是合理的,因为要返回给代理服务器数据,但是为什么8080端口会给8224端口返回数据?8080端口的数据从哪里来的?难道是8225这个代理中转端口发送给8080的?接下来应该怎么观测代理服务器端口8225到前端服务器8080的数据交接过程呢?

接下里看看不使用代理的跨域失败的情况:

从抓包结果来看确实只发起了一个请求,但是不清楚的是数据包发送完后,开发服务器会向后端接口发送RST标识(终止链接),难道是因为已经检测到是跨域请求并且非法的情况,所以提前主动强制终止链接?这一点没搞清楚。还有一点不清楚的是:为什么非法跨域请求时数据包只发了不到10个就提前结束(当然应该和RST有关),正常情况下的数据包大概有30多个。

后端解决分析

接下来,看看后端解决跨域问题时,在浏览器端与抓包时的情况:

首先是浏览器network选项卡:

与前端代理不同的是:

接下来看一下再wireshark中抓包的情况:

可以看到与前端代理不同的是,这里只有一个http请求,直接从前端服务器8080发送到了目标端口8081,相较于前面的两次数据传输与两次请求,整个过程更加简洁清晰与轻量。

前端Vs后端

至此,通过浏览器开发者模式,以及使用wireshark抓包观察对比了一种前端代理与一种后端增加cors头部信息的方式解决跨域问题的异同,那么跨域应该前端解决还是后端解决?

首先,还是先问一下chatGPT这个问题:跨域应该前端解决还是后端解决?

答:跨域问题通常需要在后端进行解决。跨域是由浏览器的同源策略(Same-Origin Policy)引起的,该策略是为了保护用户隐私和安全而设计的。因此,浏览器会限制在不同域名之间的页面间的交互。前端可以采取一些方法(如JSONP、代理服务器等)来缓解跨域问题,但这些方法并不能完全解决跨域的根本问题。真正的解决方案通常需要后端的协助,因为它涉及到设置响应头以允许特定域的访问。

通过前面对前端与后端分别解决跨域问题在开发者模式中的表现与抓包详情的解读,个人现在认为chatgpt说得还是很中肯的:跨域问题最优的解决方式还是需要后端来解决,原因如下(网络资源众说纷纭,虽然结论很对,但是很多结论并没有解释原因,大部分人应该也是人云亦云,所以本人一直以来也没有明白到底为什么。本次自己详细对比后,就可以用wiki中涉及的内容自我推导与解释):

  • 后端配置跨域粒度更细,从上面的可以看到从前端代理解决,响应头为Access-Control-Allow-Origin:*,允许任意域的页面访问该资源,如果在后端解决可以通过在响应中设置适当的 CORS 头部,如 Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers 等来允许特定域的请求。
  • 后端维护更方便:后端解决方案对于所有前端应用都适用,不受前端技术栈的限制。
  • 前端代理消耗更大:前端代理通常需要向后端代理服务器发送额外的请求,这可能导致更多的网络传输开销,虽然开发者模式开发者只感受到了一个http请求,但是从wireshark的抓包结果来看,确实会产生两个http请求。特别是在高延迟的网络环境下,这可能会影响页面加载速度和性能。
  • 前端代理需要额外维护代理服务器:虽然本文的前端代理只是增加了简单的代理配置,如果是线上环境,应该会比较复杂,但是没有接触过。
  • 后端控制跨域规则更安全:反证法---如果跨域交给前端解决,是不是意味着攻击者可以决定跨域规则?即攻击者可能通过操纵前端代码或其他手段来修改跨域规则,使得浏览器可以向攻击者指定的域名发送请求。这可能导致跨站请求伪造(CSRF)等安全问题,使得攻击者能够发起对用户账户等敏感操作的请求,解决跨域应该是后端定义哪些域可以访问我的接口。

在实际应用中,选择前端解决还是后端解决跨域问题取决于你的应用程序架构和安全需求。如果你的应用程序只是简单地需要与少数几个API进行跨域通信,那么前端解决可能更为简单和直接。但如果你的应用程序需要与多个不同域名的服务进行跨域通信,或者需要更复杂的安全控制,那么后端解决可能更适合。

六、总结

此处暂且用一张比较简单的图来展示跨域的整个流程:

跨域这个话题已经存在很多年了,当初作为一个计算机网络初学者时更注重的跨域的概念以及跨域问题的解决方式,但是问题解决后复盘深度不够,很少从更底层的网络传输层面去对比过几种解决方式之间的异同,所以对于跨域的理解一直停留在比较浅显、刚好够用的阶段。本次通过深入了解网络协议和Wireshark的使用,我逐渐领悟到跨域问题的复杂性。这次分享不仅让我更好地理解了跨域的根本原因,也为解决类似问题提供了更全面的视角。同时,我开始意识到前端与后端之间的通信涉及到更多层面的考虑,而不仅仅是接口是否正常可调用。但是目前对于跨域依然有一些不清楚的地方,都在文中用红字标出,时间和能力有限暂时没有得出自己的理解,也许解答这些疑问需要更进一步研究。同时文中大部分内容来自于自己的理解,理解有误的地方可以一起探讨下。

前端是用户与应用程序交互的窗口,而后端则是处理数据和逻辑的引擎。这两者之间的协同工作是实现一个功能完整、高效的应用的关键。前端负责用户界面的设计和交互,后端则处理数据存储、逻辑运算和服务器端的任务。本来是一个后端选手,最近学习了一些前端的知识,从整体上更感受到前后端之间的关联性和整体的开发流程。为了实现更高效的开发,把前后端结合到一起思考可以更快速、更全面的理解应用程序的整体架构。单单是一个跨域问题,都可以在前后端分别找到好几种不同的解决方案,并且各有优劣,如果这个时候有一些前后端交融的思维方式,可以为解决方案的选择提供更综合、有效的视角。通过深度了解前后端的特点和相互之间的协作,能够更灵活地选择最适合应用场景的跨域解决方案。

总的来说,这次经历让我在跨域问题上取得了新的认识,并为未来的协作与解决类似问题提供了更多的工具和思考角度。

关于预检请求

最后额外提一下预检请求。前面提到了预检请求概念和原理,接下来看一下看一个在浏览器中真实的预检请求(由于出发预检请求需要复杂请求,因此我将请求方式由get更换为了put后触发了预检请求):

接下来看一下跨域失败时的情况:

和前面跨域失败时的结果对比可以发现,触发预检请求时还能帮助跨域请求快速失败,在极短的时间内就能停止请求,反观没有触发预检时响应时间要长得多,同时,通过wireshark抓包发现,浏览器实际只发起了一个option请求,并没有真正的去发起put请求,这也与前面介绍预检请求的概念时提到的结论是相符的。

参考文档

【网络安全】浅析跨域原理及如何实现跨域_网络安全_H_InfoQ写作社区

跨源资源共享(CORS) - HTTP | MDN

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
要解决FLV.js解析FLV视频报跨域问题,可以采取以下几种常见的方法。 首先,可以在服务器端进行跨域解决,通过设置合适的响应头信息来允许跨域访问。例如,可以在服务器端设置Access-Control-Allow-Origin头,允许特定域名或所有域名进行跨域访问。这样,前端页面在请求FLV视频资源时,服务器会返回合适的响应头信息,从而解决跨域问题。 其次,可以利用代理服务器进行跨域请求。前端页面将FLV视频请求发送给代理服务器,代理服务器再转发请求给实际的视频资源服务器,然后将响应返回给前端页面。由于代理服务器和视频资源服务器在同一域,因此不存在跨域问题。 另外,还可以通过JSONP进行跨域请求。JSONP利用<script>标签的跨域能力,可以跨域加载远程资源。具体操作是,在前端页面动态创建一个<script>标签,src属性指向FLV视频资源的URL,然后定义一个全局函数来处理响应数据。服务器返回的响应数据需要包裹在该函数中作为参数进行返回。这样,前端页面就可以通过JSONP进行跨域请求并解析FLV视频。 还可以使用Nginx等反向代理服务器进行跨域解决。在Nginx配置文件中添加相关配置,设置合适的跨域规则。具体可参考Nginx的相关文档和配置示例。 综上所述,解决FLV.js解析FLV视频跨域问题的方法包括设置合适的响应头信息、利用代理服务器、使用JSONP和使用反向代理服务器等。根据具体情况选择合适的方法来解决跨域问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值