草庐IT

跨域:后端工程师最熟悉的陌生“人”

华为云开发者社区 2023-03-28 原文
摘要:跨域,对后端工程师来说,可谓既熟悉又陌生。

本文分享自华为云社区《后端老司机的跨域之旅》,作者: 勇哥java实战分享。

跨域,对后端工程师来说,可谓既熟悉又陌生。

这两个月我以架构师的角色参与一款教育产品的孵化,有了一段难忘的跨域之旅。

写这篇文章,我想分享我在跨域这个知识点的经历和思考,希望对大家有所启发。

1 遇见跨域

产品有多端:机构端,局方端 ,家长端等 。每端都有独立的域名,有的是在PC上访问,有的是通过微信公众号来访问,有的是扫码后H5展现。

接入层调用的接口域名统一使用 api.training.com这个独立的域名,通过Nginx来配置请求转发。

通常,我们提到的跨域指:CORS。

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing), 它需要浏览器和服务器同时支持他,允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服 AJAX 只能同源使用的限制。

那么如何定义同源呢?我们先看下一个典型的网站的地址:

同源是指:协议、域名、端口号完全相同。

下表给出了与 URL http://www.training.com/dir/page.html 的源进行对比的示例:

当用户通过浏览器访问应用(http://admin.training.com)时,调用接口的域名非同源域名(http://api.training.com),这是显而易见的跨域场景。

2 CORS详解

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。

规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。

服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

2.1 简单请求

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

1.使用GET、POST、HEAD其中一种方法;

2.只使用了如下的安全首部字段,不得人为设置其他首部字段;

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type 仅限三种之一:text/plain,multipart/form-data,application/x-www-form-urlencoded:
    • HTML头部 header field字段:DPR、Download、Save-Data、Viewport-Width、WIdth

3.请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问;

4.请求中没有使用 ReadableStream 对象。

简单请求模式,浏览器直接发送跨域请求,并在请求头中携带Origin的头,表明这是一个跨域的请求。 服务器端接到请求后,会根据自己的跨域规则,通过Access-Control-Allow-Origin和Access-Control-Allow-Methods响应头,来返回验证结果。

应答中携带了跨域头 Access-Control-Allow-Origin。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 http://admin.training.com 的访问,该首部字段的内容如下:

Access-Control-Allow-Origin: http://admin.training.com

现在,除了 http://admin.training.com,其它外域均不能访问该资源。

2.2 预检请求

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

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

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

如果preflight request验证通过,浏览器才会发送真正的跨域请求。

3 后端配置

后端配置我尝试过两种方式,经过两个月的测试,都能非常稳定的运行。

  • MND推荐的Nginx配置;
  • SpringBoot自带CorsFilter配置。

▍MND推荐的Nginx配置

Nginx配置相当于在请求转发层配置。

location / {
 if ($request_method = 'OPTIONS') {
 add_header 'Access-Control-Allow-Origin' '*';
 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
 #
 # Custom headers and headers various browsers *should* be OK with but aren't
 #
 add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
 #
 # Tell client that this pre-flight info is valid for 20 days
 #
 add_header 'Access-Control-Max-Age' 1728000;
 add_header 'Content-Type' 'text/plain; charset=utf-8';
 add_header 'Content-Length' 0;
 return 204;
 }
 if ($request_method = 'POST') {
 add_header 'Access-Control-Allow-Origin' '*' always;
 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
 add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
 add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
 }
 if ($request_method = 'GET') {
 add_header 'Access-Control-Allow-Origin' '*' always;
 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
 add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
 add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
 }
}

在配置Access-Control-Allow-Headers属性的时候,因为自定义的header包含签名和token,数量较多。为了简洁方便,我把Access-Control-Allow-Headers配置成 * 。

在Chrome和firefox下没有任何异常,但在IE11下报了如下的错:

Access-Control-Allow-Headers 列表中不存在请求标头 content-type。

原来IE11要求预检请求返回的Access-Control-Allow-Headers的值必须以逗号分隔。

▍SpringBoot自带CorsFilter

首先基础框架里默认有如下跨域配置。

public void addCorsMappings(CorsRegistry registry) {
 registry.addMapping("/**")
 .allowedOrigins("*")
 .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
 .allowCredentials(true)
 .allowedHeaders("*")
 .maxAge(3600);
}

可是部署完成,进入还是报CORS异常:

从nginx和tomcat日志来看,仅仅收到一个OPTION请求,springboot应用里有一个拦截器ActionInterceptor,从header中获取token,调用用户服务查询用户信息,放入request中。当没有获取token数据时,会返回给前端JSON格式数据。

但从现象来看CorsMapping并没有生效。

为什么呢?实际上还是执行顺序的概念。下图展示了 过滤器,拦截器,控制器的执行顺序。

DispatchServlet.doDispatch()方法是SpringMVC的核心入口方法。

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
 return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

那么CorsMapping在哪里初始化的呢?经过调试,定位于AbstractHandlerMapping

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(new CorsInterceptor(config));
 }
return chain;
}

代码里有预检判断,通过PreFlightHandler.handleRequest()中处理,但是处于正常的业务拦截器之后。

最终选择CorsFilter 主要基于两点原因:

  • 过滤器的执行顺序优先级最高;
  • 通过调试CorsFilter的源码,发现源码有很多细节的处理。
private CorsConfiguration corsConfig() {
 CorsConfiguration corsConfiguration = new CorsConfiguration();
 corsConfiguration.addAllowedOrigin("*");
 corsConfiguration.addAllowedHeader("*");
 corsConfiguration.addAllowedMethod("*");
 corsConfiguration.setAllowCredentials(true);
 corsConfiguration.setMaxAge(3600L);
 return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
 source.registerCorsConfiguration("/**", corsConfig());
 return new CorsFilter(source);
}

下面的代码里,allowHeader是通配符 * 的时候,CorsFilter在设置 Access-Control-Allow-Headers 的时候,会将 Access-Control-Request-Headers 以逗号拼接起来,这样就可以避免IE11响应头的问题。

public List<String> checkHeaders(@Nullable List<String> requestHeaders) {
 if (requestHeaders == null) {
 return null;
 }
 if (requestHeaders.isEmpty()) {
 return Collections.emptyList();
 }
 if (ObjectUtils.isEmpty(this.allowedHeaders)) {
 return null;
 }
 boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
   List<String> result = new ArrayList<>(requestHeaders.size());
 for (String requestHeader : requestHeaders) {
 if (StringUtils.hasText(requestHeader)) {
 requestHeader = requestHeader.trim();
 if (allowAnyHeader) {
 result.add(requestHeader);
 }
 else {
 for (String allowedHeader : this.allowedHeaders) {
 if (requestHeader.equalsIgnoreCase(allowedHeader)) {
 result.add(requestHeader);
 break;
 }
 }
 }
 }
 }
 return (result.isEmpty() ? null : result);
}

浏览器的执行效果如下:

4 preflight响应码:200 vs 204

后端配置完成之后,团队里的小伙伴问我:“勇哥,那预检请求返回的响应码到底是200还是204呀?”。这个问题真把我给问住了。

我司的API网关的预检响应码是200,CorsFilter预检响应码也是200。

MDN给的示例预检响应码全部是204。

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

我只能采取Google大法,赫然发现大名鼎鼎的API网关Kong的开发者也针对这个问题有一番讨论。

  1. MDN曾经推荐的preflight响应码是200 ,所以Kong也和MDN同步成200;
    The page was updated since then. See its contents on Sept 30th, 2018:
    https://web.archive.org/web/20180930031917/https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
  2. 后来MDN将响应码修改204,于是Kong的开发者争论要不要和MDN保持同步。
    争论的核心点在于:有没有迫切的必要。200响应码运行得很好,似乎也将永远正常运行下去。而更换成204,不确定是否有隐藏问题。
  3. 说到底,框架开发者还是依赖于浏览器的底层实现。在这个问题上,没有足够权威的资料能够支撑框架开发者,而各个知识点都散落在网络的各个角落,充斥着不完整的细节和部分解决方案,这些都让框架开发者非常困惑。

最后,Kong的源码里预检响应码仍然是200,并没有和MDN保持同步。

我仔细查看了各大主流网站,95%预检响应码是200。而经过两个多月的测试,Nginx配置预检响应码204,在主流的浏览器Chrome , Firefox , IE11 也没有出现任何问题。

所以,200 works everywhere , 而204在当前主流的浏览器里也得到非常好的支持。

5 Chrome: 非安全私有网络

本以为跨域问题就这样解决了。没想到还是有一个小插曲。

产品总监需要给客户做演示,我负责搞定演示环境。申请域名,准备阿里云服务器,应用打包,部署,一切都很顺利。

可是在公司内网访问演示环境,有一个页面一直报CORS报错,报错内容类似下图:

跨域的错误类型是:InsecurePrivateNetwork。

这和原来遇到的跨域错误完全不一样,我心里一慌。马上Google , 原来这是chrome更新到94之后新的特性,可以手工关闭这个特性。

  1. 打开 tab 页面 chrome://flags/#block-insecure-private-network-requests
  2. 将其 Block insecure private network requests 设置为 Disabled, 然后重启就行了, 这样子就相当于把这个功能禁用掉。

但这样是治标不治本呀。有点诡异的是,当我们不在公司内网访问演示环境的时候,演示环境完全正常,出错的页面也能正常访问。

仔细看官方的文档,CORS-RFC1918 指出如下三种请求会受影响。

  • 公共网络访问私有网络;
  • 公共网络访问本地设备;
  • 私有网络访问本地设备。

这样,我把问题定位在这个出错的第三方接口地址上。公司很多产品都依赖这个接口服务。当在公司内网访问的时候,该域名映射地址类似:172.16.xx.xx。

而这个ip正好是rfc1918上规定的私有网络。

10.0.0.0     -  10.255.255.255 (10/8 prefix)
172.16.0.0   -  172.31.255.255 (172.16/12 prefix)
192.168.0.0  - 192.168.255.255 (192.168/16 prefix)

内网通过Chrome访问这个页面的时候,会触发非安全私有网络拦截。

如何解决呢?官方给出的方案分两步走:

  1. 私有网络只能通过Https来访问;
  2. 未来,添加特定的预检头,比如说:Access-Control-Request-Private-Network等。

当然还有一些临时方法:

  • 关闭Chrome该特性;
  • 换用其他浏览器比如Firefox;
  • 关闭网络内网开手机热点;
  • 修改本地host绑定外网ip。

基于官方的方案 ,生产环境完全使用Https,公司内网访问就没有出现这样的跨域问题了。

6 复盘

API网关非常适合当前产品的架构。架构设计之初,系统多端都会调用我司的API网关。API网关可以SAAS部署和私有化部署,有单独的域名,提供完善的签名算法。考虑到上线时间节点,团队成员对于API网关的熟悉程度以及多套环境部署投入时间成本,为了尽快交付,从架构层面,我做了一些平衡和妥协。

接入层调用的接口域名统一使用 api.training.com这个独立的域名,通过Nginx来配置请求转发。同时,我和前端Leader统一了前后端协议,保持和我司API网关一致,为后续切回API网关做前置准备。

API网关可以做鉴权,限流,灰度等,同时可以配置CORS。内部服务端不用特别关注跨域这个问题。

同时,在解决跨域的问题过程中,我的心态也发生了变化。从最初的轻视,到逐渐沉下心来,一步步理解CORS的原理,分清楚不同解决方案的优缺点,事情也就慢慢顺遂起来。 我也观察到:”有的项目组已经反馈过Chrome非安全私有网络问题,并给出了解决方案。对于技术管理者来讲,一定要重视项目中反馈的问题,做好梳理分析,整理预案。这样当同类问题出现时,也会条理有序“。

 

点击关注,第一时间了解华为云新鲜技术~

有关跨域:后端工程师最熟悉的陌生“人”的更多相关文章

  1. 华为认证的网络工程师证好考吗,含金量高吗 ? - 2

    华为认证分等级的,相当于初中高三个等级,当然高级是比较难考的,也是含金量最高的。我就慢慢给你介绍一下。1.了解华为认证华为认证网络工程师是由华为公司认证与采购部推出的独立认证体系,与之前的华为认证不同,简称HCIA。同时华为认证是华为技术有限公司凭借多年信息通信技术人才培养经验,以及对行业发展的理解,以层次化的职业技术认证为指引,推出的覆盖IP、IT、CT以及ICT融合技术领域的认证体系,是ICT全技术领域认证体系。​2.怎么考取华为认证网络工程师?要考取华为认证网络工程师必须选择最近的Prometric授权考试中心APTC报名并参加GB0-190的考试,考试通过后,以获得由华为统一签发的“华

  2. 打通源码,高效定位代码问题|云效工程师指北 - 2

    大家好,我叫胡飞虎,花名虎仔,目前负责云效旗下产品Codeup代码托管的设计与开发。代码作为企业最核心的数据资产,除了被构建、部署之外还有更大的价值。为了帮助企业和团队挖掘更多源代码价值以赋能日常代码研发、运维等工作,云效代码团队在大数据和智能化方向进行了一系列的探索和实践(例如代码搜索与推荐),本文主要介绍我们如何通过直接打通源代码来提高研发与运维效率。随着微服务架构的流行,一个业务流程需要多个微服务共同完成。一旦出现问题,运维人员在面对数量多、调用链路复杂的情况下,很难快速锁定导致问题发生的罪魁祸首:代码。为了提高排查效率,目前常见的解决方案是:链路跟踪+日志分析工具相结合。即通过链路跟踪

  3. ruby - 将 Ember.js 与简单的 Sinatra 后端集成 - 2

    有很多文档介绍如何构建和创建以Rails作为后端的Ember.js应用程序。流行的解决方案是使用gems作为ember-rails和ember-source或合二为一的ember-appkit-rails。但是我正在尝试创建一个简单的Sinatra应用程序,该应用程序以Ember.js作为前端来处理仅JSON后端。我发现的少数资源似乎有点过时,所以我正在寻找简单的方法来做到这一点。所以我的问题是:我如何将Ember.js与简单的Sinatra后端集成?如何执行此操作的示例将不胜感激。 最佳答案 有一个verysimplerepoon

  4. 软件工程毕业设计课题(81)微信小程序毕业设计PHP校园跑腿小程序系统设计与实现 - 2

        项目背景和意义 目的:本课题主要目标是设计并能够实现一个基于微信校园跑腿小程序系统,前台用户使用小程序发布跑腿任何和接跑腿任务,后台管理使用基于PHP+MySql的B/S架构;通过后台管理跑腿的用户、查看跑腿信息和对应订单。意义:手机网络时代,大学生通过手机网购日常用品、外卖外卖、代取快递等已不再是稀奇的事情。此外,不少高校还流行着校园有偿工作,校园跑腿就成了大学生创业服务项目。        因为你在校园里,所以不会有进入的限制。并不是所有的外卖平台都可以随意进入校园,比如小黄和小蓝的双打外卖平台。许多大学禁止送餐进入学校,更不用说送餐进入宿舍了。这一措施使得校园服务市场的竞争相对不

  5. ruby - 从我的胰岛素泵 Controller 逆向工程统计数据文件 - 2

    这可能是也可能不是灰色地带主题,尽管我的意图肯定不是,所以我的意图不是激起关于逆向工程主题的道德辩论。我是1型糖尿病患者,目前正在接受泵治疗。我是OmniPod用户,这是一个一次性胶囊,可以粘在我的身上并分配胰岛素3天。它由个人糖尿病管理器[PDM](见下文)控制,该管理器控制进餐期间分配的胰岛素量、血糖读数,并且包含一个用于计算碳水化合物的食物指数。(来源:myomnipod.com)新的PDM有一个用于下载数据的USB端口。该软件对Windows用户免费(名为CoPilot的软件包),但不支持Mac。将PDM插入我的Mac后,它像任何其他USB设备一样安装,并为我提供了一个可读卷,

  6. U3D游戏开发工程师正确入行姿势指南 - 2

    2021年,游戏圈上演了一场精彩绝伦的抢人大战。在上海游戏圈,年薪百万的人越来越多了。据多名HR估算,在上海,过去一年TA、引擎、美术等稀缺岗位拟的薪资涨幅大概在20%-30%左右。某位圈内知名资深游戏猎头对此发出感叹:“50K的数值策划、角色原画;70K的技术美术;80K的技术总监...他们的年薪总包都接近百万,就连应届生入行的薪资也水涨船高,这要是放在以往都是不敢想象的”。以往含年薪、期权等的年总包收入上百万元,起码得是总监级别。如今工作五六年的人从广深跳到上海游戏公司,年薪能从50-70万跃上100万元,拿百万年薪的游戏从业者越来越多了上海游戏圈近年发展迅速,既有颇具发展潜力的中生代F4

  7. QT 设计一个串口调试工具,用一个工程就能轻松解决,外加虚拟串口工具模拟调试,在日常工作中可类比模块间通信,非常详细建议收藏 - 2

    QT串口调试工具第一节虚拟串口工具安装第二节QT创建一个基于QWidget的项目第三节UI界面设计第三节项目头文件widget.h第四节项目实现文件widget.cpp第五节main函数第六节编译结果重点第七节使用QT打包程序,不安装QT的电脑可使用第一节虚拟串口工具安装-----------------------------------------下载所需工具---------------------------------------------------------------------链接:https://pan.baidu.com/s/1QkT36S4EnH2HEAhZ1TZ8

  8. 【uniapp】uni.request请求跨域问题解决方案 - 2

    例如,运行H5页面,请求一个地址资源,如果不是本站地址,浏览器就会报跨域错误,这样访问受限问题呈现例如,项目代码里是这样写的,运行H5测试uni.request({ url:'https://gitcode.net/zs1028/stat...ouces_2023/-/...', success(res){ console.log(res) }, fail(err){ console.error(err) }})因为https://gitcode.net不是本站地址,根据浏览器同源策略,是会报跨域错误,解决步骤打开项目的manifest.json文件,以源码视图查看,添加以下代码{ //.

  9. 7nm+跨域计算+极致性价比,这家芯片厂商助攻车企「降本增效」 - 2

    汽车芯片赛道的「卷」,或许超出了所有人的预期。对于单纯TOPS算力的比拼,已经翻篇,如何让车企有的用,用得上,还要用得好,已经是新风向。实际上,在汽车智能化刚刚开始的2018年,彼时类似斑马智行这样的车机系统仅仅是从软件层面改变传统座舱的人机交互体验(从功能机到智能机)。而类似Mobileye这样的ADAS视觉感知系统方案(EyeQ5之前),也仅仅是辅助驾驶的入门级。在高工智能汽车研究院看来,汽车芯片赛道经历了几个发展周期,1.0时代(以2020年上车的高通8155为代表),智能座舱进入硬件变革节点;2.0时代(以2021年上车的英伟达Orin为代表),智能驾驶进入硬件变革节点。而3.0时代,

  10. (一)专题介绍:移动端安卓手机改造成linux服务器&linux服务器中安装软件、部署前后端分离项目实战 - 2

    快捷目录前言一、涉及到的相关技术简介二、具体实现过程及踩坑杂谈1.安卓手机改造成linux系统实现方案2.改造后的手机Linux中软件的安装3.手机Linux中安装MySQL5.7踩坑实录4.手机Linux中安装软件的正确方法三、Linux服务器部署前后端分离项目流程1.前提准备(安装必要软件,搭建环境):2.前后端分离项目的详细部署过程:总结前言总体概述:本篇文章隶属于“手机改造服务器部署前后端分离项目”系列专栏,该专栏将分多个板块,每个板块独立成篇来详细记录:手机(安卓)改造成个人服务器(Linux)、Linux中安装软件、配置开发环境、部署JAVA+VUE+MySQL5.7前后端分离项目

随机推荐