8、网络传输安全:HTTPS强制与证书锁定、请求头与Body中敏感信息脱敏、第三方SDK网络行为监控
网络传输安全这块,说白了就是你的App在跟服务器“说话”的时候,不能被别人偷听、篡改或者冒充。我见过太多App,本地数据存得挺严实,结果一联网全裸奔了。这章咱们就聊聊怎么把网络这条通道守好。
8.1 HTTPS强制:别给中间人留机会
先问个问题:你的App还在用HTTP吗?如果答案是肯定的,那基本上等于把用户数据写在明信片上寄出去。任何经过路由器的设备都能看到内容。
我个人习惯是,在AndroidManifest里直接禁止明文流量。从Android 9(API 28)开始,系统默认就禁止了明文HTTP,但很多老项目还在用,或者混着用。我建议你一步到位:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
然后在 res/xml/network_security_config.xml 里这样写:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
嗯,这里要注意:如果你有特殊场景必须用HTTP(比如某些老旧的CDN),可以用 <domain-config> 单独放行,但一定要做白名单,别图省事直接开全局。
android:usesCleartextTraffic="true",上线前忘了删。结果用户数据在公共WiFi下被中间人抓了个精光。所以我的建议是:用 networkSecurityConfig 的方式,别依赖那个全局开关。
8.2 证书锁定:防的就是“合法”的中间人
HTTPS就安全了吗?不一定。你想想看,如果用户的设备里被装了个根证书(比如某些企业监控软件、或者恶意App),那中间人攻击照样能解密你的HTTPS流量。这就是为什么需要证书锁定(Certificate Pinning)。
证书锁定的原理很简单:客户端只信任你指定的那个证书或公钥,其他的一概不认。就算系统信任了某个CA签发的假证书,你的App也不买账。
我推荐用OkHttp的 CertificatePinner 来实现:
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("api.yourdomain.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.yourdomain.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build())
.build();
这里有个坑:证书是会过期的,或者你可能会换证书。如果你只锁定一个证书,一旦换证,所有旧版本App就全挂了。我的做法是:
- 锁定两个公钥:一个当前用的,一个备份的。备份的可以是你未来计划换的证书,或者根CA的公钥。
- 留个后门:在App里内置一个远程配置开关,万一出问题可以临时关闭锁定,通过热修复推送新配置。
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com 2>/dev/null | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
8.3 请求头与Body中的敏感信息脱敏
HTTPS加密了传输通道,但日志呢?抓包工具呢?你想想看,如果开发人员在调试时把完整的请求日志打到了控制台,或者上传到了日志平台,那Token、手机号、身份证号这些信息就等于公开了。
我个人习惯是,在网络层统一做脱敏处理。具体来说:
8.3.1 请求头脱敏
常见的敏感Header包括:Authorization、Cookie、X-Token 等。在打印日志或上报网络数据时,必须把这些字段的值替换掉。
public class SafeHttpLogger implements HttpLoggingInterceptor.Logger {
private static final Set<String> SENSITIVE_HEADERS = new HashSet<>(Arrays.asList(
"Authorization", "Cookie", "Set-Cookie", "X-Token", "X-Session-Id"
));
@Override
public void log(String message) {
// 对包含敏感Header的行进行脱敏
for (String header : SENSITIVE_HEADERS) {
if (message.contains(header + ": ")) {
message = message.replaceAll(
"(?i)(" + header + ":\\s*)(.*)",
"$1******"
);
}
}
Log.d("OkHttp", message);
}
}
8.3.2 Body脱敏
Body里的敏感字段更隐蔽。比如登录接口的密码、注册接口的手机号、支付接口的银行卡号。我的做法是定义一个注解,标记哪些字段需要脱敏:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
SensitiveType type() default SensitiveType.DEFAULT;
enum SensitiveType {
DEFAULT, // 全部掩码
PARTIAL, // 部分掩码,如 138****1234
EMAIL // 邮箱掩码
}
}
// 然后在序列化时统一处理
public class SensitiveSerializer {
public static String toJson(Object obj) {
// 反射遍历字段,对标记了@Sensitive的字段做掩码处理
// 具体实现略...
}
}
8.4 第三方SDK网络行为监控
这是个大问题。你的App里集成了多少第三方SDK?统计、推送、广告、社交分享……每个SDK都可能在你不知情的情况下发起网络请求。GDPR和个人信息保护法都要求,App必须对第三方SDK的数据收集行为负责。
我建议从两个层面来监控:
8.4.1 网络代理层监控
利用OkHttp的 EventListener 或者 NetworkInterceptor,可以监控所有经过OkHttp的请求。但问题是,很多第三方SDK用的是自己的网络库,不走OkHttp。
这时候可以用Android的 ProxySelector 或者 NetworkCapabilities 来做全局监控。更彻底的做法是:
- 抓包分析:在测试阶段,用Charles或Wireshark抓取所有流量,逐个SDK排查它们发了什么请求、传了什么数据。
- VPNService:在App内启动一个本地VPN,拦截所有流量。但这个方法对性能有影响,而且Google Play可能审核不通过。
8.4.2 代码层面的SDK行为审计
我个人习惯是,在集成每个SDK之前,先做一次代码审计:
- 反编译SDK的jar/aar:用jadx或Bytecode Viewer打开,搜索
HttpURLConnection、OkHttp、Socket等关键字。 - 检查URL白名单:看SDK请求了哪些域名,有没有把设备信息、位置信息等敏感数据传出去。
- 查看隐私政策:确认SDK的隐私政策是否跟你App的隐私政策一致。
8.5 本章知识体系
下面这张图总结了网络传输安全的核心要点,你可以对照着检查自己的App:
网络传输安全这件事,说白了就是“防人之心不可无”。你永远不知道用户处在什么样的网络环境下,也不知道第三方SDK会不会“越界”。所以我的建议是:把安全措施做在默认配置里,而不是等出了问题再补。