TLS 1.3 当前(2018.10)支持与部署之现状

今年8月10日,历经三年有余,TLS 1.3 最终版本终于得以发布——RFC 8446. 关于 RFC 的详细介绍可以进一步阅读 A Detailed Look at RFC 8446 (a.k.a. TLS 1.3). TLS 1.3 因为其在握手延迟以及安全性上的改进 (可以参考拙文《TLS1.3/QUIC 是怎样做到 0-RTT 的》),毫不夸张的说,这是一件将深刻而长远影响互联网发展的技术里程碑。那么将 TLS 1.3 尽快平滑应用到线上环境无疑是一件势在必行的事情了。

在我日常的工作中,对于 TLS 1.3 的支持和部署主要关注两个层面: 编程语言(Go, Java)、 API gateway (Nginx) 和浏览器. 下面分别介绍一下这三个层面 TLS 1.3 的支持部署现状。(因为 RFC 8446 已经发布,因此本文中说的支持如无特殊说明,都是指对最终版本 RFC 8446 的支持。)

编程语言对 TLS 1.3 的支持

Go 方面,官方在 TLS 1.3 draft 阶段一直没有跟进。因此,有一个关于对 TLS 1.3 支持的 issue 从 2015 年 open 至今都没有关闭:crypto/tls: add support for TLS 1.3. 其实 Go 发布版本和改进标准库的效率还是挺高的,对于 TLS 1.3 上的“不作为”更多是因为 Go 在兼容性上的承诺导致其并适合在最终版发布前实现互不兼容的 draft 方案。

而 Go 1.11 的发布时间(2018.08.24)与 RFC 8446 的发布时间比较接近,没有足够时间实现并发布该特性。从 golang-dev 小组讨论 Re: crypto/tls and TLS 1.3 看,由于 1.11 没有实现 TLS 1.3 ,那么 1.12 实现 TLS 1.3 基本是板上钉钉的事了:

The key scheduling fact is that the Go 1.11 feature freeze is just a week away, so we decided that it would be too rushed to merge the 1.3 patches for it. I definitely aim to have TLS 1.3 in Go 1.12.

根据惯例, Go 1.12 的发布时间将会是 2019.02~03. 如果期间你实在想用 Go 编程测试 TLS 1.3, 可以尝试使用 CloudFlare 的 tls-tris 库。根据 Go net/http 标准库维护者 Brad Fitzpatrick 的消息,这个库将会被合并到标准库作为 Go 官方 TLS 1.3 的实现。因此,如果你不得不用这个库干一些生成环境的活也大可放心,即使日后升级 Go 1.12, 接口兼容性还是有保证的。

Java 方面,由于 Java 11 出生时间好(2018.09.25), 因此是出生就支持 TLS 1.3 RFC 8446, 具体可以参见 JEP 332: Transport Layer Security (TLS) 1.3. Java 11 是 LTS 版本,因此,如果有条件升级到 11, 推荐使用 Java 11 实现的 TLS 1.3 以及配套的 HttpClient;如果生产环境暂无法升级 Java 版本,推荐使用 OkHttp. 关于 Java Http Client 选型可以参见Java HTTP 组件库选型看这篇就够了

Nginx 对 TLS 1.3 的支持

准确讲应该是 Nginx 所使用 SSL lib 对 TLS 1.3 的支持。在这方面,Boring SSL 跟进速度飞快,在 RFC 发布后第4天实现了对最终版本的支持。OpenSSL 虽然很早就跟进了 draft 的实现,但是对最终版本的支持需要 1.1.1-pre9 及以后的版本:

The OpenSSL git master branch (and the 1.1.1-pre9 beta version) contain our development TLSv1.3 code which is based on the final version of RFC8446 and can be used for testing purposes (i.e. it is not for production use). Earlier beta versions of OpenSSL 1.1.1 implemented draft versions of the standard. Those versions contained the macro TLS1_3_VERSION_DRAFT_TXT in the tls1.h header file which identified the specific draft version that was implemented. This macro has been removed from 1.1.1-pre9 and the current master branch.

TLSv1.3 is enabled by default in the latest development versions (there is no need to explicitly enable it). To disable it at compile time you must use the “no-tls1_3” option to “config” or “Configure”.

Although the latest 1.1.1 versions support the final standard version, other applications that support TLSv1.3 may still be using older draft versions. This is a common source of interoperability problems. If two peers supporting different TLSv1.3 draft versions attempt to communicate then they will fall back to TLSv1.2.

而第一个 OpenSSL 1.1.1 release 是在 2018.09.11, 因此如果你跟我一样是 OpenSSL 的死忠粉,当前阶段 Nginx 支持 TSL 1.3 的最佳方式是 Nginx 1.15.5 + OpenSSL 1.1.1. 而这种脏活、苦活、累活当然是交给 Docker 解决了:从源代码编译 nginx docker 镜像开启 TLS 1.3,项目地址可以参见 docker-nginx.

配置 Nginx 支持 TLS 1.3 需要注意一点:默认情况下 Nginx 因为安全原因,没有开启 TLS 1.3 0-RTT,可以通过添加 ssl_early_data on; 指令开启 0-RTT. 完整配置可以参考 nginx.conf.

浏览器对 TLS 1.3 的支持

当前阶段,Chrome 69 和 Firefox 62 都只支持到 draft 28, 而 draft 28 与最终版本是不兼容的。因此,要测试体验 TLS 1.3 final 需要使用 Chrome Beta 测试版。然后在 chrome://flags/#tls13-variant 开启 TLS 1.3 final:

扩展阅读

Java HTTP 组件库选型看这篇就够了

Java HTTP 组件库选型看这篇就够了

最近项目需要使用 Java 重度调用 HTTP API 接口,于是想着封装一个团队公用的 HTTP client lib. 这个库需要支持以下特性:

  1. 连接池管理,包括连接创建和超时、空闲连接数控制、每个 host 的连接数配置等。基本上,我们想要一个 go HTTP 标准库自带的连接池管理功能。
  2. 域名解析控制。因为调用量会比较大,因此希望在域名解析这一层做一个调用端可控的负载均衡,同时可以对每个服务器 IP 进行失败率统计和健康度检查。
  3. Form/JSON 调用支持良好。
  4. 支持同步和异步调用。

在 Java 生态中,虽然有数不清的 HTTP client lib 组件库,但是大体可以分为这三类:

  1. JDK 自带的 HttpURLConnection 标准库;
  2. Apache HttpComponents HttpClient, 以及基于该库的 wrapper, 如 Unirest.
  3. 非基于 Apache HttpComponents HttpClient, 大量重写应用层代码的 HTTP client 组件库,典型代表是 OkHttp.

HttpURLConnection

使用 HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。以发起一个 HTTP POST 请求为例:

可以看到,使用 HttpURLConnection 发起 HTTP 请求是比较原始(low level)的,基本上你可以理解为它就是对网络栈传输层(HTTP 一般为 TCP,HTTP over QUIC 是 UDP)进行了一次浅层次的封装,操作原语就是在打开的连接上面写请求 request 与读响应 response. 而且 HttpURLConnection 无法支持 HTTP/2. 显然,官方是知道这些问题的,因此在 Java 9 中,官方在标准库中引入了一个 high level、支持 HTTP/2 的 HttpClient. 这个库的接口封装就非常主流到位了,发起一个简单的 POST 请求:

封装的最大特点是链式调用非常顺滑,支持连接管理等特性。但是这个库只能在 Java 9 及以后的版本使用,Java 9 和 Java 10 并不是 LTS 维护版本,而接下来的 Java 11 LTS 要在2018.09.25发布,应用到线上还需要等待一段时间。因此,虽然挺喜欢这个自带标准库(毕竟可以不引入三方依赖),但当前是无法在生产环境使用的。

Apache HttpComponents HttpClient

Apache HttpComponents HttpClient 的前身是 Apache Commons HttpClient, 但是 Apache Commons HttpClient 已经停止开发,如果你还在使用它,请切换到 Apache HttpComponents HttpClient 上来。

Apache HttpComponents HttpClient 支持的特性非常丰富,完全覆盖我们的需求,使用起来也非常顺手:

对 Client 细致的配置和自定义支持也是非常到位的:

完整示例请参考 ClientConfiguration.

基本上,在 Java 原生标准库不给力的情况下,Apache HttpComponents HttpClient 是最佳的 HTTP Client library 选择。但这个库当前还不支持 HTTP/2,支持 HTTP/2 的版本还处于 beta 阶段(2018.09.23),因此并不适合用于 Android APP 中使用。

OkHttp

由于当前 Apache HttpComponents HttpClient 版本并不支持 HTTP/2, 而 HTTP/2 对于移动客户端而言,无论是从握手延迟、响应延迟,还是资源开销看都有相当吸引力。因此这就给了高层次封装且支持 HTTP/2 的 http client lib 足够的生存空间。其中最典型的要数OkHttp.

OkHttp 接口设计友好,支持 HTTP/2,并且在弱网和无网环境下有自动检测和恢复机制,因此,是当前 Android APP 开发中使用最广泛的 HTTP clilent lib 之一。

另一方面,OkHttp 提供的接口与 Java 9 中 HttpClint 接口比较类似 (严格讲,应该是 Java 9 借鉴了 OkHttp 等开源库的接口设计?),因此,对于喜欢减少依赖,钟情于原生标准库的开发者来说,在 Java 11 中,从 OkHttp 切换到标准库是相对容易的。因此,以 OkHttp 为代表的 http 库以后的使用场景可能会被蚕食一部分。

小结

  • HttpURLConnection 封装层次太低,并且支持特性太少,不建议在项目中使用。除非你的确不想引入第三方 HTTP 依赖(如减少包大小、目标环境不提供三方库支持等)。
  • Java 9 中引入的 HttpClient,封装层次和支持特性都不错。但是因为 Java 版本的原因,应用场景还十分有限,建议观望一段时间再考虑在线上使用。
  • 如果你不需要 HTTP/2特性,Apache HttpComponents HttpClient 是你的最佳选择,比如在服务器之间的 HTTP 调用。否则,请使用 OkHttp, 如 Android 开发。

扩展阅读

那些产品设计中的小惊喜

虽然技术出身,但是日常工作中经常需要单独的设计一些产品的原型,因此,需要对产品有一些感觉。

你也许被《人人都是产品经理》“荼毒”过,不用悲伤,我也是;你也许跟我一样技术出身,习惯了从技术角度看功能和需求,对产品和业务比较麻木,不要心急,我也是。你也许无数次的跟产品经理争论这个功能的合理性和必要性,谁也说服不了谁,最后你还是做了这个功能,仅仅是“厌恶”了跟产品无意义的浪费时间,没有关系,我也是。

然而,你要知道,大多数时候我们要做的产品设计其实都被前辈验证和解决过,是存在最佳实践的,也是可以从其他产品习得的。这种习得可能是一个特定的产品实现方式,但更多的是产品设计的思考和思路。这种习得,产品经理的整套方法论并不是必要条件,只需要你在使用产品的时候有意识的培养发现问题的敏感性和好奇心。

这里,我先把这几天使用产品过程中看到和体会到的一些产品设计中有意思的「小惊喜」整理一下,以后会不定期更新,争取能形成一个系列。

登录页面:简单的功能比你想象的更难更重要

随着 OAuth 以及开放平台的发展,国内绝大部分产品都支持微信、微博、QQ等多种社交账号的登录方式。这类登录方式不仅可以降低用户登录/注册的心里决策和输入成本,还能够从技术上简化整个账号系统的设计。因此,除了阿里和腾讯自成一体的公司产品,绝大多数产品都会在第一时间支持这类登录方式。然而,把这件事情做好的产品却寥寥无几。

以国内技术社区 segmentfault 为例,很多产品的社交账号登录页面可能是这样的:

从功能层面看,支持的登录方式挺多,把使用频率高的登录方式放在前面,这都没有问题。但是,我们的用户是健忘也是“无情”的。如果因为某种原因(APP更新、网络异常、更换手机等),用户需要重新登录,当用户再次看到这个页面的时候,是比较抓狂的:因为“登录”功能的使用频率很低,用户有很大概率不记得自己当初是以什么方式登录进来的;于是用户只能一个一个的去尝试不同社交账号登录方式来判断自己以前的账号是怎么登录的(这样做唯一能想到的好处就是注册用户数会提高,提前完成 KPI ……)。即使用户粘性高如微信,你去问问身边的朋友,他们的微信是手机号登录还是QQ登录,还是微信号登录,大部分人是搞不清楚的。

几天前用喜马拉雅听《晓说》,因为前一天晚上更新过APP,登录状态被踢了出来(这是个很糟的体验),尝试重新登录:

左下角提示微信为上次登录方式,这让我有了一点惊喜的感觉!

这是最佳的实践方式吗?私以为还可以再进一步:

之所以更加推荐这种将用户上次登录的头像和昵称显示出来的方式,不是因为这是东家咕咚APP采用的方式,而是因为,1)图片的阅读或者说注意力效率是远高于文字的;2)关系亲近的人之间可能会共用设备,显示头像和昵称有助于提高账号和登录方式判定的准确率。

在很多人眼里,登录页面这些改进太过于“鸡毛蒜皮”。但是如果你们的APP在登录页面的各个环节都有埋点的话,你也对这些数据足够熟悉的话,你会发现这些微不足道的改进对用户转化率的影响比你想象的重要得多。

说起转化率,使用社交账号登录一个非常常见也是非常糟糕的设计是:社交账号登录后,要求用户绑定手机/邮箱并设定密码。我能理解绑定手机号在我朝当前实名制政策下的无奈,但是大部分业务其实是不需要强制绑定手机号的。如果你是担心太过于依赖微信,要求用户必须有独立的登录用户民和密码,你应该想想你是不是太把自己当回事了?大部分情况下,我遇到这种社交账号登录后要求用户绑定手机和邮箱的服务,都会默默问候一个“产品13B”,然后直接关闭页面。

懂场景的音乐 APP

在听觉上,我应该算是典型的木耳。但是偏偏有喜欢在开车的时候听音乐。以前一直用豆瓣 FM, 歌曲推荐做的真心赞。但是豆瓣 FM 登录模块实在太渣,会因为各种原因把我从登录状态踢出来要求重新登录,老哥,我在开车呢,这样真的好吗?几经辗转,一次出行的时候,偶然打开了虾米音乐,因为连接了车载蓝牙,判断我是要开车听音乐,询问我是否进入车载模式:

进入车载模式以后,界面就变成跟老年手机一样,按钮巨大,按键也只有简单的几个上一曲,下一曲,暂停。心里默默念了一句: MMP, 这正是我想要的!

还没听完一首歌,又发现了一个小惊喜:

虾米把显示作者信息的地方用来显示歌曲的单词了!很Low是不是?是!很解决问题是不是?当然是!以后在车里想哼两句的时候终于不用因为不知道(不是不记得哟)歌词而随机脑补了。我知道很多技术背景的同学可能会对这种实现方式嗤之以鼻,但是以我对汽车软件行业平均水平的了解和自己折腾这台车歌词显示的经历看,这就是通用性最好、成本最低的方案!

坦率讲,虾米最主要的使用场景肯定不是车载,但是,透过这两个细节,我还是能够非常清晰的感受到虾米的产品在做音乐类 APP 是对场景的细致、全面的思考和实践。我们常常说一个功能或产品好用不好用,这其实是比较抽象和模糊的,如果你想找到具体的问题和原因,回到具体的场景和交互上,你一定能发现一些有趣的问题,同时给用户带来惊喜。

微信悬浮窗:悬浮的内容,流逝的时间

微信在不久前迎来了它的年度更新,其中最为大家津津乐道的一点就是悬浮窗设计:

但凡是常用微信进行阅读的同学,都能深刻的体会阅读文章与聊天消息之间切换的痛苦。悬浮窗一出,很多人大呼痛快。

使用一段时间后,发现了一个细节,随着悬浮时间的变长,悬浮的图标会逐渐变红,最终变成全红。有人说,悬浮窗是微信拯救不断被抖音等APP吞噬的APP使用时间所祭出的武器,此言不虚,但我想也一定有产品对提供更加良好的阅读体验的细致入微的思考和设计,毕竟,大家都是成年人了,「稍后阅读」往往就变成了「永远不读」。另一方面,我也比较悲观的认为,碎片化阅读其实是有非常明显的局限和天花板的,悬浮窗能做的已经接近这个极限。如果你真的是一个重度的微信公众号订阅者,悬浮窗是解决不了你的问题的,你应该关注一下订阅号展示形式的变化。

验证码繁与简

小区楼下有两个自动存取货柜,一般没人在家的时候,我都喜欢让快递老哥帮我把快递放在货柜中。回来的时候,凭验证码提取快递。一家是已经被中国邮政收编的速递易,它的验证码是这样的:

一家是顺丰家的丰巢,它的验证码是这样的:

都是6位验证码,不同的是前者使用的书“数字+字母”组合,后者只使用了数字。前者应该更加符合工程师思维:验证码空间要足够大,这样才能减少碰撞的概率,降低安全风险。但是,我倒觉得后者其实对验证码的认识更加深刻:

  1. 验证码是需要人工输入的,输入方式越简单成本越低。打一个不恰当的对比,前者需要用户会标准键盘输入,后者只需要用户知道如何打电话就够了。“会标准键盘输入”这个要求很过分吗?对你也许都不值一提,但是你要知道小区很多大爷大妈连手机上的T9输入法都不会用,只会手写输入,你要他会基本的标准键盘输入,显然是对你的用户群不够了解。
  2. 绝大部分用户短时间记忆一串数字是经过训练的,这种训练贯穿我们的小学数学教育以及功能机时代的“背电话号码训练”。因此,对于喜欢晚上出来遛弯取快递的同学来说,他们是可以看一眼验证码,然后放心的不带手机去取货。而背诵数字+字母组合,对很多人来说几乎就是不可完成的任务。
  3. 部分数字和字母字符不易区分,引入额外的错误率。典型字符如数字“1”和字母“I”, 数字“0”和字母“O”。
  4. 货柜的安全问题其实不在验证码。这个不方便细说,可以讲的是,如果你推演一下如何非正常手段获取货柜的的货物,你最后推演出来的方法一定不是傻乎乎的到带有好几个摄像头的货柜前去试验证码。

在验证码这个问题上,并不是说丰巢做的多好,而是想说速递易实在做得太烂:产品上的很多问题发展到今天已经是被很好的解决了,如果想在一些常用的产品设计上创新,请一定思考清楚这个「改变」是仅仅为了不同,还是因为当前设计存在很具体的问题。

浏览器地址栏存在多年的 bug

相信绝大部分人都注意到我们使用的浏览器地址栏是可以显示中文的:

但是,较真的工程师会告诉你:URL 地址的合法字符是 ASCII 字符的子集,是不支持中文的。也许浏览器地址栏显示中文是一个 bug?

虽然,平日里我是不太支持研发同学说 “这不是bug, 这是feature”,但是,这一次,我得说这的确是浏览器的一个 feature: 用户毕竟不是工程师,对于他们来讲,他们不关心也不在乎地址栏上面的字符是否经过 urlencode, 地址栏提供给他们的信息越直接越好。从这个逻辑出发,我们就不难理解地址栏中的安全绿色小锁以及直接显示中文了。当然,为了确保技术上的正确性,你通过 Chrome 地址栏复制的和开发者工具上看到的 URL 地址其实是经过 urlencode 过的: