4. 敏感数据字段的混淆保护策略
4.1 原理:为什么不能混淆金额、订单号、签名串
ProGuard 默认会将类成员(字段、方法)重命名为简短无意义的标识符,如 a、b。对于支付核心代码中的金额、订单号、签名串等字段,混淆后会导致以下问题:
- 序列化/反序列化失败:很多场景使用
Gson、FastJson或Kotlin serialization时,字段名通过反射或命名策略映射,混淆后的名称与通信协议或序列化名称不匹配。 - 签名校验失效:签名串字段名被混淆后,服务端或客户端根据固定字段名生成签名时无法正确提取值。
- 动态访问受阻:如果代码通过反射
field.setAccessible(true)等方式动态读写金额字段,混淆后字段名已变化,导致NoSuchFieldException。 - 日志/监控混淆:调试日志中打印的字段名变为
a/b,难以定位问题。
因此,对这些字段必须使用 Keep 策略——即保留原始字段名称,禁止混淆重命名。
关键原则:混淆不是万能的,支付敏感字段需要保留原名以保证运行时正确性。不要为了“更安全”而错误地让所有字段都混淆。
4.2 保护方式对比:@Keep 注解 vs keep 规则
| 维度 | @Keep 注解 | keep 规则(proguard-rules.pro) |
|---|---|---|
| 适用范围 | 只对标注的 单个字段/类/方法 生效 | 可匹配 包、类、字段名模式,批量生效 |
| 侵入性 | 需要在源码中显式添加注解 | 完全在配置文件中定义,不修改源码 |
| 可维护性 | 注解紧贴字段定义,易于检索 | 所有规则集中在 proguard-rules.pro,适合全局管理 |
| 第三方库兼容 | 无法修改第三方库源码时不可用 | 可对第三方库的字段编写 keep 规则 |
| 推荐场景 | 自己维护的实体类、支付接口层 | 接入的 SDK、数据模型、序列化框架 |
4.3 实现流程与代码/配置示例
4.3.1 流程步骤
- 识别敏感字段:在支付核心代码中,检索所有出现
amount、orderNo、sign、transactionId、merchantId等字段的类。 - 选择保护策略:对自有代码使用
@Keep注解;对第三方依赖或需要通配命中的字段使用 keep 规则。 - 编写配置:在
proguard-rules.pro中添加 -keep 规则,或使用@Keep注解。 - 验证:通过
mapping.txt检查字段是否被重命名。
4.3.2 使用 @Keep 注解(Java / Kotlin)
// Java 示例:支付请求实体
import android.support.annotation.Keep;
public class PaymentRequest {
@Keep
public String orderNo; // 订单号(保留)
@Keep
public double amount; // 金额(保留)
@Keep
public String sign; // 签名串(保留)
public String extraInfo; // 可混淆
}
// Kotlin 示例(需引入 @Keep 注解)
import androidx.annotation.Keep
data class PaymentResponse(
@Keep val transactionId: String, // 保留
@Keep val status: String, // 保留
val rawData: String // 可混淆
)
注意:@Keep 注解在 Android 中由 androidx.annotation:annotation 或 android.support.annotation 提供,且在 ProGuard 中默认会识别。无需额外配置即可生效。
4.3.3 使用 keep 规则(proguard-rules.pro)
# 保留 PaymentRequest 类的所有字段名(含 keep 已在注解中,也可直接写)
-keep class com.pay.sdk.model.PaymentRequest {
*; # 保留该类所有成员
}
# 更精确:只保留特定字段名
-keep class com.pay.sdk.model.PaymentResponse {
public java.lang.String transactionId;
public java.lang.String status;
}
# 保留包下所有类的所有字段(谨慎使用)
-keep class com.pay.sdk.model.** { *; }
规则说明:
-keep class 类名 { 字段签名; }:保留指定字段不被混淆名称。*通配符表示所有成员(字段、方法)都保留原名。- 可以使用
<fields>保留所有字段:-keep class 类名 { <fields>; }。
最佳实践:优先使用
@Keep 注解来精细化控制单个字段,避免通配 * 导致保护过度、增大包体积。但注意 @Keep 只能保护字段名不被混淆,不能阻止代码移除或内联,如需完全保留成员则需结合 -keep。
4.4 调试:验证敏感字段是否被保留
- 执行 Release 构建(通常会自动生成 mapping.txt):
./gradlew assembleRelease - 解压 APK 或查看
build/outputs/mapping/release/mapping.txt。 - 搜索敏感字段名(如
orderNo、amount),如果该字段出现在 mapping.txt 的左侧(原名 → 混淆名映射)中,说明未被保留;如果只在右侧(混淆后名)中出现,或者根本没有出现(表示被删除),则保留失败。
# mapping.txt 示例(正确保留的字段不会出现在这里)
# 如果出现类似:com.pay.sdk.model.PaymentRequest -> com.pay.sdk.model.a:
# java.lang.String amount -> a // 表示 amount 被重命名,错误!
# 应该没有任何 amount 字段的重命名记录,或者记录为:
# java.lang.String amount -> amount // 表示保留原名
可靠的方法:在代码中反射读取 PaymentRequest.class.getDeclaredFields() 并打印名称,在 Release 版本中验证。
常见陷阱:
@Keep 在某些旧的 ProGuard 版本中可能不被识别。确保项目中引入了正确的 androidx.annotation:annotation 依赖,并且 ProGuard 配置文件不排除该注解。如果仍失效,请改用显式 -keep 规则。
4.5 部署注意事项
- 多 flavor 场景:每个 Build Type 或 Product Flavor 都可以有独立的 ProGuard 配置(如
src/release/proguard-rules.pro),务必确认 release 的规则包含了 keep 字段。 - 与资源混淆(AndResGuard)共存:AndResGuard 只处理资源,不影响字段混淆,两者无冲突。
- 签名串字段:如果你的签名串是拼接后的字符串,通常会作为普通字段存在,请务必 keep。如果签名串是通过函数动态计算的,则只需 keep 该函数。
4.6 常见错误及解决
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| 发布后支付签名失败 | 签名串字段 sign 被混淆名覆盖,服务端无法解析 | 在 keep 规则中明确保留 sign 字段 |
| Gson 反序列化得到 null | 金额字段 amount 被重命名,Gson 按 a 找不到映射 | 添加 -keepclassmembers class 类名 { double amount; } |
| 反射报 NoSuchField | 通过字段名字符串反射获取,混淆后名称变化 | 对反射字段使用 -keepclassmembers 并指定原始名称 |
| @Keep 注解不生效 | ProGuard 版本低或未引入 android.support.annotation | 升级 ProGuard 至 6.0+,确保 proguard-android-optimize.txt 未移除 Keep |
4.7 性能与稳定性优化
- 避免过度 Keep:不要对整个支付模块使用
-keep class com.pay.** { *; },这会导致大量代码无法优化,增加包体积(10%~30%)。 - 字段级粒度优先:只对确需保留的字段加上
@Keep或精确规则,其他字段仍可混淆。 - 开启 ProGuard 优化:在
proguard-rules.pro中不要禁用-optimizationpasses 5,让非敏感部分获得压缩收益。 - Kotlin 数据类:数据类的
componentN()方法依赖于字段名,如果字段被混淆可能破坏解构。如需保持解构,应保留所有字段名或使用@Keep注解在类级别。
稳定性建议:在 CI/CD 流程中增加自动化测试——构建 Release 后通过反射检查所有预期保留的字段名是否未被混淆。如果发现字段名被重命名,立即构建失败并提示。