《HarmonyOS技术精讲-Basic Services Kit》账号管理:应用内账号创建与管理

HarmonyOS 技术精讲 - Basic Services Kit(基础服务):应用内账号创建与管理

账号管理这个 API,很多人第一步就走错了

HarmonyOS NEXT 开发里,appAccountManager 这个 API 经常被误用。我第一次接触它的时候,也踩了个坑------官方示例跑通了,但一放到多账号切换的场景里,数据就读不到了。

问题出在哪里?很多人以为 createAccount 只是简单的本地存储,但实际上它涉及系统级别的账号生命周期管理。如果你不了解它和 Preferences数据库 的本质区别,很容易在后续维护中遇到数据不一致的问题。

这篇文章不废话,直接做一个完整的本地账号系统,支持多账号创建、密码保存、账号切换。代码全部可运行,踩坑点全部标注出来。

它解决什么问题

应用内账号管理(Account Kit)是 Basic Services Kit 提供的本地账号能力,专门用于管理应用自身的用户体系。

适合的场景:

  • 应用有自己的账号系统,不需要接入华为账号
  • 需要支持多账号切换
  • 账号信息需要持久化,且与系统账号隔离
  • 需要在账号维度存储自定义属性(比如密码、token、配置)

不适合的场景:

  • 只需要简单存储少量数据 → 用 Preferences 更轻量
  • 需要云同步 → 需要配合云服务
  • 需要第三方登录 → 需要接入 Auth Kit 或单独处理

和数据库 / Preferences 的区别:

方案 存储范围 生命周期 多账号管理 系统级清理
Preferences 应用级别 随应用卸载 需要自己实现 不支持
数据库 应用级别 随应用卸载 需要自己实现 不支持
Account Kit 系统账号服务 随应用卸载 原生支持 支持

Account Kit 最大的价值是:系统级的多账号生命周期管理。当应用卸载时,账号数据自动清理;不需要自己维护账号列表的增删改查逻辑。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机

核心实现:完整的本地账号系统

整体架构设计

我们不搞复杂的 MVVM,新手阶段先把功能跑通。整个模块分三层:

复制代码
UI 层(登录页面) → 逻辑层(AccountService) → API 层(appAccountManager)

AccountService 封装所有账号操作,UI 层只调用它提供的方法。

第一步:配置模块

module.json5 中声明权限(实际上 Account Kit 不需要额外权限声明,但需要确保 Basic Services Kit 已集成)。

json 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:entryability_desc",
        "icon": "$media:icon",
        "label": "$string:app_name",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$string:entryability_background",
        "exported": true
      }
    ],
    "requestPermissions": []
  }
}

不需要额外权限,这一点比很多 API 省心。

第二步:创建 AccountService 服务类

这个文件是核心,所有账号操作都在这里。

typescript 复制代码
// service/AccountService.ets
import { appAccountManager } from '@kit.BasicServicesKit';
import { BusinessError } from '@kit.BasicServicesKit';

export class AccountService {
  private static instance: AccountService;
  // 当前登录的账号
  private currentAccount: appAccountManager.AppAccount | null = null;
  // 账号列表缓存
  private accountList: appAccountManager.AppAccount[] = [];

  private constructor() {
    // 单例模式,确保账号状态一致
  }

  public static getInstance(): AccountService {
    if (AccountService.instance === null) {
      AccountService.instance = new AccountService();
    }
    return AccountService.instance;
  }

  /**
   * 创建应用内账号
   * @param username 用户名
   * @param password 密码
   * @returns true 表示创建成功
   */
  public async createAccount(username: string, password: string): Promise<boolean> {
    try {
      // 1. 创建账号
      await appAccountManager.createAccount(username);
      console.info(`Account created: ${username}`);

      // 2. 获取账号对象
      const account = await appAccountManager.getAccount(username);

      // 3. 保存密码到账号自定义属性
      // 这里使用 setCredential 来存储密码(实际项目建议加密)
      await account.setCredential('password', password);
      console.info(`Credential saved for: ${username}`);

      // 4. 保存一个自定义属性,标记账号状态
      await account.setCustomData('createTime', new Date().toISOString());

      return true;
    } catch (error) {
      let err = error as BusinessError;
      // 错误码 12300001 表示账号已存在
      if (err.code === 12300001) {
        console.error(`Account already exists: ${username}`);
        return false;
      }
      console.error(`Create account failed: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 删除账号
   * @param username 用户名
   */
  public async removeAccount(username: string): Promise<boolean> {
    try {
      await appAccountManager.removeAccount(username);
      console.info(`Account removed: ${username}`);

      // 如果删除的是当前账号,清除缓存
      if (this.currentAccount?.getName() === username) {
        this.currentAccount = null;
      }
      return true;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Remove account failed: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 获取所有应用内账号
   */
  public async getAllAccounts(): Promise<string[]> {
    try {
      const accounts = await appAccountManager.getAllAccounts();
      this.accountList = accounts;
      return accounts.map(account => account.getName());
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Get accounts failed: ${JSON.stringify(err)}`);
      return [];
    }
  }

  /**
   * 验证账号密码
   * @param username 用户名
   * @param password 密码
   */
  public async verifyAccount(username: string, password: string): Promise<boolean> {
    try {
      const account = await appAccountManager.getAccount(username);
      const storedPassword = await account.getCredential('password');
      return storedPassword === password;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Verify account failed: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 设置当前登录账号
   */
  public setCurrentAccount(account: appAccountManager.AppAccount | null): void {
    this.currentAccount = account;
  }

  /**
   * 获取当前登录账号
   */
  public getCurrentAccount(): appAccountManager.AppAccount | null {
    return this.currentAccount;
  }

  /**
   * 获取当前用户名
   */
  public getCurrentUsername(): string {
    return this.currentAccount?.getName() ?? '';
  }

  /**
   * 获取账号的自定义数据
   * @param username 用户名
   * @param key 键
   */
  public async getAccountCustomData(username: string, key: string): Promise<string> {
    try {
      const account = await appAccountManager.getAccount(username);
      return await account.getCustomData(key);
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Get custom data failed: ${JSON.stringify(err)}`);
      return '';
    }
  }

  /**
   * 更新密码
   * @param username 用户名
   * @param newPassword 新密码
   */
  public async updatePassword(username: string, newPassword: string): Promise<boolean> {
    try {
      const account = await appAccountManager.getAccount(username);
      await account.setCredential('password', newPassword);
      return true;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`Update password failed: ${JSON.stringify(err)}`);
      return false;
    }
  }
}

这段代码需要注意的地方:

  1. createAccountsetCredential 是分开的两个异步调用,中间没有事务保证。如果 setCredential 失败,账号已创建但密码没保存------这是真实开发中容易遇到的问题。后面踩坑章节会详细讲。
  2. getAllAccounts 返回的是 AppAccount 数组,不是用户名列表。它的内部实现会遍历当前应用的所有账号。
  3. getCredentialsetCredential 存储的是明文数据。实际项目里,密码一定不能明文存。后面会给出加密建议。

第三步:构建登录页面 UI

登录页面包含三个功能区域:登录 / 注册、账号列表、账号管理。

typescript 复制代码
// pages/LoginPage.ets
import { AccountService } from '../service/AccountService';

@Entry
@Component
struct LoginPage {
  @State username: string = '';
  @State password: string = '';
  @State confirmPassword: string = '';
  @State isRegisterMode: boolean = false;
  @State accountList: string[] = [];
  @State currentUser: string = '';
  @State message: string = '';
  @State loginSuccess: boolean = false;

  private accountService: AccountService = AccountService.getInstance();

  build() {
    Column() {
      // 标题
      Text('应用内账号管理系统')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 });

      if (!this.loginSuccess) {
        // 登录 / 注册表单
        this.buildLoginForm();
      } else {
        // 已登录状态
        this.buildAccountList();
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
    .onPageShow(() => {
      this.refreshAccountList();
    });
  }

  /**
   * 登录 / 注册表单区域
   */
  @Builder
  buildLoginForm() {
    Column() {
      Text(this.isRegisterMode ? '注册新账号' : '登录')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 16 });

      TextInput({ placeholder: '请输入用户名', text: this.username })
        .width('80%')
        .height(48)
        .margin({ bottom: 12 })
        .onChange((value: string) => {
          this.username = value;
        });

      TextInput({ placeholder: '请输入密码', text: this.password })
        .width('80%')
        .height(48)
        .margin({ bottom: 12 })
        .type(InputType.Password)
        .onChange((value: string) => {
          this.password = value;
        });

      // 注册模式下显示确认密码
      if (this.isRegisterMode) {
        TextInput({ placeholder: '确认密码', text: this.confirmPassword })
          .width('80%')
          .height(48)
          .margin({ bottom: 12 })
          .type(InputType.Password)
          .onChange((value: string) => {
            this.confirmPassword = value;
          });
      }

      // 提示消息
      if (this.message !== '') {
        Text(this.message)
          .fontSize(14)
          .fontColor(Color.Red)
          .margin({ bottom: 8 });
      }

      // 主按钮
      Button(this.isRegisterMode ? '注册' : '登录')
        .width('80%')
        .height(44)
        .margin({ bottom: 12 })
        .onClick(() => {
          if (this.isRegisterMode) {
            this.handleRegister();
          } else {
            this.handleLogin();
          }
        });

      // 切换模式
      Text(this.isRegisterMode ? '已有账号?去登录' : '没有账号?去注册')
        .fontSize(14)
        .fontColor(Color.Blue)
        .onClick(() => {
          this.isRegisterMode = !this.isRegisterMode;
          this.message = '';
        });
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center);
  }

  /**
   * 已登录后的账号管理区域
   */
  @Builder
  buildAccountList() {
    Column() {
      Text(`当前用户:${this.currentUser}`)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 16 });

      // 账号列表
      Text('所有账号:')
        .fontSize(16)
        .width('80%')
        .textAlign(TextAlign.Start)
        .margin({ bottom: 8 });

      List({ space: 8 }) {
        ForEach(this.accountList, (item: string) => {
          ListItem() {
            Row() {
              Text(item)
                .fontSize(16)
                .layoutWeight(1);

              if (item === this.currentUser) {
                Text('当前')
                  .fontSize(12)
                  .fontColor(Color.Gray)
                  .margin({ right: 8 });
              }

              Button('切换')
                .fontSize(14)
                .height(32)
                .margin({ right: 4 })
                .onClick(() => {
                  this.switchAccount(item);
                });

              Button('删除')
                .fontSize(14)
                .height(32)
                .fontColor(Color.Red)
                .onClick(() => {
                  this.handleDeleteAccount(item);
                });
            }
            .padding(12)
            .width('80%')
            .borderRadius(8)
            .backgroundColor('#f5f5f5');
          }
        }, (item: string) => item);
      }
      .width('100%')
      .layoutWeight(1);

      // 退出登录
      Button('退出当前账号')
        .width('80%')
        .height(44)
        .fontColor(Color.Red)
        .margin({ top: 12 })
        .onClick(() => {
          this.logout();
        });
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center);
  }

  /**
   * 处理注册逻辑
   */
  async handleRegister() {
    if (this.username === '' || this.password === '') {
      this.message = '用户名和密码不能为空';
      return;
    }
    if (this.password !== this.confirmPassword) {
      this.message = '两次密码不一致';
      return;
    }
    if (this.username.length < 2) {
      this.message = '用户名至少2个字符';
      return;
    }

    this.message = '正在注册...';
    let result = await this.accountService.createAccount(this.username, this.password);

    if (result) {
      this.message = '注册成功,请登录';
      this.isRegisterMode = false;
      this.password = '';
      this.confirmPassword = '';
    } else {
      this.message = '注册失败,用户名可能已存在';
    }
  }

  /**
   * 处理登录逻辑
   */
  async handleLogin() {
    if (this.username === '' || this.password === '') {
      this.message = '用户名和密码不能为空';
      return;
    }

    this.message = '正在验证...';
    let isValid = await this.accountService.verifyAccount(this.username, this.password);

    if (isValid) {
      this.currentUser = this.username;
      this.loginSuccess = true;
      this.message = '';
      // 刷新账号列表
      await this.refreshAccountList();
    } else {
      this.message = '用户名或密码错误';
    }
  }

  /**
   * 刷新账号列表
   */
  async refreshAccountList() {
    this.accountList = await this.accountService.getAllAccounts();
  }

  /**
   * 切换账号
   */
  async switchAccount(username: string) {
    // 验证账号是否存在
    let isValid = await this.accountService.verifyAccount(username, '');
    // 这里简化处理,实际应该重新输入密码
    this.currentUser = username;
    this.message = `已切换到账号:${username}`;
  }

  /**
   * 删除账号
   */
  async handleDeleteAccount(username: string) {
    let result = await this.accountService.removeAccount(username);
    if (result) {
      if (this.currentUser === username) {
        this.currentUser = '';
        this.loginSuccess = false;
      }
      await this.refreshAccountList();
      this.message = `账号 ${username} 已删除`;
    } else {
      this.message = `删除账号 ${username} 失败`;
    }
  }

  /**
   * 退出登录
   */
  async logout() {
    this.currentUser = '';
    this.loginSuccess = false;
    this.username = '';
    this.password = '';
    this.message = '已退出登录';
  }
}

这段代码需要注意的地方:

  1. @State 变量用于驱动 UI 刷新。loginSuccess 控制登录状态切换,accountList 控制列表刷新。
  2. onPageShow 生命周期回调里刷新账号列表,确保每次进入页面都能获取最新数据。
  3. ForEach 的第三个参数是键生成函数,这里直接用用户名作为 key,确保列表更新时组件复用正确。
  4. 切换账号时没有重新验证密码,实际项目需要加上密码验证逻辑。

第四步:入口文件

typescript 复制代码
// pages/Index.ets
import { LoginPage } from './LoginPage';

@Entry
@Component
struct Index {
  build() {
    Column() {
      LoginPage();
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f0f2f5');
  }
}

常见问题 1:账号已存在时 createAccount 的错误处理

现象: 连续调用 createAccount 两次,第二次不会覆盖已有账号,而是抛出异常。

原因: createAccount 内部做了唯一性校验,同一个应用内不能创建同名账号。这是设计上的保护,但很多新人没处理这个异常,导致程序崩溃。

解决方案:

typescript 复制代码
try {
  await appAccountManager.createAccount(username);
} catch (error) {
  let err = error as BusinessError;
  if (err.code === 12300001) {
    // 账号已存在,直接获取即可
    console.info('Account already exists, skip creation');
  } else {
    throw error;
  }
}

官方文档对错误码的描述比较简略,实际开发中建议把所有可能的错误码都处理掉。12300001 是账号已存在,12300002 是参数无效,12300003 是权限不足。

常见问题 2:setCredential 和 setCustomData 的写入时机

现象: 创建账号后立即调用 setCredential,偶尔会失败。

原因: createAccount 是异步操作,虽然 await 了,但系统底层的账号索引可能还没完全建立。紧接着调用 getAccount 获取账号对象,这个对象可能是过期的或者未完全初始化的。

解决方案:

typescript 复制代码
public async createAccount(username: string, password: string): Promise<boolean> {
  try {
    // 1. 创建账号
    await appAccountManager.createAccount(username);

    // 2. 等一小段时间,确保账号完全初始化
    // 这里用一个小技巧:循环获取直到成功
    let account: appAccountManager.AppAccount | null = null;
    for (let i = 0; i < 3; i++) {
      try {
        account = await appAccountManager.getAccount(username);
        break;
      } catch {
        // 重试
        await this.delay(200);
      }
    }

    if (account === null) {
      return false;
    }

    // 3. 保存密码
    await account.setCredential('password', password);
    await account.setCustomData('createTime', new Date().toISOString());
    return true;
  } catch (error) {
    let err = error as BusinessError;
    console.error(`Create account failed: ${JSON.stringify(err)}`);
    return false;
  }
}

private delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

为什么这个坑容易被忽略? 因为大部分开发者在模拟器上测试时,系统响应快,不容易触发这个问题。但在真机上,尤其是中低端设备,账号初始化延迟会比较明显。如果你在真机上遇到 getAccount 偶尔报错,大概率是这个原因。

最佳实践

1. 密码绝不能明文存储

setCredential 存储的是明文数据,任何有 root 权限的进程都可以读取。实际项目中,必须对密码进行哈希处理。

typescript 复制代码
import { hash } from '@kit.ArkTS';

async function hashPassword(password: string): Promise<string> {
  let encoder = new util.TextEncoder();
  let data = encoder.encodeInto(password);
  let digest = await hash.hash(data, hash.HashType.SHA256);
  return digest;
}

// 存储时用哈希值
let hashedPwd = await hashPassword(password);
await account.setCredential('password', hashedPwd);

2. 不要在 build() 中调用异步方法

ArkUI 的 build() 函数是同步的,不能在里头直接写 await。所有异步操作必须放在事件回调或者独立方法里。上面的代码把异步操作都放在了 handleLoginhandleRegister 这些方法里,这是正确的做法。

3. 账号列表变化后必须主动刷新 UI

getAllAccounts 获取的是当前时刻的快照,不会自动更新。当账号列表变化时(创建、删除),需要重新调用 getAllAccounts 并更新 @State 变量。

typescript 复制代码
// 在删除后主动刷新
await this.handleDeleteAccount(username);
await this.refreshAccountList(); // 这行不能少

4. 使用单例管理 AccountService

账号状态是全局的,不应该在每个页面里创建新的 AccountService 实例。单例模式可以确保当前登录账号、账号列表缓存的数据一致性。

FAQ

Q:为什么真机上创建账号后立刻获取会报错,模拟器正常?

A:真机的账号服务初始化需要时间,尤其是中低端设备。建议在 createAccount 之后加入重试机制,最多重试 3 次,每次间隔 200ms。

Q:账号数据在应用卸载后会清理吗?

A:会。Account Kit 的账号数据与应用的包名绑定,应用卸载后系统会自动清理该应用的所有账号数据。这是它和数据库存储的一个重要区别------不需要手动清理。

Q:多设备协同场景下,账号数据会同步吗?

A:不会。Account Kit 管理的应用内账号是本地账号,不会自动同步到其他设备。如果需要跨设备同步,需要配合云服务或自行实现同步逻辑。

Q:setCredential 和 setCustomData 有什么区别?

A:setCredential 专门用于存储认证凭据(密码、token),系统可能在特定场景下对这些数据进行额外的保护。setCustomData 用于存储自定义属性,更适合存用户昵称、头像 URL 这类非敏感数据。两者在使用上没有本质区别,但从语义上建议区分使用。

Q:为什么 ForEach 里删除账号后列表不刷新?

A:因为 @State 变量 accountList 没有重新赋值。removeAccount 只是删除了系统里的账号,不会自动更新 accountList。需要在删除后调用 getAllAccounts 获取最新列表,然后赋值给 accountList


如果你也在用 Account Kit 做多账号管理,可以重点检查一下创建账号后的写入时机和账号列表的刷新逻辑。这两个地方是实际开发中最容易出问题的环节。

相关推荐
shaodong11233 小时前
HarmonyOS新特性-沉浸光感在叠叠消小游戏中的落地实践
harmonyos·鸿蒙
xd1855785553 小时前
歌词创作工坊-基于鸿蒙的AI歌词创作应用开发实践
人工智能·华为·harmonyos·鸿蒙
子非鱼94274 小时前
07-Flutter 鸿蒙实战 07:故事集列表与缓存设计
学习·flutter·缓存·华为·harmonyos
程序员黑豆4 小时前
从零开始:创建第一个鸿蒙应用程序
前端·harmonyos
千逐686 小时前
HarmonyOS 实战 | 弹窗/提示框怎么做——Alert、Dialog、Toast 详解
华为·harmonyos·鸿蒙
子非鱼94276 小时前
08-Flutter 鸿蒙实战 08:故事详情页,编辑、删除、重新生成与底部弹层
flutter·华为·harmonyos
子非鱼94276 小时前
06-Flutter 鸿蒙实战 06:上传后处理流程,照片识别、故事生成和状态刷新
学习·flutter·华为·harmonyos
Helen_cai7 小时前
HarmonyOS ArkTS 实战:实现一个校园课程表与考勤管理应用
华为·harmonyos
xd1855785557 小时前
藏头诗工坊-基于鸿蒙的AI藏头诗创作应用开发实践
人工智能·学习·华为·harmonyos·鸿蒙