贴吧无线 Web 的 HTTPS 改造实践
1. 背景
百度公司一直都面临着严峻的安全威胁和数据隐私风险,未加密的HTTP业务流量被第三方监听、追踪甚至劫持,进而导致用户的访问页面被篡改、passport认证信息被窃取、个人数据被泄漏、下载的安装包被替换等安全问题。这些攻击产生了巨大的利益,已经成为黑色或者灰色产业成规模地运营;这些攻击不但发生在诱饵WIFI热点上,也发生在某些骨干网上;这些攻击威胁着用户的安全和隐私,也给百度的声誉和利益带来巨大损失。
全站HTTPS化改造,可以有效的解决网络劫持、隐私泄漏等严重安全问题,这在百度连接人与服务,打造支付闭环的背景下,显得极其重要。在政策层面上,也对公司在安全和拥护隐私保护方面提出了更高的要求。另一方面,由于HTTP2.0在主流浏览器实现上强制要求HTTPS,这一技术变革也无法避免。
贴吧是一个具有十余年历史的老产品,拥有庞大的用户群体,每日PV数十亿量级。它暴露在HTTP下的不安全性将会比其它产品线对用户的影响更严重、范围更广。为了响应公司技术委员会和安全工作组的号召,贴吧在2016年Q2启动了改造HTTPS的改造工作。
2. 评估
2.1 范围
贴吧多年的业务运营,造成了前端代码十分繁杂和分散,从功能机时代的无线WAP页面,到现今的智能手机H5版本(以下简称“智能版”)、PAD版和PC版都有部署,时间跨度在5年以上,很多代码都已经找不到负责人,因此踩坑的几率极大。为了尽可能地降低对用户的影响,同时使得整个改造周期不要拉得太长,我们决定分端改造,先改造智能版。
2.2流程
由于智能版相对与贴吧PC版与移动客户端来说关注度不高,一开始我们采取的改造流程非常简单:
- 建设HTTPS开发环境
- 改造代码
- QA验证
- 小流量
上线的流程是有致命问题的,我们后面会提到。
2.3 细节
无效的HTTPS证书会导致现代浏览器主动阻塞请求,在PC上,可以通过手动放行来强行加载页面和资源,在移动端上,一些浏览器(如Safari)则不提供此功能,从而无法加载页面。如果手动安装信任证书,则过于繁琐,在测试过程中不具有实际可操作性。
真实的HTTPS证书只存在于公司的线上环境中,应用于_*.baidu.com_,因此,我们申请了一台线上机器,但不接入任何用户流量。在内网环境,需要绑hosts,就可以像正常一样去访问了。
3. 改造
迁移HTTPS在代码上需要改动的点主要包括:
- 域名代理;
- 资源路径;
- 跳转页面;
- 动态链接;
- CORS;
- 白名单
考虑到小流量模式下,HTTP与HTTPS是共存的,因此应以相对协议“**//**”引用所有的静态资源和iframe,用同一套代码支持两种协议。注意在PC的IE8浏览器上,使用相对协议引用CSS会造成两次下载行为。这个bug仅影响加载性能,但贴吧的IE8浏览器用户并不多,且本次改造仅涉及无线端,因此暂不予考虑。
3.1 域名代理
注意你可能无法将所有的资源都简单地替换为HTTPS或相对协议,因为有些域名,比如说引用的第三方资源,可能并不支持HTTPS。这种情况下,我们有两种方案:
- 如果是静态不变资源,将其下载,上传到我们的CDN服务器上,比如图片;
- 如果资源的内容可能会被改变,使用代理域名代理其地址
代理是改造HTTPS过程中所必不可少的,即使是百度自己的域名,也有非常多的并没有部署HTTPS证书,因此我们对于不支持的域名,直接使用了代理域名进行了替换,这是一个已经存在的公司内部代理服务。这种域名替换可以是硬性的,即无论当前页面是处于HTTP下还是处于HTTPS下,都走HTTPS的代理域名——HTTP页面下完全允许加载HTTPS的资源;也可以是软性的,仅在HTTPS下使用代理域名。
主要特别注意的是,代理可能需要透传HTTP协议的end-to-end头部以及所有search参数,当然也包括负载。如果有可能地话,GET、POST之外的其它method最好也能予支持。例如我们有一个地址:_http://www.test.com/a/b/c?d=1&e=2_,在替换为地理地址后则可能为 _https://www.proxy.com/a/b/c?d=1&e=2_,路径和参数是不变的。
3.2 资源路径
我们在这里把资源分为静态资源和框架资源(即iframe),仅感官差异,但并无实质区别。
HTTPS页面中加载的非HTTPS资源被称为Mixed Content。理论上,Mixed Content会对HTTPS的页面造成安全威胁。不同的浏览器对Mixed Content的处理方式不尽相同。如果你使用过IE6,就一定会被一个提示“是否只查看安全传送的网页内容?”信息的确认对话框搅扰过,如果你点击了“是”的话,那么所有Mixed Content都不会被加载,反之都会加载。后来的IE则默认允许加载Mixed Content图片,但script和css则仍需要用户确认。现代浏览器大多遵循了W3C的Mixed Content规范,将Mixed Content分为Optionally-blockable和Blockable两类。
前者包含危险系数较小,即使被篡改也无大碍的资源,比如SVG、图片、声音和视频。它们默认会被加载,但浏览器会在控制台打印警告信息。对于Chrome来讲,只要有一个Mixed Content资源,地址栏协议前面的绿色小锁就会变为灰色的感叹号
后者包含的资源被篡改则更容易引起严重的后果,比如css、script、字体以及iframe。默认情况下它们是禁止被加载的。
这两种Mixed Content的行为都可以通过CSP来自定义。
3.2.1 静态资源
贴吧的静态资源托管于CDN上,主要有两个域名。显然此域名必须支持HTTPS,好在这个前置依赖已经ready,并不需要我们推动改造。
在HTML页面中加载的所有css和script,都是在运行时拼接的URL。其中域名部分是从配置文件中读取的,因此只要修改该配置文件即可。但还有更多的单case外链资源的URL是硬编码到代码中的,这部分需要挨个文件搜索排查。
除了css和script这两种静态资源外,无线页面上还有视频、声音和图像等媒体静态资源我们也尽可能地替换为HTTPS协议,虽然可能影响页面整体的性能,但这是值得的。
如果一个js脚本加载了另一个js脚本会怎么样呢?这对于一些独立的组件(如广告)来说非常常见。你必须确保被异步加载的js也要通过HTTPS去下载。如果不能,那么你可能需要一些非技术手段去解决它。我们推动了其它部门进行了相应的改造,这里往往是整个项目的风险点,因为如果你不是对所有业务都熟悉的话,那么依赖于外部则很容易让进度被阻塞。
3.2.2 iframe地址
iframe的地址同css和script一样,必须保证与页面协议一致(除非配置了CSP),否则不能加载。这非常严重,因为很多跨域技术(如passport)和广告都会使用iframe,直接影响KPI指标与收入。
3.3 跳转页面
采用HTTPS的网站,一般都会下发HSTS指令, 从而让浏览器直接发起HTTPS请求。对于不支持HSTS头部的浏览器,需要服务器回复一次302响应以强迫浏览器以HTTPS访问。我们希望用户能尽可能地在HTTPS协议下浏览页面,包括站内和站外的,因此,需要修改a链接的href属性。
贴吧可链接至百度的其它很多产品线,但他们并非都支持HTTPS(目前百科、文库等都不支持),因此在修改外链之前一定要确保目标站点的HTTPS协议可用性。
3.4 动态链接
其实,贴吧无线页面中的资源往往是动态部分占绝大多数,即URL是从服务端输出给前端的,无法通过硬编码的方式修改协议属性。这些URL中,有些域名是支持HTTPS的,但还有些不是。
我们的解决方案是使用正则表达式去匹配并替换,为此,我们提供了三个PHP函数,以供不同场景之用:
1 | function ssl_url_replace($url){} // 替换这个URL字符串 |
在HTTP协议下访问,这三个函数均直接返回原参数;在HTTPS下访问,则会尝试匹配参数中的URL,并替换为HTTPS地址,一般是代理服务器地址(如果原域名支持HTTPS,则直接替换协议即可)。
动态链接部分的改造量是所有类型工作中最大的,其代码分布非常之广,有相当一部分是由QA通过bug发现的。对于ssl_url_replace()的使用,往往伴随着标签或者background以及background-image内联属性。而对于其它两个函数,则很难通过搜索源代码找到其位置。
3.5 CORS
即 Cross-Origin Resource Sharing,是一种优雅的跨域通信方式,通过配置一些服务器响应头来实现。在贴吧的无线页面中,_CORS_被用于两种场景:
- 加载CDN域名上的字体文件(现代浏览器要求字体文件必须同域,除非配置CORS);
- 向其它域上传图片
如果_CORS_的目标域名支持HTTPS,则增加响应头部即可,成本很低,字体文件即是这样的一个例子。但如果不支持的话,百度的代理服务器并不方便配置这样复杂的头部,因此我们不得不花费一周的时间申请了一个新域名并做了相应的配置。
3.6 白名单
即便如此,仍然有些页面是无法在HTTPS协议下运行的,包括:
- 嵌入了第三方的不支持HTTPS的资源或iframe;
- 遗失源代码的页面;
- 暂且不想迁移的页面
是的,确实有少量的页面我们无法找到源代码,也就不能通过修改源代码来迁移到HTTPS下。至于第三方资源,贴吧目前有嵌入其它公司H5游戏的业务,目前几乎不可能推动其改造HTTPS。
虽然迁移HTTPS的大部分工作是替换链接,但URL出现的场景很多,特别要注意不要被依赖的第三方拖住进度。因此,前期的准备是十分必要的。
4. 检验
检查阶段由开发期间的QA测试以及小流量期间的数据观察构成。QA遍历所有重要业务,寻找可能的缺陷与故障。这是一项很繁杂的任务,但我们的QA以极高的测试覆盖度完成了任务,直至上线后也几乎没有发现任何致命缺陷。
贴吧的几乎所有页面都接入了公司内部的性能检测平台,数据显示,虽然接入了TLS套接层,使用了大量的正则替换函数,但页面的性能几乎没有受到影响。
开启1%小流量时,一切正常。不过随着小流量比例的增加,异常紧接而来,PV/UV等核心KPI指标开始大幅下滑,并有用户报告页面打不开。但是内网查看没有任何问题复现,经过服务端的一系列排查也一直没有发现任何问题。我们怀疑部分用户的设备在TLS1.0的使用上有问题,因此执行了一次线上统计:关闭所有的HTTPS小流量,在页面上检查对HTTPS连接的访问情况。
为了模拟真实的地址环境,我们创建了一个贴吧域名的空图片URL,使用JavaScript脚本加载它并检测请求结果,伪代码大概是这样的:
1 | var img = new Image(); |
其中*track()*是统计函数。我们最终得到的是一个成功与失败的分布比例,这个数值经过计算后十分接近100%,于是我们认为用户访问HTTPS的通路没有问题。
然而事实并非如此,经过较长时间的排查后,发现问题仍出在服务端,具体原因与公司的网络接入层架构有关,这里不表,但导致的后果是——大部分用户都不可能与服务端HTTPS所使用的443端口建立连接!
那为什么之前我们统计到的访问HTTPS的成功率这么高呢?
这个时候,我们才忽然意识到统计的方法可能有问题。这是一个特别特别小的图片,如果能访问到的话,应该是瞬间的(2G网络很比较慢,但统计数据表明我们的2G用户微乎其微),如果由于不能与服务器建立连接,那么报错的时延会非常地长,也就是onerror很晚才会被调用,这个时间可能达10几秒以上,而我们统计的页面,用户一般不会停留这么久。这个现象的后果是,失败的计数很多都丢失了,成功率自然很高。
为了验证这个猜测,我们修改了统计代码,不再一直等待onerror被触发:
1 | new Promise(function(resolve, reject) { |
关键在于setTimeout,它让程序最多等待一秒就发送了结果。经过这样的修改,我们再来看统计结果,就与理论值很接近了,验证了之前的猜测。虽然这种方案会误杀那种访问很慢但能访问得到的情况,但根据以往经验,这部分量级很少,并且我们的目标并非严格定量地统计它,这仅是一个短期的临时统计方案。
5. 总结
HTTPS改造实际上并没有太多的技巧可言,很大程度上都是一种“体力活”。但体力活并非“蛮干”,依然需要评估到所有可能的情况。我们得到的经验教训是:首先应对线上环境进行正确可信的检查,上一节中我们遇到的问题并非仅由一个原因造成的,有很多环节都可以发现问题并及时止损,但每一个环节我们都由于或行政或技术的原因与真相失之交臂,不能不说是遗憾。但反过来想我们确实达到了踩坑的目的,有效地防止了对PC端这一大头的影响,算是有一定的收获吧。其次,提前确认依赖外部团队业务的实现部分,因为你无法控制他们的资源与排期,容易拖慢你的进度。
在未来的HTTPS持续改造中,我们的流程将是:
- 验证线上HTTPS环境,保证通道畅通
- 评估所有依赖,必要时发起合作,确定时间点接洽
- 建设HTTPS开发环境
- 改造代码,尽可能所有流量都走HTTPS
- QA验证,全量覆盖,确保所有重要业务运行正常
- 小流量,可采用log函数递增方式缓慢开启流量,观察统计指标