3、Android 签名机制:APK 签名流程、Keystore 在签名中的作用
签名这玩意儿,说出来你可能不信——它不只是给 APK 盖个章。我早年刚入行时,觉得签名就是点一下「Generate Signed Bundle」完事,直到有一次线上包因为签名配置搞砸,用户升级时直接报安装失败……那场面,我到现在都记得。
说白了,Android 签名是一种数字签名机制。它保证三件事:APK 的来源可追溯、完整性不被破坏、应用身份唯一。你要是把签名搞混了,轻则发布不了,重则覆盖安装都成问题。
今天我们就把签名流程和 Keystore 的角色说透。你理解了这个,后面 CI 里管多环境 Keystore 才顺手。
APK 签名到底签了些什么?
我习惯把 APK 拆成两部分看:文件内容和签名块。签名流程简单讲就是三步:
- 计算摘要——对 APK 内所有受保护文件(DEX、资源、Manifest 等)分别算 SHA256 摘要。
- 打包摘要——把摘要列表放进一个特殊的签名数据结构里。
- 签名字节——用 Keystore 里的私钥对这个摘要结构做一次 RSA/ECDSA 签名,然后把公钥证书和签名结果塞进 APK 的签名块。
为什么要有这一步?你想想看,如果没有签名,任何人都能篡改 APK 里的代码,用户安装时完全无从判断真假。签名就是那道「防伪标签」。
三版签名方案:v1、v2、v3
Android 这些年陆续出了三种签名方案,我在项目中全踩过坑。直接上表格,看得更清楚:
| 方案 | 引入版本 | 签名内容范围 | 特点 |
|---|---|---|---|
| v1 (JAR) | Android 1.0 | 仅签名 META-INF 下的文件 | 兼容最老设备,但可被二次签名,慢 |
| v2 (APK Signature) | Android 7.0 | 整个 APK 的原始字节 | 快速校验,不可篡改,推荐 |
| v3 (Key Rotation) | Android 9.0 | 与 v2 相同,增加证书轮换 | 支持签名密钥替换,向后兼容 |
核心观点:现在打包必须同时开启 v1 + v2(如果需要兼容 Android 7 以下则保留 v1)。我习惯默认只开 v2 + v3,因为 v3 能让你在密钥泄露时换新证书而不丢失用户信任。
说实话,大部分项目根本用不到 v3 的证书轮换,但我还是建议你开。为什么?因为在 CI 里,当生产密钥被严格保管、调试密钥频繁更换时,v3 可以做到「新包替换旧包」而不触发签名验证失败。我见过一个团队,就因为没开 v3,换了一台签名机之后所有已安装用户都收不到覆盖更新……那叫一个惨。
Keystore 到底在里面扮演什么角色?
Keystore 就是一个容器。它里面存着:
- 私钥——不能泄漏的核心资产
- 公钥证书——公开给验证方的东西
- 证书链——用来追溯颁发者的信任链
你可以把 Keystore 理解成是「印鉴盒」。我做签名时,实际上只用两样东西:
- Keystore 文件(.jks / .keystore / .pk12)
- 密钥的别名(alias)和密码
在签名工具(如 apksigner 或 jarsigner)看来,它会这样操作:
# 典型签名命令(Android 签名工具)
apksigner sign \
--ks release.jks \
--ks-key-alias release_key \
--ks-pass pass:your_keystore_password \
--key-pass pass:your_key_password \
--out app-release-signed.apk \
app-unsigned.apk
你看,这些参数里全部指向 Keystore。签名工具拿到 Keystore 后,会用你的别名定位到对应的私钥条目,然后用密码解锁该私钥,做完签名再把公钥证书塞回去。
避坑指南:我曾经直接在 build.gradle 里明文写过 Keystore 密码。结果 Git 提交后忘记 .gitignore,差点把公司生产密钥全暴露了。从那时候起,我改成了环境变量 + CI Secrets 的方式,后面章节会具体说。
生产签名 vs 调试签名
Android Studio 在开发时自动用了一个「debug.keystore」,这个密钥是公开已知的(密码 android,别名 androiddebugkey)。好处是方便,坏处是——任何人用这个密钥签的包都一模一样。如果你在 CI 里也用了默认调试签名,那最终包跟随便在本地 run 出来的没区别,连安装时系统都会认为它们是同一个应用。
我建议你这样划分:
- debug build:用 debug.keystore,完全公开,可多人共用
- release build:用独立的 release.keystore,严格保管
- 内测/预发布:如果有多套环境(比如 staging、beta),就需要独立的 Keystore 别名甚至独立的 Keystore 文件,防止签名一致导致的覆盖冲突
嗯,这里要特别强调:同一应用在不同环境(production / staging)的包名相同但签名不同时,用户无法从 staging 覆盖安装 production 版。在 CI 里,如果你把 staging 和生产用了同一个 Keystore 但不同别名,虽然技术上可行,但管理上容易混淆。我个人习惯:每个环境配独立的 Keystore 文件,文件名里就带环境名,比如 keystore_prod.jks、keystore_staging.jks。
一张图看清 APK 签名全流程
下面这张图我把 APK 从「未签名包」到「可分发包」的路径画出来了,重点标出了 Keystore 在哪个环节发挥作用:
实践提示:上图中的「Keystore」是紫色高亮块,它贯穿了整个签名流程。你可能会问:为什么还要额外标个「安装时验证」? 因为 Android 系统在安装 APK 时,会用包内公钥证书去核实签名块里的签名结果。如果公钥对不上,或者签名被篡改过,系统直接拒绝安装。正是这个机制,让你的 app 不会被别人伪造。
关于 Keystore 的格式和命名
我经常看到有人把 .jks 和 .keystore 混用。其实本质上它俩都是 Java 的 KeyStore 格式,JKS 是旧版本,PKCS12 (.p12) 是新标准。我建议一律用 PKCS12,因为更安全、跨平台兼容性更好。转换命令也很简单:
keytool -importkeystore \
-srckeystore old.jks \
-destkeystore new.p12 \
-deststoretype PKCS12 \
-srcalias my_alias \
-destalias my_alias
另外,文件命名一定要带环境标识——这不仅是为了人眼识别,更是为了 CI 脚本里能精确匹配。我的习惯是:
keystore_prod.p12keystore_beta.p12keystore_staging.p12
每个文件里只放一个别名,别名与文件名保持一致(比如 prod / beta / staging)。这样即使后面 CI 自动化管理,也不会搞混。
⚠️ 重要警告:Keystore 文件一旦丢失或密码遗忘,你发布的 app 将永远无法更新——因为新包签名不同,系统不让覆盖安装。生产 Keystore 务必做备份,并存放在离线安全位置。我见过有团队把 Keystore 放在共享网盘上,结果同事离职时误删了,全组连着加班两天才找回来……
好了,现在你应该对签名流程和 Keystore 的角色有了清晰认知。接下来 CI 中如何管理多套 Keystore、怎么把它们安全地注入构建流水线,就是后面章节的事。我们继续。