18. TLS 1.3 新特性:Android 中的支持与配置
聊到 TLS 1.3,我其实挺感慨的。这个协议从 2018 年正式发布,到现在已经好几年了。但说实话,我在很多 Android 项目里检查代码时,发现不少人还在用 TLS 1.2,甚至更老的版本。嗯,这其实是个隐患。
TLS 1.3 不是简单的升级,它是一次彻底的协议重构。说白了,它把很多历史包袱都扔掉了。我当年第一次看 RFC 8446 时,第一反应是:「终于有人把那些陈年旧伤给治了。」
18.1 TLS 1.3 到底改了啥?
先说说最直观的变化——握手速度。TLS 1.2 完成一次完整握手需要 2-RTT(两次往返),而 TLS 1.3 只需要 1-RTT。如果你之前已经连过这个服务器,甚至能做到 0-RTT。我在一个海外项目里实测过,从上海连到美西的服务器,TLS 1.3 比 1.2 快了将近 200 毫秒。对于用户体验来说,这差别很明显。
为什么会这么快?因为 TLS 1.3 把密钥协商和证书验证合并到了一次往返里。它去掉了那些可选的、拖慢速度的步骤。你想想看,以前要来回确认好几轮,现在一轮搞定,效率自然上去了。
另一个重大变化是——它砍掉了一堆不安全的加密套件。我记得 TLS 1.2 时代,光是加密套件就有几十种,很多还是带坑的。比如 CBC 模式的密码,容易受到 padding oracle 攻击。TLS 1.3 直接把这些全删了,只保留了 AEAD 类算法(比如 AES-GCM、ChaCha20-Poly1305)。
核心变化一览:
- 握手从 2-RTT 降到 1-RTT(0-RTT 可选)
- 移除了所有不安全/过时的加密套件
- 强制使用前向安全(ECDHE 或 DHE)
- 简化了密钥派生流程
- 废弃了静态 RSA 和 DH 密钥交换
18.2 Android 对 TLS 1.3 的支持情况
这个问题我经常被问到。直接说结论:Android 10(API 29)及以上原生支持 TLS 1.3。底层用的是 Conscrypt 安全提供者,它从 Android 10 开始默认启用了 TLS 1.3。
但这里有个坑——很多国产手机厂商会魔改系统。我曾经在一台某品牌的 Android 11 手机上发现,它的 Conscrypt 版本被降级了,导致 TLS 1.3 无法正常工作。嗯,这种问题排查起来很头疼。
| Android 版本 | API 级别 | TLS 1.3 支持 | 默认启用 |
|---|---|---|---|
| Android 9 及以下 | ≤ 28 | 不支持(需第三方库) | 否 |
| Android 10 | 29 | 支持 | 是 |
| Android 11 | 30 | 支持 | 是 |
| Android 12+ | 31+ | 支持(增强) | 是 |
如果你需要兼容 Android 9 及以下设备,我建议用 Google Play 服务提供的 Provider Installer,或者直接集成 Conscrypt 的独立 jar 包。不过说实话,现在 Android 9 以下的设备占比已经很低了,我个人倾向于直接要求最低 API 29。
18.3 如何在代码中配置 TLS 1.3
配置其实很简单。如果你用的是 HttpsURLConnection,默认情况下 Android 10+ 会自动协商 TLS 1.3。但如果你想显式指定,可以这样做:
// 显式指定 TLS 1.3
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(null, null, null);
URL url = new URL("https://api.example.com");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
如果你用的是 OkHttp,那就更简单了。OkHttp 3.12+ 已经支持 TLS 1.3,而且它会自动协商最优协议版本。我个人习惯这样配置:
OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(
ConnectionSpec.RESTRICTED_TLS, // 只允许 TLS 1.2 和 1.3
ConnectionSpec.COMPATIBLE_TLS // 兼容模式
))
.build();
小提示:ConnectionSpec.RESTRICTED_TLS 只允许 TLS 1.2 和 1.3,并且强制使用前向安全的加密套件。如果你的服务器配置没问题,我建议直接用这个。
18.4 0-RTT 模式:快,但有代价
TLS 1.3 的 0-RTT 模式确实诱人——你可以在第一次请求时就带上数据,省掉一次往返。但我曾经在项目中吃过它的亏。
0-RTT 存在重放攻击的风险。什么意思呢?攻击者可以截获你的 0-RTT 数据包,然后重复发送给服务器。如果服务器没有做幂等性处理,就可能造成重复下单、重复扣款等问题。
所以我的建议是:不要在写操作(POST、PUT、DELETE)中使用 0-RTT。只用于读操作,比如 GET 请求。或者,确保你的服务器端实现了重放检测机制。
警告:Android 的 Conscrypt 实现默认禁用了 0-RTT。如果你非要开启,需要手动配置 SSLSession 的 PSK(预共享密钥)。但除非你非常清楚自己在做什么,否则别碰它。
18.5 加密套件的变化
TLS 1.3 只保留了 5 个加密套件。你没看错,就 5 个。相比 TLS 1.2 的几十个,这简直是断舍离的典范。
| 套件名称 | 密钥交换 | 认证 | 加密 | Android 支持 |
|---|---|---|---|---|
| TLS_AES_128_GCM_SHA256 | ECDHE | RSA/ECDSA | AES-128-GCM | ✅ |
| TLS_AES_256_GCM_SHA384 | ECDHE | RSA/ECDSA | AES-256-GCM | ✅ |
| TLS_CHACHA20_POLY1305_SHA256 | ECDHE | RSA/ECDSA | ChaCha20-Poly1305 | ✅ |
| TLS_AES_128_CCM_SHA256 | ECDHE | RSA/ECDSA | AES-128-CCM | ⚠️ 部分支持 |
| TLS_AES_128_CCM_8_SHA256 | ECDHE | RSA/ECDSA | AES-128-CCM-8 | ❌ 不支持 |
我个人推荐优先使用 TLS_AES_128_GCM_SHA256 或 TLS_CHACHA20_POLY1305_SHA256。前者在大多数硬件上有 AES 指令集加速,后者在低端设备上表现更好。我在一个 IoT 项目里测试过,ChaCha20 在 ARM Cortex-A 系列芯片上比 AES 快 30% 左右。
18.6 服务端兼容性检查
配置好客户端只是第一步。你还需要确认服务端是否支持 TLS 1.3。我曾经遇到过一个案例:客户端明明配好了,但连不上服务器。查了半天,发现是 Nginx 版本太老,不支持 TLS 1.3。
你可以用 OpenSSL 命令行快速检查:
openssl s_client -connect api.example.com:443 -tls1_3
如果返回了证书链和握手信息,说明服务端支持。如果报错,那就得升级服务端了。Nginx 1.14+、Apache 2.4.37+、Tomcat 9.0.13+ 都支持 TLS 1.3。
避坑指南:我曾经在项目中遇到一个奇葩问题——服务端宣称支持 TLS 1.3,但实际只支持了部分加密套件。客户端协商时选了服务端不支持的套件,结果握手失败。解决方案是:在客户端显式指定加密套件列表,不要依赖自动协商。
18.7 知识体系总览
下面这张图总结了 TLS 1.3 在 Android 中的核心要点。你可以把它当作一个快速参考。
18.8 写在最后
TLS 1.3 不是银弹,但它确实解决了很多 TLS 1.2 时代的顽疾。我个人建议,新项目直接上 TLS 1.3,老项目逐步迁移。迁移过程中注意两点:一是服务端兼容性,二是 0-RTT 的安全风险。
嗯,如果你在配置过程中遇到奇怪的问题,不妨先抓个包看看。Wireshark 里过滤 tls.handshake.type == 1 就能看到 ClientHello,确认一下协商的版本和套件。这招我用了很多年,屡试不爽。