HarmonyOS 鸿蒙 TOTP 身份验证器开发(一):TOTP 卡片组件的实现
本文介绍了基于HarmonyOS和ArkTS的TOTP验证码生成组件实现方案。项目采用分层架构设计,包含算法服务层、加密工具层和UI组件层。核心实现包括TOTP算法、加密模块和Base32编解码等。具有模块化、高性能和标准兼容等特点,可集成到各类HarmonyOS应用中。
项目背景
TOTP (Time-based One-Time Password) 是一种基于时间的一次性密码算法,广泛应用于双因素身份验证场景。Google Authenticator、Microsoft Authenticator等知名应用都采用这个标准协议。本文将详细介绍如何在HarmonyOS应用中使用ArkTS语言实现一个功能完整的TOTP验证码生成组件。

项目结构设计
项目采用了典型的分层架构,将不同职责的代码分离到不同目录:
├── common/
│ ├── constants/ # 常量定义,如默认周期、位数等
│ └── utils/ # 工具类,包括加密、编码、时间处理
├── model/ # 数据模型,定义账户结构
├── service/ # 核心业务逻辑,TOTP算法实现
└── components/ # UI组件,负责展示和交互
这种组织方式的优势在于:
- 职责分离: 业务逻辑与UI渲染解耦,便于单独测试
- 可维护性: 修改某个模块不会影响其他部分
- 可复用性: 工具类和服务层可以在其他项目中复用
核心实现详解
1. TOTP算法实现
TOTP算法遵循RFC 6238标准,其核心是将当前时间转换为计数器,然后通过HMAC算法生成动态验证码。整个过程可以分解为几个关键步骤。
首先看TOTP服务类的完整实现:
// service/TOTP.ets
export class TOTP {
private secret: string; // Base32编码的密钥
private digits: number; // 验证码位数
private period: number; // 时间周期(秒)
private algorithm: TOTPAlgorithm; // 哈希算法
private decodedSecret?: Uint8Array; // 解码后的密钥缓存
private cachedToken?: CachedToken; // 验证码缓存
constructor(config: TOTPConfig) {
this.secret = config.secret;
this.digits = config.digits || TOTPConstants.DIGITS;
this.period = config.period || TOTPConstants.PERIOD;
this.algorithm = config.algorithm || TOTPConstants.ALGORITHM;
}
async generateToken(timestamp: number = TimeUtils.getTime()): Promise<string> {
// 1. 计算时间计数器
// 将毫秒时间戳转为秒,再除以周期得到计数器值
const epoch = Math.floor(timestamp / 1000);
const counter = Math.floor(epoch / this.period);
// 2. 缓存检查,避免同一时间片重复计算
if (this.cachedToken?.timeslot === counter) {
return this.cachedToken.value;
}
// 3. 将计数器转为8字节大端序数组
// TOTP标准要求使用8字节表示计数器
const counterBuffer = new ArrayBuffer(8);
const counterView = new DataView(counterBuffer);
counterView.setUint32(0, 0, false); // 高4字节为0
counterView.setUint32(4, counter, false); // 低4字节存储计数器
// 4. 使用HMAC算法计算哈希值
const hmacArray = await CryptoUtils.hmac(
this.algorithm,
this.getDecodedSecret(),
new Uint8Array(counterBuffer)
);
// 5. 动态截断(Dynamic Truncation)
// 取哈希结果最后一个字节的低4位作为偏移量
const offset = hmacArray[hmacArray.length - 1] & 0x0f;
// 从偏移位置取4字节,转为整数后对10^digits取模
const code =
((hmacArray[offset] & 0x7f) << 24 |
(hmacArray[offset + 1] & 0xff) << 16 |
(hmacArray[offset + 2] & 0xff) << 8 |
(hmacArray[offset + 3] & 0xff)) % Math.pow(10, this.digits);
// 6. 格式化为指定位数的字符串,不足补0
const token = code.toString().padStart(this.digits, '0');
// 7. 更新缓存
this.cachedToken = { value: token, timeslot: counter };
return token;
}
// 计算当前时间片剩余秒数
getTimeRemaining(timestamp: number = TimeUtils.getTime()): number {
const epoch = Math.floor(timestamp / 1000);
return this.period - (epoch % this.period);
}
// 计算当前时间片已用进度(0~1)
getProgress(timestamp: number = TimeUtils.getTime()): number {
const remainingMs = timestamp % (this.period * 1000);
return remainingMs / (this.period * 1000);
}
}
这段代码实现了TOTP的核心逻辑。其中最关键的是generateToken方法,它严格遵循RFC 6238标准。动态截断算法确保了即使攻击者知道哈希结果的一部分,也无法推算出完整的验证码。
缓存机制的设计也很重要:由于验证码在30秒内保持不变,频繁调用generateToken时可以直接返回缓存结果,避免重复的加密计算,提升性能。
2. 使用CryptoArchitectureKit进行加密计算
HarmonyOS提供了@kit.CryptoArchitectureKit框架,封装了各种加密算法。在TOTP中,我们需要使用HMAC(Hash-based Message Authentication Code)算法。
// common/utils/CryptoUtils.ets
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { BusinessError } from '@kit.BasicServicesKit';
export class CryptoUtils {
static async hmac(algName: string, key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
try {
// 1. 创建对称密钥生成器
// HMAC使用对称密钥,这里创建专门的生成器
const symKeyGenerator = cryptoFramework.createSymKeyGenerator('HMAC');
// 2. 将密钥转换为框架要求的DataBlob格式
const keyBlob: cryptoFramework.DataBlob = { data: key };
// 3. 生成HMAC密钥对象
const symKey = await symKeyGenerator.convertKey(keyBlob);
// 4. 创建HMAC实例,指定哈希算法(SHA1/SHA256/SHA512)
const mac = cryptoFramework.createMac(algName);
// 5. 初始化HMAC实例,绑定密钥
await mac.init(symKey);
// 6. 输入要计算的数据
await mac.update({ data: data });
// 7. 完成计算并获取结果
const macResult = await mac.doFinal();
return macResult.data;
} catch (error) {
let error = e as BusinessError;
throw new Error(`HMAC计算失败: ${error.code} - ${error.message}`);
}
}
}
这个工具类封装了CryptoArchitectureKit的使用细节。需要注意的几点:
- 异步操作: 所有加密操作都是异步的,必须使用
async/await或Promise处理 - DataBlob格式: 框架要求数据以特定格式传入,需要进行转换
- 错误处理: 加密操作可能失败(如密钥格式错误),需要捕获异常
相比自己实现HMAC算法,使用系统提供的框架有几个优势:经过充分测试、性能优化好、可能利用硬件加速。
3. Base32编码解码
TOTP密钥通常以Base32格式编码(如JBSWY3DPEHPK3PXP),需要解码为原始字节才能用于加密计算。Base32使用32个字符(A-Z和2-7)表示数据,每5位二进制对应1个字符。
// common/utils/Base32Utils.ets
export class Base32Utils {
// Base32字符表,共32个字符
private static readonly BASE32_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
static decode(base32: string): Uint8Array {
// 1. 预处理:转大写并移除填充符
const cleanStr = base32.toUpperCase().replace(/=+$/, '');
const alphabet = Base32Utils.BASE32_CHARS;
const decoded: number[] = [];
// 2. 位操作解码
let buffer = 0; // 临时缓冲区
let bits = 0; // 当前缓冲区中的位数
for (let i = 0; i < cleanStr.length; i++) {
const char = cleanStr[i];
const val = alphabet.indexOf(char);
if (val === -1) continue; // 跳过非法字符
// 3. 将5位数据加入缓冲区
buffer = (buffer << 5) | val;
bits += 5;
// 4. 缓冲区满8位时输出一个字节
if (bits >= 8) {
bits -= 8;
decoded.push(buffer >> bits);
buffer &= (1 << bits) - 1; // 保留剩余位
}
}
return new Uint8Array(decoded);
}
}
Base32解码的核心是位操作:
- 每次读入一个字符(5位数据)
- 累积到缓冲区
- 当缓冲区达到8位时,输出一个字节
- 这样可以将5位对齐的Base32转为8位对齐的字节数组
4. 时间同步处理
TOTP算法依赖准确的时间,客户端和服务器必须使用相同的时间基准。项目使用HarmonyOS的系统时间接口:
// common/utils/TimeUtils.ets
import { systemDateTime } from '@kit.BasicServicesKit';
export class TimeUtils {
// 获取当前时间戳(毫秒)
static getTime() {
return systemDateTime.getTime(false);
}
}
关于时间同步的几点说明:
- 不使用NTP: 虽然NTP可以获取精确时间,但会增加网络依赖和安全风险
- 系统时间: 直接使用设备系统时间,简单可靠
- 容差窗口: 服务端通常允许前后30-60秒的误差,轻微时钟偏差可以被接受
- 用户责任: 应该在界面提示用户保持系统时间准确
5. ArkUI组件实现
TOTPItem组件负责展示单个账户的验证码、发行方信息和倒计时进度。这是一个典型的ArkUI自定义组件:
// components/TOTPItem.ets
@Component
export struct TOTPItem {
// 1. 组件属性
@Prop account: TOTPAccount; // 从父组件传入的账户信息
@State countdown: number = 30; // 倒计时秒数
@State progress: number = 1; // 进度值(0~1)
@State token: string = ""; // 当前验证码
private totp?: TOTP; // TOTP服务实例
private totalFrames: number = 3600; // 动画总帧数
private progressAnimator?: AnimatorResult; // 动画控制器
// 2. 更新验证码
async updateToken() {
if (this.totp) {
const newToken = await this.totp.generateToken();
// 只有验证码真正变化时才更新(避免不必要的渲染)
if (newToken != this.token) {
this.token = newToken;
}
}
}
// 3. 更新进度和倒计时
updateProgress() {
if (this.totp) {
this.countdown = this.totp.getTimeRemaining();
this.progress = this.totp.getProgress();
// 周期结束时生成新验证码
if (this.countdown >= this.account.period) {
this.updateToken();
}
}
}
// 4. UI结构
build() {
Row() {
// 左侧:发行方和账户名
Column({ space: 2 }) {
Text(this.account.issuer)
.fontSize(18)
Text(this.account.label)
.fontColor($r('sys.color.font_secondary'))
.fontSize(12)
}
.alignItems(HorizontalAlign.Start)
Blank() // 弹性空白,将左右内容推开
// 右侧:验证码和倒计时
Text(this.token)
.margin({ right: 12 })
.fontSize(28)
.fontWeight(FontWeight.Bold)
// 环形进度条,中心显示倒计时秒数
Progress({
value: this.progress * this.totalFrames,
total: this.totalFrames,
type: ProgressType.Ring
})
.width(32)
.color(Color.Red)
.style({ strokeWidth: 2 })
.overlay(this.countdown.toString(), {
align: Alignment.Center
})
}
.width('100%')
.height(72)
.padding(12)
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_primary'))
}
}

这个组件的UI结构比较清晰:
- Row布局: 左右排列内容
- 左侧Column: 垂直堆叠发行方和账户名
- Blank: 自动填充中间空白
- 右侧: 大号验证码文本 + 环形倒计时进度条
状态管理使用@State装饰器,当这些状态变化时,UI会自动重新渲染。@Prop装饰器表示该属性从父组件传入,是只读的。
6. 组件生命周期管理
ArkUI组件有完整的生命周期钩子,类似于React或Vue。正确使用生命周期对于资源管理至关重要:
// components/TOTPItem.ets (生命周期部分)
@Component
export struct TOTPItem {
// ... 省略其他代码
// 组件即将显示时调用
aboutToAppear(): void {
// 1. 创建TOTP服务实例
this.totp = new TOTP({
secret: this.account.secret,
digits: this.account.digits,
period: this.account.period
});
// 2. 初始化状态
this.updateProgress(); // 计算初始进度
this.updateToken(); // 生成初始验证码
// 3. 启动定时器
this.startTimer();
}
// 组件即将销毁时调用
aboutToDisappear(): void {
// 清理定时器,防止内存泄漏
this.stopTimer();
}
// 启动动画定时器
private startTimer() {
this.totalFrames = this.account.period * TOTPConstants.MAX_FRAME_RATE;
// 使用Animator API创建动画
this.progressAnimator = this.getUIContext().createAnimator({
duration: this.account.period * 1000, // 动画时长等于TOTP周期
easing: 'linear', // 线性动画
delay: 0,
fill: 'forwards',
direction: 'normal',
iterations: -1, // 无限循环
begin: this.totalFrames, // 从满进度开始
end: 0 // 到0结束
});
// 每帧回调,更新进度
this.progressAnimator.onFrame = () => {
this.updateProgress();
};
this.progressAnimator.play();
}
// 停止定时器
private stopTimer() {
this.progressAnimator?.finish();
this.progressAnimator = undefined;
}
}
生命周期管理的关键点:
aboutToAppear时机:
- 在组件首次渲染之前调用
- 适合进行数据初始化、网络请求、定时器启动等操作
- 此时可以安全地访问组件的props
aboutToDisappear时机:
- 在组件从组件树移除时调用
- 必须在这里清理资源:停止定时器、取消网络请求、移除事件监听等
- 不清理会导致内存泄漏,定时器继续运行浪费资源
为什么不用setInterval:
createAnimator与系统刷新率同步,性能更好- 自动处理应用切到后台的情况
- 更适合动画场景,不会出现卡顿
7. 数据模型设计
使用TypeScript类定义账户数据结构,提供类型安全和默认值处理:
// model/TOTPAccount.ets
import { TOTPConstants } from "../common/constants/TOTPConstants";
import { TOTPAlgorithm } from "../service/TOTP";
export class TOTPAccount {
id: string; // 唯一标识符
label: string; // 账号名称(如用户名、邮箱)
issuer: string; // 服务提供商(如Google、GitHub)
secret: string; // Base32编码的密钥
algorithm: TOTPAlgorithm; // 哈希算法(SHA1/SHA256/SHA512)
digits: number; // 验证码位数(通常为6,有些服务用8)
period: number; // 时间周期(通常为30秒)
constructor(
id: string,
label: string,
issuer: string,
secret: string,
algorithm: TOTPAlgorithm = TOTPConstants.ALGORITHM,
digits: number = TOTPConstants.DIGITS,
period: number = TOTPConstants.PERIOD
) {
this.id = id;
this.label = label;
this.issuer = issuer;
this.secret = secret;
this.algorithm = algorithm;
this.digits = digits;
this.period = period;
}
}
这个模型类的设计考虑:
- 必填字段: id、label、issuer、secret必须提供
- 可选参数: algorithm、digits、period有合理的默认值
- 扩展性: 后续可以添加图标、颜色、排序等字段
实际使用时,这些数据会从二维码扫描或手动输入中获取,符合otpauth://协议格式。
8. 常量定义
将魔法数字集中管理,便于维护和调整:
// common/constants/TOTPConstants.ets
import { TOTPAlgorithm } from "../../service/TOTP";
export class TOTPConstants {
static readonly PERIOD: number = 30; // 标准周期30秒
static readonly DIGITS: number = 6; // 标准6位验证码
static readonly ALGORITHM: TOTPAlgorithm = TOTPAlgorithm.SHA1; // 标准算法SHA1
static readonly UPDATE_INTERVAL: number = 500; // UI更新间隔(毫秒)
static readonly MAX_FRAME_RATE: number = 120; // 动画最高帧率
}
使用常量的好处:
- 统一配置: 修改默认值只需改一处
- 可读性: 代码中直接使用常量名,语义清晰
- 类型安全: TypeScript会检查常量使用是否正确
使用示例
在页面中使用TOTPItem组件很简单:
// pages/Index.ets
@Entry
@Component
struct Index {
@State accounts: TOTPAccount[] = [
new TOTPAccount(
'1',
'user@example.com',
'Google',
'JBSWY3DPEHPK3PXP'
)
];
build() {
Column() {
ForEach(this.accounts, (account: TOTPAccount) => {
TOTPItem({ account: account })
})
}
}
}
技术总结
1. 架构设计
- 分层清晰: UI、业务逻辑、工具类各司其职
- 单一职责: 每个类只负责一件事,代码简洁易懂
- 依赖管理: 组件通过接口依赖服务,而非直接耦合实现
2. ArkUI特性应用
- 声明式UI: 使用
@Component和@State实现响应式界面 - 生命周期: 正确使用
aboutToAppear和aboutToDisappear管理资源 - 动画系统: 使用
createAnimator实现流畅的倒计时动画
3. 系统能力集成
- 加密框架: 使用CryptoArchitectureKit而非自己实现算法,安全可靠
- 时间服务: 使用BasicServicesKit获取系统时间
4. 性能优化
- 缓存机制: 同一时间片内复用验证码,避免重复计算
- 懒加载: Base32解码结果缓存,只在首次使用时计算
- 动画优化: 使用Animator替代setInterval,与系统刷新同步
后续扩展方向
这个基础实现可以继续完善:
- 数据持久化: 使用首选项或数据库保存账户列表
- 二维码扫描: 集成相机Kit实现扫码添加账户
- 密钥安全: 使用HUKS(密钥管理服务)加密存储密钥
- 批量导入: 支持从其他验证器导出的文件导入
- 备份恢复: 实现账户的云备份和恢复功能
- 生物识别: 添加指纹或面容识别保护
总结
本文从实际代码出发,详细介绍了在HarmonyOS中实现TOTP验证器卡片组件的完整过程。重点关注了:
- TOTP算法的准确实现
- CryptoArchitectureKit的正确使用
- ArkUI组件的生命周期管理
- 合理的项目结构组织
- 性能和安全性的平衡
代码按功能模块分离,每个文件职责明确,便于理解、测试和维护。这种架构设计适合中小型HarmonyOS应用开发。
HarmonyOS赋能资源丰富度建设活动进行中,加入班级成为学员,完成课程并通过认证考核,将获得HarmonyOS 应用开发者认证电子证书,助力职业发展。另外发布学习资源还有机会拿到奖品激励,赶快一起加入鸿蒙生态的建设中吧~
https://developer.huawei.com/consumer/cn/training/classDetail/465c461c9200457b9ece8d31e5a0d94b?type=1?ha_source=hmosclass-zonghe&ha_sourceId=89000236
更多推荐


所有评论(0)