3、Android 签名机制:APK 签名流程、Keystore 在签名中的作用

签名这玩意儿,说出来你可能不信——它不只是给 APK 盖个章。我早年刚入行时,觉得签名就是点一下「Generate Signed Bundle」完事,直到有一次线上包因为签名配置搞砸,用户升级时直接报安装失败……那场面,我到现在都记得。

说白了,Android 签名是一种数字签名机制。它保证三件事:APK 的来源可追溯、完整性不被破坏、应用身份唯一。你要是把签名搞混了,轻则发布不了,重则覆盖安装都成问题。

今天我们就把签名流程和 Keystore 的角色说透。你理解了这个,后面 CI 里管多环境 Keystore 才顺手。

APK 签名到底签了些什么?

我习惯把 APK 拆成两部分看:文件内容和签名块。签名流程简单讲就是三步:

  1. 计算摘要——对 APK 内所有受保护文件(DEX、资源、Manifest 等)分别算 SHA256 摘要。
  2. 打包摘要——把摘要列表放进一个特殊的签名数据结构里。
  3. 签名字节——用 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 在哪个环节发挥作用:

APK 签名流程 & Keystore 作用 开发者 (提供代码 & 配置) 未签名 APK (apk 原始文件) 签名工具 (apksigner/jarsigner) 已签名 APK (含签名块) Keystore 私钥 + 证书 + 别名 读取私钥 安装时验证 (完整性 & 身份) 分发 验证失败 → 安装失败 (签名不匹配) 🔑 Keystore 是签名过程的核心资产:它提供私钥用于签名,也携带证书供安装时校验 不同环境使用不同 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.p12
  • keystore_beta.p12
  • keystore_staging.p12

每个文件里只放一个别名,别名与文件名保持一致(比如 prod / beta / staging)。这样即使后面 CI 自动化管理,也不会搞混。

⚠️ 重要警告:Keystore 文件一旦丢失或密码遗忘,你发布的 app 将永远无法更新——因为新包签名不同,系统不让覆盖安装。生产 Keystore 务必做备份,并存放在离线安全位置。我见过有团队把 Keystore 放在共享网盘上,结果同事离职时误删了,全组连着加班两天才找回来……

好了,现在你应该对签名流程和 Keystore 的角色有了清晰认知。接下来 CI 中如何管理多套 Keystore、怎么把它们安全地注入构建流水线,就是后面章节的事。我们继续。