当前位置: 首页 > 科技观察

想起一个跨域配置引发的思考

时间:2023-03-19 11:50:57 科技观察

作者简介Flora,携程资深研发经理,专注于Node.js相关领域。如果您对跨域不熟悉,可以阅读MDNHTTP访问控制(CORS)一文。相关概念本文不再赘述。1.背景回顾一个星期五的下午,我们收到了一个调整响应头中Access-Control-Allow-Origin字段的请求。这种需求的原因是什么?我们先来看看目前的情况。对于webresource站点(这个站点以后会作为资源站点的代号),不管是不是跨域请求,都会返回这样的header。见图1。图1请求一个webresource站点的响应头截图Fig.1请求一个webresource站点的响应头截图这个响应好像没有问题。但是考虑这样一个场景:如果用户需要发送基于HTTPcookies和HTTP认证信息的身份凭证,那么需要在客户端设置一个特殊的凭证标志。例如使用fetch,需要添加fetch的配置,如图2所示:图2为fetch方法添加credentials配置图2fetch方法添加credentials配置客户端调整为以上后配置好后,再次运行会报如下错误,见图3。“从来源‘https://www.ctrip.com’获取‘https://webresource.c-ctrip.com/ResUnionOnline/R1/common/marinRedirect.js?v=20220903’的权限已被CORS阻止策略:当请求的凭据模式为“include”时,响应中“Access-Control-Allow-Origin”标头的值不得为通配符“*”。图3请求错误截图图3请求错误截图通过阅读这篇文章(原因:CredentialisnotsupportediftheCORSheader'Access-Control-Allow-Origin'is'*'),我们可以得到答案:“发出CORS请求时,已经设置了Credentials,但是服务器已经为HTTP响应头Access-Control-Allow-Origin配置了一个通配符(“*”)值,这与使用credentials是相反的。“所以,这又回到了最初,我们需要在本节开头做的一个调整是将原来的Access-Control-Allow-Origin设置为特定的原始值,而不是*星号。再次调整后,服务响应头更新如图4所示:图4请求web资源站点响应头截图单元测试跑通,发布给金丝雀测试。用户也反映没有报错,改版正式发布。监控板上一切正常,就高高兴兴地回家过周末了。周六早上,一个开发同学突然拍了一张照片发给我,说他们的应用报错:图5在线故障截图图5在线故障截图用户访问https://ebooking.ctrip.com一个资源,但是这个资源响应的Access-Control-Allow-Origin头是https://flights.ctrip.com。我访问了这个页面,没有发现这样的错误。回访了一些用户,让同事一起尝试访问。我得到的反馈是有的客户端报错,有的客户端正常。3、原因分析当时我们的第一反应是再次查看源站的逻辑变化,发现源站的Access-Control-Allow-Origin的配置代码是正常的,Access-的值为Control-Allow-Originwouldneverbechangedtoorigin错误的设置。再次结合反馈,部分用户会报错,开始转向CDN(ContentDeliveryNetwork)排查问题。如果您不熟悉CDN,可以阅读维基百科CDN或什么是CDN(内容分发网络)?先看这张简化的CDN结构图(图6)。目前网络资源站点的CDN提供者有3种,我们称之为:B提供者、W提供者和A提供者。其中供应商B和W为国内用户提供服务,其流量占比分别为50%和50%;供应商A为海外用户提供服务,流量占比100%。国内用户请求某个web资源站点的资源时,他可能会被分配到B,也可能会被分配到W。B或W会有一个概率(如果CDN节点命中失败),会请求该站点的服务资源的源站点。Figure6SimplifiedCDNStructureDiagramFig.6SimplifiedCDNStructureDiagram由于客户端反馈部分正确,部分异常,推测可能是某个CDN提供商出现异常或者某个节点出现异常。然后再次绑定SupplierB和SupplierW的服务器节点进行测试,将请求头中的Origin设置为https://ebooking.ctrip.com。我们得到如下结果:1)供应商B的回复内容与源站一致,如图7所示。图7B供应商响应体截图图7B供应商响应体截图2)供应商W的响应内容与源站有两个响应头不一致,如图8所示。图8截图W供应商的响应主体图8W供应商的响应主体截图第一个不一致是Access-Control-Allow-Origin不是源站点,第二个不一致是缺少Varyheader。细心的同学可以通过“图4ResponseHeaderRequestingWebresourceSite的截图”看到,源站已经将Varyheader设置为“Origin,Accept-Encoding”,如图9所示。要知道一旦这个header丢失,将无法识别基于Origin的协商缓存。对Vary不熟悉的同学可以参考HTTPVary。"Vary是一个HTTP响应头信息,它决定了是使用缓存的响应(response)还是应该向源服务器请求一个新的响应作为以后的请求头。它被服务器用来表示在内容协商中在算法(内容协商算法)中选择资源代表时应该使用什么头信息(headers)。如果Vary字段中有Origin,那么简单理解可以根据Origin+URL进行缓存。当Origin不同时,需要更新header信息。同样的,比如一些特殊的文件polyfill需要基于浏览器进行处理,那么可以将User-Agent设置为Vary,这样同一个文件就会基于User-Agent进行缓存。图9请求web资源站点的响应头截图CDN缓存头没有根据资源服务返回的响应正确配置,会导致返回的Access-Control-Allow-Origin值混淆。4.故障处理解决在线故障的第一要素是快速响应。因此,我们将国内CDN的比例从原来的50%改为100%,以保证客户端的正常响应。然后联系W供应商,当我们认为是供应商的严重bug时,供应商的回复是:①请求Origin:http://ebooking.ctrip.comcached后(对应的Etag为W/"D96CF9DBB3B578CC1721941E799BE22D"),因为源站响应了Vary:origin,accept-encoding,所以进入了Vary缓存的逻辑;②RequestOrigin:http://a.ctrip.com,因为进入了Vary缓存的逻辑,并且VaryData不匹配http://a.ctrip.com,所以进入了Varymiss的逻辑。当miss返回上层时,它带来If-None-Match:W/"D96CF9DBB3B578CC1721941E799BE22D"。这次返回上层的Origin是http://a.ctrip.com,但是因为If-None-Match,不同的源站,Etag值是一样的。所以响应304,此时会直接复用Origin:http://ebooking.ctrip.com的响应,Origin:http://ebooking.ctrip.com的响应的Access-Control-Allow还会用到-Originheadisgone”这里的Wsupplier有一个致命的逻辑错误:使用If-None-Match请求源站时,源站返回了304,意思是body没有变化,但是在同时源站返回正确的Access-Control-Allow-Originheader给CDN,但是CDN并没有替换源站给的header,而是直接在缓存中读取了一个错误的header,虽然我们的源站遵循HTTP标准,CDN没有遵循,导致返回给用户的响应头错误。图10304请求也需要响应Vary头,避免出错的问题。但是它需要根据资源域名一一配置,也就是config的方式uring白名单。所以最终的解决方案是给W供应商一批资源域名列表,让供应商手动配置。而且你需要记住,每次添加资源域名,都要同步到W提供者。5、经验总结经过这次失败,我们有以下结论:1)测试完整性:每个CDN提供商除了要验证自己应用的正确性之外,还需要对源站的每次更新进行测试。集成测试。因为不知道哪个环节、哪个配置可能会踩坑。2)开发标准化:不管我们上游怎么处理,资源源站服务的开发都必须遵循HTTP标准。只有参照标准,才能有序治理。HTTP是一个需要经常阅读的文档。3)资源的唯一性:在引用静态资源时,尽量保证资源URL的唯一性,例如可以使用md5来识别文件。这样做的好处是当这个资源出现一些意外故障时,可以及时更新文件,达到快速刷新客户端请求内容的效果,而不是依赖缓存清理工具。一方面是因为每个CDN提供者都有不同的purge(缓存清理)机制,没有治理工具可以知道每个CDN节点的缓存是否被正确清除。我想可能CDN提供商的交付指标中没有写入“缓存清理成功率”的指标。还有一个方面就是还存在一些未知的缓存节点,比如客户端的缓存,或者某酒店使用的系统。酒店内网可能有缓存。碰巧无论我们在CDN端如何执行缓存清理脚本,客户端都无法获取到新的资源。找CDN提供商找了半天,只好修改引用的URL地址(比如增加查询字段,虽然不优雅,但至少可以暂时解决问题)。因此,需要保证资源的唯一性。最后,我想说,如果所有CDN提供商的标准能够统一,那将是一件很棒的事情。经过一番深入的了解,了解到一些CDN提供商的设计初衷是直连存储,而不是一个静态的源站服务。一些header的配置直接放在CDN提供商的控制面板中进行配置。例如,默认不启用Vary等配置以提高缓存效率。