Android 网络深度系列 · 第 2 篇
系列导航:第1篇:HTTP 协议全解 | 第2篇:HTTPS 与网络安全 | 第3篇:OkHttp 架构剖析 | 第4篇:Retrofit 原理与实战 | 第5篇:WebSocket 与长连接 | 第6篇:网络实战场景
前言
"把 HTTP 换成 HTTPS 就安全了"--这句话只说对了一半。
HTTPS 确实加密了通信内容,但如果你不知道它加密什么、不加密什么,不知道证书是如何被信任的,那你的 App 可能只是穿了件皇帝的新衣。Charles 能抓包、Fiddler 能解密、某些 CA 被入侵过、证书链验证可能跳过......这些现实告诉我们:理解 HTTPS 的内部机制,是 Android 开发者的基本功,不是加分项。
本文从密码学基础讲起,一路走到 TLS 握手、证书体系、MITM 防护,最后给出 Certificate Pinning 的完整实战,帮你建立 HTTPS 安全的完整认知。
1. 密码学基础
在理解 TLS 之前,先把四种核心密码学工具搞清楚。
1.1 对称加密
典型算法:AES(Advanced Encryption Standard)
原理:通信双方使用同一个密钥进行加密和解密。
优点:快。AES 硬件加速在现代 CPU 上是标配,加密几 MB 数据几乎无感知。
致命问题:密钥分发。怎么把密钥安全地传给对方?如果在线上传密钥,那这个密钥本身也需要加密保护--这就成了鸡生蛋的问题。
实际场景:你给同事发了一个加密 ZIP 文件,密码通过微信发过去。微信传输就是那个"不安全的密钥分发通道",但只要你觉得微信那端确实是他本人,这就够了。但在互联网上,你没法确定另一端是谁。
1.2 非对称加密
典型算法:RSA、ECDSA(椭圆曲线)
原理:一对密钥--公钥公开,私钥保密。用公钥加密的数据只有私钥能解密。
优点:解决了密钥分发问题--任何人都能用你的公钥给你发密文,只有你能解密。
缺点:慢。RSA-2048 加密速度比 AES-256 慢上千倍。不适合加密大量数据。
重要区别:
- 加密:用对方的公钥加密 → 对方用私钥解密 → 保密性
- 签名:用自己的私钥签名 → 对方用你的公钥验签 → 真实性
1.3 混合加密方案
这是实际 TLS 采用的方式:
1. 客户端生成随机对称密钥(会话密钥)
2. 用服务器的公钥加密这个对称密钥,发给服务器
3. 服务器用私钥解密,拿到对称密钥
4. 双方用这个对称密钥进行 AES 加密通信
为什么这样做?非对称加密只用来交换一个小数据(对称密钥),后续大量数据用对称加密传输。既解决了密钥分发问题,又保证了性能。
这就是"混合加密"--集合两种加密方式的优点。
1.4 摘要算法
典型算法:SHA-256、MD5(不推荐)
原理:任意长度输入 → 固定长度输出(摘要/指纹)。单向不可逆。
用途:
- 验证数据完整性:下载文件后算 SHA-256,对比官方指纹
- 密码存储:存密码的哈希值,不存明文
- 数字签名的基础
1.5 数字签名
作用:证明"这确实是我发的,且内容没被改过"。
流程:
- 对消息做 SHA-256 摘要
- 用自己的私钥加密这个摘要 → 签名
- 接收方用你的公钥解密签名 → 拿到摘要
- 接收方也对消息做 SHA-256 → 对比两个摘要
注意:这里用的是"私钥加密、公钥解密",和前面说的"公钥加密、私钥解密"对称相反。这是签名和加密的区别--签名是为了证明身份,加密是为了保密。
2. TLS 握手全流程
有了上面的基础,现在来看 TLS 握手。我们将对比 TLS 1.2 和 TLS 1.3,理解为什么 1.3 更快、更安全。
2.1 TLS 1.2 握手(2-RTT)
以最常见的 ECDHE 密钥交换为例,完整握手需要两个网络往返(2-RTT):
客户端(App) 服务器
| |
|--- 1. ClientHello ----------------->|
| (TLS 版本, 密码套件列表, |
| ClientRandom, |
| 支持的椭圆曲线) |
| |
|<-- 2. ServerHello ------------------|
| (选定的 TLS 版本, 密码套件, |
| ServerRandom) |
| |
|<-- 3. Certificate ------------------|
| (服务器证书链) |
| |
|<-- 4. ServerKeyExchange ------------|
| (ECDHE 公钥参数, 签名) |
| |
|<-- 5. ServerHelloDone --------------|
| |
|--- 6. ClientKeyExchange ----------->|
| (客户端 ECDHE 公钥参数) |
| |
|--- 7. ChangeCipherSpec ------------>|
|--- 8. Finished (加密) -------------->|
| |
|<-- 9. ChangeCipherSpec -------------|
|<-- 10. Finished (加密) <------------|
| |
|===== 加密通信开始 ====================|
一步一步解释:
- ClientHello:客户端打招呼,告诉服务器自己支持什么。包括 TLS 1.2/1.3、密码套件列表(如
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)、一个 32 字节的随机数、支持的椭圆曲线等。 - ServerHello:服务器从客户端列表里挑一个双方都支持的组合,也发回自己的随机数。
- Certificate:服务器把自己的证书链发给客户端。客户端需要验证这个证书链的有效性(详见第 3 节)。
- ServerKeyExchange:服务器发送 ECDHE 的临时公钥参数,并用服务器的私钥对参数签名。客户端用服务器证书里的公钥验签,确保这个参数确实是服务器发的。
- ServerHelloDone:服务器说"我发完了"。
- ClientKeyExchange:客户端发送自己的 ECDHE 临时公钥参数。现在双方都有了对方的临时公钥,可以算出同一个预主密钥(Pre-Master Secret)。
7~8. ChangeCipherSpec + Finished:客户端说"我要切到加密了",然后用计算出的会话密钥加密一个 Finished 消息发给服务器。服务器解密验证,确保握手过程没被篡改。
9~10. 服务器同样切到加密,发回加密的 Finished。
至此握手完成,双方有了相同的会话密钥,开始对称加密通信。
为什么需要两个 RTT?因为第一次往返(1-5)协商参数和发证书,第二次往返(6-10)交换密钥参数并验证。在延迟 200ms 的网络环境下,光握手就要 400ms。
2.2 TLS 1.3 握手(1-RTT)
TLS 1.3 把握手压缩到一个往返:
客户端 服务器
| |
|--- ClientHello ------------------------>|
| (支持的版本, |
| key_share: 客户端 ECDHE 公钥参数, |
| 支持的密码套件) |
| |
|<-- ServerHello -------------------------|
| (选定版本, |
| key_share: 服务器 ECDHE 公钥参数, |
| 选定密码套件) |
|<-- Certificate (加密) ------------------|
|<-- CertificateVerify (加密) -------------|
|<-- Finished (加密) ---------------------|
| |
|--- Finished (加密) -------------------->|
| |
|===== 加密通信开始 =======================|
关键变化:
- ClientHello 直接带 key_share:客户端猜测服务器会用什么椭圆曲线,直接把 ECDHE 公钥参数带上。猜错了大不了服务器回一个 Retry,但大部分情况猜对(主流是 X25519),就省了一个往返。
- 移除不安全算法:TLS 1.3 移除了 RSA 密钥交换、CBC 模式加密、RC4、3DES、SHA-1 等所有被认为不安全的算法。密码套件从 30+ 种精简到 4 种。
- 握手消息加密传输:服务器的 Certificate 消息在 1.3 中是加密的--客户端拿到会话密钥后才能验证证书。这增强了隐私性(别人不知道你连接了哪个服务器)。
2.3 0-RTT 模式
TLS 1.3 还支持 0-RTT--如果之前连接过,客户端可以直接发送数据:
客户端(之前连接过,有 PSK) 服务器
| |
|--- ClientHello + 0-RTT Data ---------->|
| (PSK 标识, key_share, |
| 0-RTT 加密数据) |
|<-- ServerHello + Finished + 响应 -------|
风险:0-RTT 数据有重放攻击问题--攻击者截获 0-RTT 数据包可以重复发送,让服务器执行多次同样的操作。
最佳实践:0-RTT 数据必须是幂等的(即重复执行不会产生副作用)。比如 HTTP GET 请求可以用 0-RTT,但 POST 转账不行。
2.4 Android 上的实践要点
在 Android 开发中,你不需要手动处理 TLS 握手--OkHttp 和系统 SSLSocket 已经帮你做了。但有几点需要注意:
// 限制最低 TLS 版本(Android 5.0+ 默认已支持 TLS 1.2)
// Android 10+ 默认启用了 TLS 1.3
val connectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.cipherSuites(
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// ... 其他安全套件
)
.build()
val client = OkHttpClient.Builder()
.connectionSpecs(listOf(connectionSpec))
.build()
为什么手动限制? 有些老旧服务器只支持 TLS 1.0/TLS 1.1,但这两个版本有已知安全漏洞(POODLE、BEAST 攻击)。如果非要连接老服务器,需要单独处理。
3. 证书体系
TLS 握手的第 3 步,服务器发来证书。客户端凭什么信任它?
3.1 X.509 数字证书
证书是一个结构化文件,包含以下字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| Version | 证书版本 | V3 |
| Serial Number | 证书唯一编号 | 04:9B:7F:... |
| Signature Algorithm | 签名算法 | sha256WithRSAEncryption |
| Issuer | 颁发者(CA) | /C=US/O=DigiCert Inc/CN=DigiCert TLS RSA SHA256 2020 CA1 |
| Validity | 有效期 | Not Before: Jan 1 00:00:00 2024 GMT |
| Subject | 主体(网站所有者) | /C=US/ST=CA/O=Google LLC/CN=*.google.com |
| Subject Public Key Info | 公钥和算法 | RSA 2048 bits |
| Extensions | 扩展信息 | Subject Alternative Name(SAN)、Key Usage 等 |
最关键的部分:
- Subject + SAN:声明这个证书属于哪个域名
- Subject Public Key Info:你拿到的服务器公钥
- Issuer:谁担保这个证书是真的
- 签名:Issuer 对证书内容的数字签名
3.2 证书链验证
证书很少是单独一张--它是一整条链:
根 CA 证书 (Root CA)
└─ 签名了 ──→ 中间 CA 证书 (Intermediate CA)
└─ 签名了 ──→ 服务器证书 (Leaf/Server Certificate)
验证过程:
- 拿到服务器证书,看 Issuer 是谁
- 用 Issuer 的公钥验服务器证书上的签名
- 如果签名通过,继续看 Issuer 的证书
- 重复直到看到根 CA
- 根 CA 证书必须在系统的受信任列表中
关键问题:谁验证根 CA 的签名?答案是--不需要验证。根 CA 证书是自签名的,它被信任不是因为它有上级背书,而是因为它被预装到你的操作系统/设备中。
3.3 Android 的信任体系
Android 系统预装了约 150-200 个根 CA 证书,这些是受信任的:
- 主流 CA:DigiCert、GlobalSign、Let's Encrypt、Sectigo 等
- 位置:
/system/etc/security/cacerts/ - 每个证书一个文件,以哈希值命名
Android 7.0(API 24)的重要变化:之前版本默认信任用户安装的 CA 证书;7.0+ 改为默认只信任系统预装 CA,用户安装的证书不会自动用于 App。
为什么?因为用户可能为了方便抓包而安装 Charles CA--这就让整个系统的 HTTPS 都变得可被中间人窃听。
3.4 自签名证书
开发环境经常使用自签名证书--自己当 CA、自己签发。
# 生成自签名根 CA
openssl req -x509 -newkey rsa:2048 -keyout ca-key.pem -out ca-cert.pem -days 365
# 用根 CA 签发服务器证书
openssl req -newkey rsa:2048 -keyout server-key.pem -out server-csr.pem
openssl x509 -req -in server-csr.pem -CA ca-cert.pem -CAkey ca-key.pem -out server-cert.pem -days 365
Android 开发中,自签名证书通过 network_security_config.xml 信任:
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">staging.example.com</domain>
<trust-anchors>
<!-- 信任自定义的 CA -->
<certificates src="@raw/my_ca"/>
</trust-anchors>
</domain-config>
</network-security-config>
然后在 AndroidManifest.xml 中引用:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
4. 中间人攻击(MITM)
理解了证书体系,就很容易理解 MITM--本质上就是证书替换。
4.1 攻击原理
正常通信:
客户端 ←--TLS 加密--→ 服务器(证书:good.com)
MITM 攻击:
客户端 ←--TLS 加密--→ 攻击者(证书:bad.com)←--TLS 加密--→ 服务器
↑
攻击者用自己的证书替换了服务器的证书
如果客户端信任了攻击者的证书,那攻击者就可以:
- 解密客户端发来的数据(包括密码、Token)
- 修改数据后发往服务器
- 修改服务器返回的数据再发给客户端
4.2 Charles/Fiddler 的本质
Charles 就是一个合法的 MITM 工具。
它之所以能工作,是因为你手动信任了 Charles 的 CA 证书:
- 你在设备上安装并信任了 Charles CA 证书
- Charles 拦截 HTTPS 请求,用自己的证书冒充服务器
- 因为你的设备信任了 Charles CA,客户端(浏览器/App)认为连接是安全的
- Charles 解密、展示、修改请求内容
- Charles 再和真正的服务器建立加密连接
Android 7.0 之前:App 默认信任用户 CA → 安装 Charles CA 后全部 App HTTPS 都可抓包。
Android 7.0 之后:App 默认只信任系统 CA → 除非 App 主动信任用户 CA(通过 network_security_config 的 debug 配置),否则 Charles 抓不到。
4.3 Android 开发者的处理方式
开发调试仍需抓包,在 network_security_config.xml 中区分环境:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Debug 构建:信任用户证书,方便抓包 -->
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>
<!-- Release 构建:只信任系统证书 -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
这样 debug 构建时可以抓包,release 构建不受影响。
5. Certificate Pinning(证书锁定)
前面说了,HTTPS 的安全性依赖于 CA 体系。但如果 CA 本身出问题了呢?
5.1 为什么要 Pinning
2011 年,荷兰 CA 公司 DigiNotar 被入侵,攻击者签发了 500+ 张假证书,包括 *.google.com、*.twitter.com、*.facebook.com。这意味着攻击者可以用假证书冒充这些网站,而用户的浏览器/App 会因为证书链验证通过而完全信任。
DigiNotar 后来破产清算,但这个事件告诉整个行业:仅仅信任 CA 是远远不够的。 你需要确认你连接的就是你的服务器,而不是任何 CA 都能担保的服务器。
Certificate Pinning 的核心思想:在客户端额外校验--直接指定"我只信任这个证书"或"我只信任这把公钥",不信任任何其他证书。
5.2 Pin 什么:公钥 vs 证书
Pin 证书:校验服务器证书的完整摘要(SHA-256 指纹)。
# 获取证书指纹
openssl x509 -in cert.pem -noout -fingerprint -sha256
# 输出: SHA256 Fingerprint=AB:CD:EF:...
问题:证书会更新,每次更新都要重新发版。
Pin 公钥:提取证书中的公钥,计算其 SHA-256 摘要。
# 获取公钥指纹
openssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
为什么 Pin 公钥更好:证书更新时公钥可能不变(证书可以续用到同一对密钥)。如果你 Pin 了中间 CA 的公钥,只要这个中间 CA 不换密钥对,你的 App 就不需要更新。
5.3 OkHttp CertificatePinner 实战
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 当前证书公钥指纹
.add("api.example.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备用证书公钥指纹
.add("api.example.com",
"sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=") // 中间 CA 公钥指纹
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
运行机制:OkHttp 在 TLS 握手完成后,从服务器拿到的证书链中提取所有公钥,计算 SHA-256 指纹,和 Pin 列表中的指纹比对。只要有一个指纹匹配就算通过。
5.4 风险与最佳实践
最大风险:忘记更新 Pin,App 大面积不可用。
真实案例:某知名 App 因为证书轮换后忘记更新 Pin,导致老版本全部无法联网,用户无法升级,只能通过推送热修复或重新安装解决。
最佳实践清单:
- Pin 至少两个密钥:当前证书公钥 + 备用证书公钥。轮换密钥前先部署新 App 版本,加新 Pin,再换证书,再移除旧 Pin。
- Pin 中间 CA 公钥(推荐):中间 CA 的密钥对生命周期通常更长(5-10 年),不需要频繁轮换。
- 不要 Pin 根 CA 公钥:根 CA 密钥被泄露是灾难级别的事件,且不同中间 CA 会使用不同的根 CA。
- 提供降级开关:在远程配置中设置 Pinning 开关,出问题时可以远程关闭 Pinning:
// Remote Config 控制 Pinning 是否启用
class PinningInterceptor(private val isPinningEnabled: () -> Boolean) : Interceptor {
private val pinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
override fun intercept(chain: Interceptor.Chain): Response {
if (!isPinningEnabled()) {
return chain.proceed(chain.request())
}
// OkHttp 会自动应用 CertificatePinner
return chain.proceed(chain.request())
}
}
- 测试覆盖:证书轮换后务必在真实设备上测试老版本和新版本的连接。
5.5 什么时候需要 Pinning
| 场景 | 建议 |
|---|---|
| 银行/金融 App | 强烈建议 Pinning |
| 社交 App | 建议 Pinning 中间 CA |
| 普通内容 App | 可选,看安全需求 |
| 内部工具 | 可以不 Pin |
| 依赖 CDN(证书频繁换) | 建议 Pin 中间 CA |
6. Android 网络安全实战
6.1 network_security_config.xml 完整示例
一个完善的网络安全配置远不止信任证书:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 基础配置:禁止明文 HTTP -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<!-- Debug 覆盖:信任用户安装的证书 -->
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>
<!-- API 域名:额外信任自定义 CA -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<trust-anchors>
<certificates src="system" />
<certificates src="@raw/my_ca" /> <!-- 自定义 CA -->
</trust-anchors>
</domain-config>
<!-- 内部服务器:允许明文 HTTP(内网环境) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain> <!-- Android 模拟器访问 localhost -->
<domain includeSubdomains="true">192.168.1.1</domain> <!-- 注意:domain 标签不支持 CIDR,需逐个列出 IP -->
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
<!-- CDN 域名:不信任用户证书(防止被中间人) -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">cdn.example.com</domain>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>
6.2 debug 与 release 环境分离
上面配置中 debug-overrides 确保只有 debug APK 信任用户证书。可以用 build 变体进一步控制:
// build.gradle
android {
buildTypes {
debug {
// 使用 debug 版本的 network_security_config
// 默认 src/debug/res/xml/network_security_config.xml
}
release {
// src/main/res/xml/network_security_config.xml
// 或者完全不引用,使用系统默认
}
}
}
更好的做法:在 src/debug/ 和 src/release/ 分别放不同的配置文件。
6.3 明文流量限制
Android 9(API 28)开始,默认禁止明文 HTTP 通信。
如果你的 App 还使用 HTTP 连接,会抛出 Cleartext HTTP traffic not permitted 异常。
解决方案:
- 全部切换到 HTTPS(推荐)
- 在
network_security_config.xml中明确开放特定域名 - 在
AndroidManifest.xml中设置android:usesCleartextTraffic="true"(不推荐--这是全局开关)
<!-- 替代方案:仅允许特定域名使用 HTTP -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">192.168.1.1</domain> <!-- 不支持 CIDR,需指定具体 IP 或域名 -->
</domain-config>
6.4 ❌ 信任所有证书:面试必杀题
// 🚫 以下是高危代码,绝对不要用在生产环境!
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) = Unit
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())
// 很遗憾,这段代码在 Stack Overflow 和 GitHub 上到处都是。
为什么这很危险:
checkServerTrusted 方法什么都不做--这意味任何证书都通过验证。攻击者只要有张合法的证书(哪怕是自签名的),就能在中间人攻击中替换你的服务器证书,而客户端完全不抵抗。
这段代码的唯一合法用途:
- 非常短暂的开发测试(且仅限于 localhost 环境)
- 专门的安全测试场景(渗透测试)
- 永远、永远不要提交到生产代码中
6.5 EventListener 监控 TLS 连接
OkHttp 提供了 EventListener 来监控连接的生命周期,包括 TLS 握手:
class TlsEventListener : EventListener() {
override fun connectionAcquired(call: Call, connection: Connection) {
Log.d("TLS", "连接已建立: ${connection.route().address}")
}
override fun secureConnectStart(call: Call) {
Log.d("TLS", "TLS 握手开始")
}
override fun secureConnectEnd(call: Call, handshake: Handshake?) {
handshake?.let {
val cipherSuite = it.cipherSuite()
val tlsVersion = it.tlsVersion()
val serverCertName = it.peerCertificates()
.firstOrNull()
?.subjectDN?.name
Log.d("TLS", """
TLS 握手完成:
加密套件: $cipherSuite
TLS 版本: $tlsVersion
服务器证书: $serverCertName
""".trimIndent())
}
}
override fun connectFailed(call: Call, inetSocketAddress: InetSocketAddress,
connectException: IOException, protocol: Protocol?) {
Log.e("TLS", "连接失败: ${connectException.message}")
}
}
// 使用
val client = OkHttpClient.Builder()
.eventListener(TlsEventListener())
.build()
这不仅用于调试,线上环境也可以用来监控异常--比如突然检查到大量证书验证失败的请求,可能是 MITM 攻击,也可能是证书快过期了。
6.6 常见证书问题排查
| 问题 | 症状 | 常见原因 |
|---|---|---|
| SSLHandshakeException | Connection refused | 证书过期、域名不匹配、自签名证书未配置 |
| SSLPeerUnverifiedException | Hostname not verified | 证书上的域名和实际访问的域名不一致 |
| CertPathValidatorException | Trust anchor not found | 证书链不完整(缺少中间 CA) |
| SSLException | Connection closed | 服务器不支持客户端使用的 TLS 版本 |
| java.security.cert.CertificateException | Untrusted | 使用了自签名证书但没有配置信任 |
最有用的调试命令:
# 查看服务器证书信息(包括完整证书链)
openssl s_client -connect api.example.com:443 -showcerts -servername api.example.com
# 验证证书链
openssl verify -CAfile ca-chain.pem server-cert.pem
7. 总结
核心要点
| 概念 | 一句话概括 |
|---|---|
| 对称加密 | 快,但密钥需要安全通道传输 |
| 非对称加密 | 解决密钥分发问题,但慢,用于交换会话密钥 |
| TLS 1.2 | 2-RTT 握手,支持多种密码套件 |
| TLS 1.3 | 1-RTT 握手,更少更安全的套件,支持 0-RTT |
| 证书链 | 服务器证书 ← 中间 CA ← 根 CA(系统预装) |
| MITM | 替换服务器证书,需要客户端信任伪造的 CA |
| Certificate Pinning | 额外校验公钥或证书指纹,绕过 CA 体系信任 |
| Network Security Config | Android 7.0+ 的网络安全声明式配置 |
面试高频题速查
- HTTPS 和 HTTP 的根本区别:加密传输 + 身份验证,不只是多了个 s。
- TLS 1.3 为什么比 1.2 快:ClientHello 直接带 key_share,减少一个 RTT;移除不安全算法减少了协商变体。
- 什么是中间人攻击:拦截通信、替换证书,客户端的证书验证通过就中招了。
- Android 7.0 的网络变化:App 默认不信任用户安装的 CA 证书,增强安全性。
- Certificate Pinning 的优缺点:优点:防 CA 误签;缺点:证书轮换时可能造成 App 断连。
- 信任所有证书的 TrustManager 为什么危险:相当于跳过了 TLS 的全部安全机制,任何攻击者都能中间人。
- network_security_config.xml 能做什么:控制证书信任(系统/用户/自定义)、明文流量限制、域名粒度的安全策略。
关于代码:本文中的所有 Kotlin 代码示例均可在 Android API 21+ 环境下运行。
关于证书:文中使用的 SHA-256 指纹为示例值,请替换为你的真实证书指纹。