开篇:签名是上架的第一道门
鸿蒙应用的安装与分发完全建立在签名之上:系统只安装签名合法且 Profile 授权的应用,应用市场只接受用发布证书签名的包。很多开发者在功能开发完成后卡在最后一步,反复被"签名校验失败"“应用未授权"这类错误拦住,根本原因是没搞清签名链路上四个文件的职责。
本文按"准备证书、配置签名、打包产物、提交审核"的顺序把这条链路走一遍。如果你还没建好工程,建议先读 HarmonyOS NEXT 全景与开发环境搭建 。
一、签名体系四件套
鸿蒙的签名体系由四个文件构成,它们存在明确的生成依赖关系,顺序不能颠倒。
| 文件 | 扩展名 | 作用 | 生成方 |
|---|---|---|---|
| 密钥库 | .p12 | 保存私钥,签名时使用 | 开发者本地生成 |
| 证书请求 | .csr | 承载公钥与主体信息 | 由密钥库导出 |
| 数字证书 | .cer | 由 CA 签发,证明公钥归属 | AppGallery Connect 签发 |
| Profile | .p7b | 描述应用权限与设备白名单 | AppGallery Connect 生成 |
生成顺序是:先在本地用密钥库工具生成 .p12 与 .csr,把 .csr 上传到 AGC 换取 .cer,再在 AGC 上基于该证书创建 .p7b。四者缺一不可,且必须互相对应:用 A 证书签名的包配 B 证书生成的 Profile,一定会校验失败。
.p7b 是唯一与"这台设备能不能装"直接相关的文件。调试用的 Profile 里写死了允许安装的设备 UDID 列表,漏加设备就会导致安装时报"应用未授权”。
1.1 证书链与签名算法
签名算法在配置里写成 SHA256withECDSA,即基于椭圆曲线的 ECDSA 签名配合 SHA256 摘要。系统在安装校验时会沿着"包签名到数字证书到根证书"的链条逐级验证,任何一环断裂都会失败。
这解释了一个常见现象:只替换 .cer 而不替换 .p7b,安装必然失败。因为 .p7b 内部记录了签发它的证书信息,证书换了,Profile 里记录的指纹就对不上了。
1.2 密钥库不是备份文件
.p12 密钥库一旦丢失,对应的证书就无法再用于签名,线上应用将无法发布新版本。因此密钥库与密码必须纳入团队的密码管理流程,而不是只存在某个人的笔记本里。这一点和任何代码签名体系的要求一致。
二、调试签名与发布签名
两种签名的用途完全不同,混用是常见错误。
| 维度 | 调试签名 | 发布签名 |
|---|---|---|
| 用途 | 真机调试 | 提交应用市场 |
| Profile 类型 | 调试 Profile | 发布 Profile |
| 设备限制 | 仅白名单设备 | 无限制 |
| 证书类型 | 调试证书 | 发布证书 |
| 有效期 | 通常较短 | 较长 |
| 能否上架 | 否 | 是 |
调试证书与发布证书在 AGC 上是两个独立的入口,不能互相替代。用调试证书打出的包上传到 AGC 会被直接拒绝。
2.1 为什么需要两套证书
根因在于设备授权模型。调试 Profile 里包含允许安装的设备 UDID 白名单,这个白名单只对开发调试有意义;发布 Profile 不限制设备,但它要求证书是发布证书。两套 Profile 的结构不同,系统据此区分包的用途。
工程上的建议是把两套签名配置同时写进 build-profile.json5,通过 products 切换,避免每次打包时手改配置。
{
"app": {
"signingConfigs": [
{ "name": "debug", "type": "HarmonyOS", "material": { "profile": "./signature/debug.p7b" } },
{ "name": "release", "type": "HarmonyOS", "material": { "profile": "./signature/release.p7b" } }
],
"products": [
{ "name": "default", "signingConfig": "debug" },
{ "name": "release", "signingConfig": "release" }
]
}
}
注意上面为简洁省略了 material 中的其余字段,实际配置必须补全 certpath、storeFile、keyAlias、storePassword、keyPassword、signAlg。
三、AppGallery Connect 侧准备
3.1 实名认证与创建应用
AGC 的第一步是完成开发者实名认证,个人与企业认证所需材料不同。认证通过后才能创建项目与应用。
创建应用时最关键的一个字段是包名(bundleName),它必须与工程 app.json5 中的 bundleName 完全一致,且全局唯一。一旦应用创建成功,包名不可修改。因此建议在动手写代码前就把包名定好,格式通常是反向域名,例如 com.example.todoapp。
3.2 注册调试设备
真机调试前必须把设备 UDID 加到 AGC 的设备列表里。
获取 UDID 的方式有两种:一是在 DevEco Studio 的设备管理界面直接复制,二是通过 hdc 命令读取。
hdc list targets # 列出已连接设备
hdc shell bm get --udid # 获取指定设备的 UDID
拿到 UDID 后在 AGC 的"设备管理"里添加,再重新生成或更新调试 Profile,把设备包含进去。改完 Profile 必须重新下载并替换工程里的 .p7b 文件,否则本地的 Profile 仍是旧的。
3.3 项目与应用的层级关系
AGC 的组织模型是"项目包含应用":一个项目下可以挂多个应用,证书与 Profile 都创建在应用层级。团队协作时最常见的混乱是把证书创建在了另一个项目下,导致 Profile 无法关联到目标应用。
判断方法很简单:在 AGC 的应用详情页能看到的证书列表,才是该应用可用的证书。跨项目复用证书在鸿蒙的签名体系里是不成立的。
四、DevEco Studio 中配置签名
4.1 自动签名
DevEco Studio 提供一键自动签名:在 File 菜单进入 Project Structure,选择 Signing Configs,勾选自动签名即可。IDE 会自动完成密钥库生成、证书申请与 Profile 下载的全过程。
自动签名的优点是零门槛,缺点是生成的证书与 Profile 绑定当前账号与当前设备,换机器或换账号时需要重新生成,不适合团队协作。
4.2 手动签名
团队协作推荐手动签名:由一个人统一生成 .p12 与 .csr,在 AGC 换取 .cer 与 .p7b,再把三个文件(.p12、.cer、.p7b)连同密码分发给团队成员,各自在本地配置。
这样做的另一个好处是发布包与调试包可以共用同一套证书,避免"本地能跑、打包上架失败"的割裂。
4.3 build-profile.json5 配置片段
签名信息写在工程根目录的 build-profile.json5 中。
{
"app": {
"signingConfigs": [
{
"name": "release",
"type": "HarmonyOS",
"material": {
"certpath": "./signature/release.cer",
"storePassword": "0000001B0A2C3D4E5F",
"keyAlias": "releaseKey",
"keyPassword": "0000001B0A2C3D4E5F",
"profile": "./signature/release.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "./signature/release.p12"
}
}
],
"products": [
{
"name": "default",
"signingConfig": "release",
"compatibleSdkVersion": "5.0.0(12)",
"runtimeOS": "HarmonyOS"
}
]
}
}
三个密码字段(storePassword、keyPassword)在 IDE 里会以加密串形式保存,手动填写时若直接写明文,IDE 会在下次打开时重新加密,容易造成配置被覆盖。建议始终通过 IDE 的签名界面填写,而不是手改文件。
五、打包命令与产物
5.1 hvigor 命令
鸿蒙的构建工具是 hvigor,命令行入口是工程根目录下的 hvigorw。
./hvigorw assembleHap --mode module -p product=default # 构建调试 HAP
./hvigorw assembleApp --mode project -p product=default -p buildMode=release # 构建发布 APP 包
./hvigorw clean # 清理构建产物
buildMode=release 会开启代码混淆与资源压缩,这也是发布包体积明显小于调试包的原因。CI 环境中通常需要配合 --no-daemon 关闭常驻进程。
5.2 产物路径
| 产物 | 路径 | 用途 |
|---|---|---|
| 调试 HAP | entry/build/default/outputs/default/entry-default-unsigned.hap | 本地安装验证 |
| 签名 HAP | entry/build/default/outputs/default/entry-default-signed.hap | 真机调试 |
| 发布 APP | build/outputs/default/*.app | 提交 AGC |
上架时提交的是 .app 文件而不是 .hap。.app 是包含多个 HAP 与资源索引的聚合包,.hap 是单个模块的安装包。用错了文件类型,AGC 会提示格式不支持。
5.3 CI 中的自动打包
在持续集成环境中,打包命令需要加上 --no-daemon 避免守护进程驻留,签名材料则通过环境变量或密钥管理服务注入,绝不提交到代码仓库。
#!/bin/bash
set -euo pipefail
mkdir -p signature
echo "$SIGN_STORE_FILE" | base64 -d > signature/release.p12
echo "$SIGN_CERT" | base64 -d > signature/release.cer
echo "$SIGN_PROFILE" | base64 -d > signature/release.p7b
./hvigorw clean --no-daemon
./hvigorw assembleApp --mode project \
-p product=release \
-p buildMode=release \
--no-daemon
ls -lh build/outputs/default/
脚本分三步:先把环境变量里的三份签名材料还原到本地目录,再清理并构建发布包,最后列出产物供后续上传。
CI 中另一个容易出问题的地方是 versionCode。建议由流水线根据构建序号自动写入,避免人工修改 app.json5 造成冲突。
5.4 打包报错速查
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 提示签名校验失败 | 证书与 Profile 不匹配 | 用同一套材料重新生成 Profile |
| 安装报应用未授权 | 设备 UDID 不在白名单 | 添加设备并更新 Profile |
| 密钥库打开失败 | 密码或别名错误 | 核对 keyAlias 与两个密码 |
| 找不到签名配置 | products 引用的名字拼错 | 检查 signingConfigs 与 products 的 name |
| 构建卡住无输出 | hvigor 守护进程异常 | 追加 –no-daemon 重试 |
| 上传提示格式不支持 | 提交了 .hap 而非 .app | 改用 assembleApp 的产物 |
这张表覆盖了绝大多数"打包到一半失败"的场景。排查顺序建议是:先确认包名与 AGC 一致,再确认证书与 Profile 成对,最后确认构建命令与产物类型。
六、混淆与资源压缩
release 构建会启用混淆,但混淆强度需要显式配置。配置写在模块级 build-profile.json5 的 buildOption.arkOptions 中。
{
"buildOption": {
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": ["./obfuscation-rules.txt"]
},
"consumerFiles": ["./consumer-rules.txt"]
}
}
}
}
obfuscation-rules.txt 里可以开启更激进的选项。
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
| 混淆选项 | 作用 | 主要风险 |
|---|---|---|
| enable-property-obfuscation | 混淆属性名 | 序列化与反射失效 |
| enable-toplevel-obfuscation | 混淆顶层名 | 动态引用失效 |
| enable-filename-obfuscation | 混淆文件名 | 动态 import 失效 |
| enable-export-obfuscation | 混淆导出名 | 跨模块引用失效 |
开启属性混淆后,凡依赖属性名的场景都会出问题,必须用 -keep-property-name 把需要保留的名字列进白名单。更关键的是,混淆后崩溃栈里的符号会变成短名,必须把构建产物中的 sourcemap 上传到 AGC,后台才能把堆栈还原成可读的函数名。这一步经常被遗漏,导致线上崩溃只能看到一堆无意义的字母。
七、版本号管理
鸿蒙应用有两个版本字段,含义完全不同。
| 字段 | 类型 | 是否用户可见 | 规则 |
|---|---|---|---|
| versionCode | 整数 | 否 | 每次提交必须严格递增 |
| versionName | 字符串 | 是 | 展示给用户,如 1.0.0 |
versionCode 在 app.json5 中配置。AGC 在接收新包时会校验它是否大于线上版本,不递增会被直接驳回,这是最容易被忽略的规则。建议在 CI 中用构建号自动生成 versionCode,避免人工遗忘。
上架前可以用一段代码自检包名与版本号是否与预期一致,避免打包后才发现 bundleName 写错。
import { bundleManager, common } from '@kit.AbilityKit';
export async function readBundleInfo(context: common.UIAbilityContext): Promise<string> {
const flag = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
const info: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(flag);
return `${info.name} ${info.versionName} ${info.versionCode}`;
}
把这段逻辑挂在一个仅调试版本可见的入口里,打包前运行一次,就能确认包名、版本名与版本号三项都符合预期。相比在 AGC 提交后才被驳回,这个自检的成本几乎为零。
BUILD_NUMBER=${CI_PIPELINE_ID:-1}
python3 - <<PY
import json, re
path = 'AppScope/app.json5'
text = open(path, encoding='utf-8').read()
text = re.sub(r'"versionCode":\s*\d+', f'"versionCode": ${BUILD_NUMBER}', text)
open(path, 'w', encoding='utf-8').write(text)
PY
把版本号交给流水线还有一个隐性收益:每次构建的 versionCode 都与流水线记录一一对应,出问题时能立刻定位到是哪次构建产出的包。
八、上架流程
| 阶段 | 主要动作 | 产出 |
|---|---|---|
| 准备 | 实名认证、创建应用、确认包名 | AGC 应用记录 |
| 打包 | 发布证书签名、release 构建 | 签名后的 .app |
| 填写信息 | 应用介绍、截图、图标、分类 | 商品信息 |
| 合规材料 | 隐私政策、软著、权限说明 | 审核附件 |
| 提交审核 | 上传包并提交 | 审核任务 |
| 测试发布 | 邀请测试或公开测试 | 测试版本 |
| 正式发布 | 审核通过后上架 | 线上版本 |
隐私政策是审核的重点,必须明确列出应用收集了哪些数据、用途是什么、如何删除。缺失或与实际行为不符都会导致驳回。软著(软件著作权)对部分类目是硬性要求,建议提前准备,办理周期通常以周计。
8.1 上架材料清单
| 材料 | 是否必需 | 说明 |
|---|---|---|
| 应用图标 | 必需 | 需要多档尺寸 |
| 应用截图 | 必需 | 至少三张,覆盖主要界面 |
| 应用介绍 | 必需 | 有字数上限 |
| 隐私政策 | 必需 | 需提供可公开访问的链接 |
| 软件著作权 | 视类目 | 部分类目为硬性要求 |
| 权限使用说明 | 必需 | 逐条对应实际申请的权限 |
| 测试账号 | 视情况 | 需登录的应用必须提供 |
8.2 测试渠道与灰度发布
AGC 提供邀请测试与公开测试两种测试渠道。邀请测试需要手动添加测试账号,适合小范围验证;公开测试有名额上限,适合放量前的压力验证。
测试版本与正式版本的 versionCode 必须不同,且测试版本不能直接转为正式版本,需要以正式包重新提交审核。因此不要把测试渠道当作"跳过审核的捷径",它只是把验证提前了一步。
九、常见驳回原因
bundleName与 AGC 应用记录不一致,签名校验失败。versionCode未递增或重复。- 隐私政策缺失,或未覆盖实际申请的权限。
- 使用了需要资质但未提供证明的类目。
- 应用内存在未声明的第三方 SDK 数据收集行为。
- 启动即崩溃、白屏或长时间无响应。
- 截图与实际界面不符。
- 发布包中残留调试日志或测试入口。
- 软著或商标材料缺失。
- 元服务包体积超出限制。
- 测试渠道的包与正式包用了同一个
versionCode,提交正式版时被判定为重复。 - 隐私政策链接不可公开访问,审核方打不开。
十、常见坑清单
- 用调试证书打发布包,AGC 直接拒绝。
- Profile 与证书不匹配,安装时报"应用未授权"。
- 添加了调试设备 UDID 但没重新下载 Profile。
- 密码或密钥别名填错,签名时提示密钥库打开失败。
- 手改
build-profile.json5的密码字段,被 IDE 重新加密后失效。 signingConfigs的name与products中引用的名字不一致。- 提交
.hap而非.app。 - release 构建未开混淆,包体积超标。
- CI 中未关闭 hvigor 守护进程,导致构建卡住。
- 换机器后仍用旧密钥库,签名结果与线上包不一致,无法覆盖安装。
签名与包体积、启动性能往往同时出问题,因为发布构建会开启混淆与压缩,可能暴露出调试构建中隐藏的缺陷。上线前建议用发布包完整跑一遍性能检查,方法见 鸿蒙原生应用性能优化与调试 。如果应用包含登录能力,账号体系与隐私政策的对应关系还需要额外核对,可参考 小程序登录与鉴权 里对授权边界的讨论。
小结
上架的难点几乎全部集中在签名链路上:四个文件必须互相对应,调试与发布两套证书不能混用,包名一旦确定不可更改。工程上的三条底线是:包名提前定死并与 AGC 保持一致,versionCode 交给 CI 自动递增,团队协作统一用手动签名分发证书。把这三件事做对,剩下的就是按流程填材料、等审核。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。