第二篇:用 AI Agent 做全栈鸿蒙应用------AGC 认证 + 云函数一条龙
上一篇44岁被裁后用AI写鸿蒙App(2):Hermes Agent 环境搭建实战被裁后我用 Hermes Agent + D - 掘金
一个人搞定一整套后端,零服务器运维成本。 Hermes Agent 帮我写完了大部分代码。
前言:独立开发者最怕的其实是后端
我做了二十年开发,很清楚一件事------写前端页面不难,难的是后端。
一个 App 要上线,至少需要:
- 用户注册登录(手机号/邮箱验证、会话管理)
- 数据存储(用户信息、家庭关系、加密数据)
- 后端业务逻辑(用户同步、密钥管理、家庭成员绑定)
传统做法:买云服务器 → 搭后端框架 → 写 API → 连数据库 → 部署运维。
两个人干一个月,一个人干......三个月起步。
但我没有三个月。而且我没有预算买服务器。
华为 AGC(AppGallery Connect)就是来解决这个问题的。它提供的服务体系中,我实际用到的是:
| 服务 | 作用 | 费用 |
|---|---|---|
AGC Auth (@hw-agconnect/auth) |
手机号/邮箱/华为账号登录 | 免费 |
Cloud Function (@kit.CloudFoundationKit) |
在华为服务器上跑后端逻辑 | 按调用计费,免费额度够用 |
| CloudDB(云函数服务端) | 数据库由云函数操作,客户端不直连 | 免费额度 |
关键点:我的项目中,客户端不直接操作 CloudDB。所有数据库操作都封装在云函数里,App 通过 cloudFunction.call() 调用。
这样做的原因很实际:安全(数据库操作在服务端不暴露给客户端)、简单(客户端只管调接口,不管 SQL)。
一、AGC Auth:用户登录
登录方式
my-parents-helper 支持三种登录方式:邮箱验证码、手机号验证码、华为账号一键登录。
老人最常用的是手机号验证码------发一条短信,输 6 位数字,完事。不需要记密码。
配置步骤
1. 在 AGC 控制台开启认证服务
登录 AppGallery Connect → 选择应用 → 构建 → 认证服务 → 启用需要的认证方式(手机号、邮箱、华为账号)。
2. 下载 agconnect-services.json
这个文件是 AGC 的身份凭证,包含了 App 的 AGC 配置信息。下载后放到项目的 AppScope/ 目录下。
3. 添加依赖
在 oh-package.json5 中添加:
json
{
"dependencies": {
"@hw-agconnect/auth": "1.0.5"
}
}
然后执行 ohpm install。
4. 初始化 AGC(EntryAbility)
在应用的入口 Ability 中读取配置文件并初始化:
typescript
// EntryAbility.ets
import auth from '@hw-agconnect/auth';
async onCreate(want, launchParam): Promise<void> {
const value = await this.context.resourceManager.getRawFileContent('agconnect-services.json');
const config = JSON.parse(value.toString());
await auth.AGCInitializer.initialize(this.context, config);
}
实际登录代码
以手机号登录为例,流程是:请求验证码 → 用户输入 → 调 signIn 登录。
typescript
// LoginPage.ets(简化)
import auth, { VerifyCodeAction } from '@hw-agconnect/auth';
// 1️⃣ 发送验证码
auth.requestVerifyCode({
verifyCodeType: { kind: 'phone', phoneNumber: '138xxxx', countryCode: '86' },
action: VerifyCodeAction.REGISTER_LOGIN,
lang: 'zh_CN',
sendInterval: 60
}).then(() => { /* 验证码已发送 */ });
// 2️⃣ 用户输入验证码后,调 signIn 登录
const result = await auth.signIn({
autoCreateUser: true,
credentialInfo: {
kind: 'phone',
countryCode: '86',
phoneNumber: '138xxxx',
verifyCode: '123456' // 用户输入的验证码
}
});
const agcUid: string = result.getUser().getUid();
邮箱登录同理,kind 改为 'email',参数相应调整:
typescript
auth.requestVerifyCode({
verifyCodeType: { kind: 'email', email: 'user@example.com' },
action: VerifyCodeAction.REGISTER_LOGIN,
lang: 'zh_CN',
sendInterval: 60
});
const result = await auth.signIn({
autoCreateUser: true,
credentialInfo: {
kind: 'email',
email: 'user@example.com',
verifyCode: '123456'
}
});
登录成功后的关键一步:桥接到云服务
登录成功后,需要把 AGC Auth 的认证凭据桥接到云函数/云存储服务。这一步很多人会漏掉:
typescript
import { cloudCommon } from '@kit.CloudFoundationKit';
import auth from '@hw-agconnect/auth';
let authProvider = auth.getAuthProvider();
cloudCommon.init({
region: cloudCommon.CloudRegion.CHINA,
authProvider: authProvider,
storageOptions: {
mode: request.agent.Mode.BACKGROUND,
network: request.agent.Network.ANY
}
});
不调用 cloudCommon.init,后续的云函数调用可能会因为认证缺失而失败。
会话恢复(无感重登)
App 关闭后重新打开,如果 AGC 会话还没过期,应该让用户无感恢复登录:
typescript
// 检查是否有已有 AGC 会话
const currentUser = await auth.getCurrentUser();
if (currentUser) {
await currentUser.getToken(true);
initCloudCommonAfterLogin();
// 同步用户数据 → 自动跳转
}
可选的隐私增强:生物识别锁
这是针对华为账号登录用户的可选增强功能。开启后,每次打开 App 都要通过人脸或指纹验证才能使用------手机丢了也不怕别人看到家庭信息。
实现方式:使用 @kit.UserAuthenticationKit 的 userAuth 模块。
用户在「设置」中开启生物识别锁后,MainPage 启动时弹出认证窗口:
typescript
import { userAuth } from '@kit.UserAuthenticationKit';
private async requireBiometricAuth(): Promise<boolean> {
const authParam: userAuth.AuthParam = {
challenge: generateLoginChallenge(), // 随机 challenge,防重放
authType: [
userAuth.UserAuthType.PIN,
userAuth.UserAuthType.FINGERPRINT,
userAuth.UserAuthType.FACE
],
authTrustLevel: userAuth.AuthTrustLevel.ATL3,
};
const widgetParam: userAuth.WidgetParam = {
title: '请验证身份以登录'
};
return new Promise((resolve, reject) => {
const instance = userAuth.getUserAuthInstance(authParam, widgetParam);
instance.on('result', {
onResult: (result) => {
instance.off('result');
resolve(result.result === userAuth.UserAuthResultCode.SUCCESS);
}
});
instance.start();
// 30 秒超时
setTimeout(() => { reject(new Error('认证超时')); }, 30000);
});
}
设计原则:这是"可选增强",不是强制。老人如果觉得每次刷脸麻烦,可以关掉。它跟 AGC Auth 的登录是两层独立的认证------AGC 管"你是谁",生物识别管"是不是你本人在用这个手机"。
二、云函数:真正的后端逻辑
架构模式:App → 云函数 → CloudDB
很多独立开发者会有一个误区:以为云数据库 SDK 可以直接在客户端调用。
我的做法是所有数据库操作都走云函数 。客户端只调 cloudFunction.call(),增删改查由云函数在服务端执行。
这样做的原因:
1. 业务逻辑可以随时更新,不需要发版
独立开发者最怕什么?App 已经上架了,突然发现后端逻辑有个 bug,或者想加一个功能。如果你把数据库操作写在客户端代码里:
- 修复 bug → 改客户端代码 → 提审 → 等 1-7 天审核 → 用户更新
- 加个功能 → 同上
如果写在云函数里:
- 修复 bug → 在 AGC 控制台改云函数代码 → 保存 → 立即生效
- 加个功能 → 同上
用户无感知。不需要他们更新 App,不需要经过应用市场审核。对一个人维护的项目来说,这个优势太大了。
2. 敏感数据和业务逻辑不被逆向
这是安全层面的考虑。鸿蒙 App 发布后,hap 包是可以被反编译的。如果你把 CloudDB 的查询语句、加密逻辑、密钥分配规则写在客户端 JavaScript 里,同行拿到你的安装包,拆开一看,什么业务逻辑全暴露了。
但云函数的代码跑在华为的服务器上,客户端只传参数、收结果,中间的业务逻辑黑盒不可见。像家族密钥分配这种核心逻辑,放在云函数里是最基本的安全底线。
3. 参数校验和权限控制不可绕过
客户端发来的请求可以被篡改。如果有人抓包改了请求参数(比如把 role: 'child' 改成 role: 'admin'),直接操作数据库的 App 可能就中招了。
走云函数的话,服务端代码可以做二次校验------确认调用者身份、检查权限、验证参数合法性------这些是客户端代码无法绕过的。
| 对比项 | 客户端直连 CloudDB | 通过云函数(我的方案) |
|---|---|---|
| 更新功能 | ❌ 必须发版+审核 | ✅ 改云函数,立即生效 |
| 代码安全 | ❌ 可被反编译 | ✅ 服务端不可见 |
| 权限控制 | ❌ 依赖客户端传参 | ✅ 服务端二次校验 |
云函数调用封装
先封装一个通用的请求类。这是我从 Hermes 那里拿到的最实用的工具代码之一:
typescript
// commons/network → agc/Request.ets
import { cloudFunction } from '@kit.CloudFoundationKit';
import { connection } from '@kit.NetworkKit';
export class Request {
async call(trigger: string, params?: Object): Promise<Object> {
// 先检查网络
const hasNet = await new Promise<boolean>(resolve => {
connection.hasDefaultNet((_, data) => resolve(!!data));
});
if (!hasNet) throw new Error('网络连接失败');
// 调用云函数
const result = await cloudFunction.call({
name: trigger,
version: '$latest',
timeout: 5000,
data: params
});
return result.result;
}
}
export const request = new Request();
使用方式:
typescript
// 调用云函数 'user-sync'
const res = await request.call('user-sync', {
agcUid: 'xxx',
nickname: '张三',
role: 'child'
});
实际业务示例:用户注册同步
当用户第一次登录成功后,需要做几件事:
- 在 CloudDB 中创建用户档案(user profile)
- 检查是否需要创建家庭
- 返回用户信息和家庭信息
这些逻辑全部在云函数 user-sync 中完成。客户端发起调用:
typescript
// AuthNetFunc.ets
export class AuthNetFunc {
/** 新用户首次登录------服务端在 CloudDB 创建 profile */
syncNewUser(agcUid: string, nickname: string, role: string): Promise<Object> {
return request.call('user-sync', { agcUid, nickname, role });
}
/** 老用户再次登录------服务端返回已有 profile + 家庭信息 */
syncReturningUser(agcUid: string, email?: string): Promise<Object> {
const params: Object = email ? { agcUid, email } : { agcUid };
return request.call('user-sync', params);
}
/** 创建家庭 */
createFamily(creatorId: string, familyName: string): Promise<Object> {
return request.call('family-create', { creatorId, familyName });
}
/** 通过邀请码加入家庭 */
bindFamily(userId: string, inviteCode: string): Promise<Object> {
return request.call('family-bind', { userId, inviteCode });
}
}
云函数返回的数据结构示例:
typescript
interface SyncResponse {
code: number;
message: string;
data?: {
user: { id: string; agcUid: string; nickname: string; role: string; familyId: string };
family: { id: string; name: string; inviteCode: string } | null;
};
}
客户端拿到返回数据后,更新本地状态(AuthModel 的 @Trace 属性),UI 自动响应。
完整的登录-同步-跳转流程
css
LoginPage.ets
│
├─ 用户输入手机号 → 发送验证码
│ auth.requestVerifyCode({...})
│
├─ 用户输验证码 → 调 signIn
│ auth.signIn({credentialInfo: {kind:'phone', ...}})
│
├─ 桥接云服务
│ cloudCommon.init({authProvider, region, ...})
│
├─ 调云函数 user-sync
│ request.call('user-sync', {agcUid})
│
├─ 判断返回
│ ├─ code=0 + data → 已有用户 → 跳转主页面
│ └─ code!=0 → 新用户 → 跳转注册页面
│
└─ 注册/创建家庭后 → 进入主页面
整个流程,客户端不需要知道 CloudDB 的存在。一切 CRUD 都封装在云函数的服务端代码里。
三、端到端加密:数据隐私的最终防线
上面讲的云函数解决了「谁能访问」的问题,但还有一个更底层的问题:数据在服务器上是以什么形式存储的?
华为的服务器确实做了存储加密,但这不够。因为云函数的运维人员、华为内部有权限的人、甚至如果服务器被入侵------理论上都可以看到数据库里的明文内容。
对于 my-parents-helper 这种涉及家庭健康信息、日常习惯、家庭成员关系的 App,这不够安全。
所以我的做法是:在客户端加密数据,再把密文发给云函数存到 CloudDB。服务器只存储密文,永远拿不到明文。
端到端加密的核心原则:加密和解密只在用户设备上进行,服务端不持有解密密钥。
加密架构
整个加密方案分为三层:
scss
用户设备 华为云
│ │
├─ 生成家族密钥 (随机256位) │
├─ 用邀请码派生包装密钥 │
├─ 用包装密钥加密家族密钥 │
├─ 密文 → 云函数 │ → CloudDB 存储
│ │
├─ 再次打开 App │
├─ 从 CloudDB 下载密文 │
├─ 用邀请码重新派生包装密钥 → 解密 │
├─ 得到家族密钥(内存中) │
│ │
├─ 写任务: │
│ 生成临时 DEK (Data Encryption Key)│
│ 用 DEK 加密任务内容 │
│ 用家族密钥加密 DEK │
│ (密文 + 加密后DEK) → 云函数 │ → CloudDB 存储
│ │
├─ 读任务: │
│ 从 CloudDB 下载 (密文 + DEK) │
│ 用家族密钥解密 DEK │
│ 用 DEK 解密任务内容 │
│ 得到明文 → 显示 │
关键角色:
| 概念 | 是什么 | 谁持有 |
|---|---|---|
| 家族密钥 (Family Key) | 256 位随机数,家族成员共享 | 每个家庭成员设备内存中 |
| 包装密钥 (Wrapping Key) | 从邀请码派生 | 临时生成,用完即弃 |
| DEK (Data Encryption Key) | 每条数据独立的加密密钥 | 随密文一起存储(用家族密钥加密后) |
具体实现
使用的是鸿蒙 @kit.CryptoArchitectureKit,AES-256-GCM 模式。
1. 创建家庭时生成家族密钥
typescript
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
// 生成 256 位随机家族密钥 + 盐值
const rand = cryptoFramework.createRandom();
const familyKey = rand.generateRandomSync(32).data; // 256-bit
const salt = rand.generateRandomSync(16).data;
2. 邀请码派生包装密钥,加密家族密钥上传
typescript
// 用邀请码派生包装密钥 (HKDF-SHA256)
const wrappingKey = await FamilyCryptoService.deriveWrappingKey(inviteCode, salt);
// 用包装密钥加密家族密钥 → base64 → 上传 CloudDB
const encryptedFamilyKey = await FamilyCryptoService.wrapFamilyKey(familyKey, wrappingKey);
await request.call('family-key-init', {
familyId: 'xxx',
encryptedFamilyKey: encryptedFamilyKey,
keySalt: salt,
// ...
});
deriveWrappingKey 的具体实现:
typescript
static async deriveWrappingKey(inviteCode: string, salt: Uint8Array): Promise<Uint8Array> {
const keyData = stringToUint8Array(inviteCode);
const spec: cryptoFramework.HKDFSpec = {
algName: 'HKDF',
key: keyData,
salt: salt,
info: new Uint8Array(0),
keySize: 32
};
const kdf = cryptoFramework.createKdf('HKDF|SHA256|EXTRACT_AND_EXPAND');
const secret = await kdf.generateSecret(spec);
return new Uint8Array(secret.data);
}
3. 写入数据时:DEK 加密
每条任务数据都有自己的 DEK(Data Encryption Key),用家族密钥加密后随数据存储:
typescript
// encryptContent 的简化逻辑
static async encryptContent(content: TaskContent): Promise<EncryptedContent> {
const plainJson = JSON.stringify(content);
const rand = cryptoFramework.createRandom();
const dek = rand.generateRandomSync(32).data; // 每个任务独享的 DEK
const ciphertext = await aesGcmEncrypt(plainJson, dek); // 用 DEK 加密内容
const encryptedDek = await aesGcmEncrypt(dek, familyKey); // 用家族密钥加密 DEK
return { contentJson: ciphertext, encryptedDek: encryptedDek };
}
4. 读取数据时:逆序解密
typescript
static async decryptContent(contentJson: string, encryptedDek: string): Promise<TaskContent> {
const dekRaw = await aesGcmDecrypt(encryptedDek, familyKey); // 先解 DEK
const dek = new Uint8Array(dekRaw);
const plainJson = await aesGcmDecryptWithKey(contentJson, dek); // 再用 DEK 解内容
return JSON.parse(plainJson);
}
AES-256-GCM 的具体加解密实现,使用了 cryptoFramework.createCipher('AES256|GCM|PKCS7'),配合随机 IV,每次加密结果不同,防止重放攻击。
完整的加解密代码在
FamilyCryptoService.ets中,约 200 行。上面是核心逻辑,实际还包含了 URL-safe base64 编码、异常处理等细节。
这套方案的几个设计要点
| 设计 | 目的 |
|---|---|
| 每任务独立 DEK | 一个 DEK 泄露只影响一条任务,不波及其他数据 |
| 家族密钥 + DEK 双层 | 更换家族密钥时不需要重新加密所有数据(只需用新密钥重加密 DEK) |
| 邀请码派生包装密钥 | 新成员通过邀请码加入时,可以安全地获取到加密后的家族密钥 |
| HKDF-SHA256 密钥派生 | 即使邀请码强度不够,100000 次 PBKDF2 迭代也大幅增加了暴力破解成本 |
| AES-256-GCM 带认证标签 | 密文被篡改后,解密时会自动校验失败,不会产生乱码数据 |
小结
这套端到端加密让 my-parents-helper 做到了:即使华为的 CloudDB 被拖库,攻击者拿到的也只是一堆无法解密的无意义密文。 每个家庭的加密密钥不同,每条任务的 DEK 不同,解密只发生在用户设备上。
做到这些,依赖的是鸿蒙 @kit.CryptoArchitectureKit 提供的 AES-256-GCM、HKDF、随机数生成等基础能力------这些 API 是系统级的,调用起来比自己造轮子安全得多。
四、Hermes Agent 在整个过程中帮了什么
这一节直接回答很多人会问的问题:「你说用 AI 写代码,具体帮你写了什么?」
1. 登录流程的代码框架
AGC Auth 的接入文档是有的,但步骤分散在好几处:初始化 SDK、请求验证码、处理回调、桥接云服务、会话恢复......
Hermes 帮我梳理出一个清晰的代码结构:
AuthModel:管理登录状态(@Trace isLoggedIn,@Trace agcUid等)AuthNetFunc:封装所有云函数调用LoginPage:UI + 登录逻辑,三种登录方式的 tab 切换- 错误处理:验证码过期重发、网络超时、会话恢复失败等
2. 云函数调用封装
那个 Request 类------网络检查 + 超时 + 重试 + 统一返回------就是 Hermes 帮我写的第一版。我只需要改改参数,就变成了项目通用的调用入口。
3. 端到端加密代码
FamilyCryptoService.ets 的核心结构------DEK 生成、AES-256-GCM 加解密、HKDF 密钥派生------是 Hermes 帮我搭的框架。我重点校验的是密钥管理逻辑(谁持有什么密钥、密钥怎么流转、怎么防泄露)。
4. 生物识别认证
userAuth 模块的事件驱动模型比较绕------on('result') 监听回调 + start() 启动 + 超时处理。Hermes 帮我写成了 Promise 封装,用起来就一行 await requireBiometricAuth()。
5. 状态管理(PersistenceV2 + 会话恢复)
登录状态的持久化和恢复是 Hermes 帮我写的关键部分。细节:
- 登录成功后把
agcUid、userId等信息写入 PersistenceV2 - App 启动时先读 PersistenceV2 缓存,有缓存再尝试恢复 AGC 会话
- 会话恢复成功后自动跳转主页面,用户感觉不到"登录"
- 登出时清除本地缓存 + 调
auth.signOut()
6. 踩坑记录
坑一:cloudCommon.init 必须在 signIn 之后立即调用
登录成功后如果不调 cloudCommon.init,后续云函数调用会报认证错误。这个坑靠 AI 没帮我避开------是跑一遍真机测试发现云函数返回 401,才追查到是少了这一步。
坑二:PersistenceV2 key 不一致
AuthModel 和 SplashPage 如果用了不同的 PersistenceV2 key,登录状态永远读不到。这个坑我已经记在 harmonyos-arkts-rules 技能里了。
五、费用:这套后端花了多少钱
| 服务 | 费用 | 说明 |
|---|---|---|
| AGC Auth | 0 元 | 免费额度对个人项目足够 |
| Cloud Function | 约 0 元 | 个人用量免费额度内 |
| CloudDB | 0 元(云函数调用) | 不直接暴露给客户端 |
| Cryptography API | 0 元 | 系统内置 |
| UserAuth API | 0 元 | 系统内置 |
| 服务器 | 0 元 | 不需要自建 |
| 合计 | 约 0 元 |
真正的"0 元后端"。
下篇预告
下一篇写前端的视觉部分:
第三篇:UIDesignKit UI------光感玻璃 + 流光交互实战
这是 my-parents-helper 的视觉风格------如何用鸿蒙的视觉特效 API 做出「像玻璃一样通透、有流光」的界面。
关注我 seal_jing,一起见证一个 44 岁独立开发者的鸿蒙 App 诞生之路。
发布时间:2026年6月 作者:seal_jing,一个被裁后自己写 App 的中年程序员