HarmonyOS 7 Asset Store Kit + ArkTS:登录 Token 双槽轮换中的新旧凭据切换、查询条件校验与延迟清理【鸿蒙心迹】
以前做 Token 轮换,我会直接覆盖旧值。后来在“新 token 写入”和“业务切换”之间注入异常,发现旧 token 已删,而新 token 因查询条件不完整没有真正被业务读到。
CredentialRotationLab 处理轮换顺序:新凭据先入库,验证通过后再切 active alias,旧凭据延迟删除。

一、为什么不是直接更新
数据:cred_rotate_20261001_12、ACTIVE_V12、Active Alias session_token_v12、Old Alias session_token_v11、Generation 12、Query Matches 1、Validation PASS、Cleanup Pending false、Old Removed 1、Last Switch 12:07:44。
凭据使用 A/B 两个槽位。session_token_v11 原来是 ACTIVE,先创建 session_token_v12 作为 STANDBY;查询和 generation、userId 校验通过后再切换入口。新凭据失败时,旧凭据仍可用。
二、先新增新槽位,不覆盖旧槽位
Asset Store Kit 适合保存 Token 等短敏感数据。Demo 中新 token 使用新 alias 写入。
import { asset } from '@kit.AssetStoreKit'
import { util } from '@kit.ArkTS'
private encoder: util.TextEncoder = new util.TextEncoder()
private bytes(text: string): Uint8Array {
return this.encoder.encodeInto(text)
}
async saveToken(
alias: string,
token: string,
generation: number,
userId: string
): Promise<void> {
const attrs: asset.AssetMap = new Map()
attrs.set(asset.Tag.SECRET, this.bytes(token))
attrs.set(asset.Tag.ALIAS, this.bytes(alias))
attrs.set(
asset.Tag.ACCESSIBILITY,
asset.Accessibility.DEVICE_FIRST_UNLOCKED
)
attrs.set(
asset.Tag.DATA_LABEL_NORMAL_1,
this.bytes(userId)
)
attrs.set(
asset.Tag.DATA_LABEL_NORMAL_2,
this.bytes(String(generation))
)
await asset.add(attrs)
}
alias 直接带版本:session_token_v11、session_token_v12。
业务 label 只存 userId、generation,token 放在 SECRET。
三、查询时 RETURN_TYPE 不能省
这个坑很具体。
只按 ALIAS 查询却没设置返回类型时,调用可能不报错,但业务拿不到期望结果,所以查询统一封装。
async queryByAlias(
alias: string
): Promise<Array<asset.AssetMap>> {
const query: asset.AssetMap = new Map()
query.set(
asset.Tag.ALIAS,
this.bytes(alias)
)
query.set(
asset.Tag.RETURN_TYPE,
asset.ReturnType.ALL
)
const result = await asset.query(query)
hilog.info(
0x0000,
'TokenAssetStore',
`query matches=${result.length} returnType=ALL`
)
return result
}
本轮 Query Matches=1。随后校验 user_10086 和 generation 12;返回 0 条、命中多条或 generation 不对都不切 active。
四、active alias 的切换位置不能提前
轮换关键是顺序。
async rotate(newToken: string): Promise<void> {
const oldAlias = 'session_token_v11'
const newAlias = 'session_token_v12'
this.state = 'STANDBY_V12'
await this.store.saveToken(
newAlias,
newToken,
12,
'user_10086'
)
this.state = 'VALIDATING'
const list = await this.store.queryByAlias(newAlias)
if (list.length !== 1) {
throw new Error('new credential not found')
}
const generation =
this.store.readGeneration(list[0])
if (generation !== 12) {
throw new Error(`generation mismatch: ${generation}`)
}
this.validation = 'PASS'
this.activeAlias = newAlias
this.state = 'SWITCHED'
await this.scheduleOldCredentialCleanup(oldAlias)
this.state = 'ACTIVE_V12'
}
active alias 只在查询和校验通过后切换。正式项目还应持久化当前 generation。
五、旧凭据为什么延迟删除
新 token 切换成功后,我也没有立刻删 v11。
一些请求可能已经带着旧 token 发出,如果切换后立刻删除旧凭据,重试链路会变脆。Demo 用短延迟模拟清理窗口:
private async scheduleOldCredentialCleanup(
oldAlias: string
): Promise<void> {
this.cleanupPending = true
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 3000)
})
const query: asset.AssetMap = new Map()
query.set(
asset.Tag.ALIAS,
this.store.bytes(oldAlias)
)
await asset.remove(query)
this.oldRemoved++
this.cleanupPending = false
}
setTimeout 只用于 Demo 展示顺序。正式项目若清理跨后台或跨进程,应保存“旧 alias 待清理”状态,再在安全时机完成。本轮最终 Cleanup Pending=false、Old Removed=1。
六、日志要能解释一次完整轮换
项目结构只保留页面、轮换协调器、Asset Store 封装和槽位模型。HiLog 固定输出:
add alias=session_token_v12 gen=12
query matches=1 returnType=ALL
validate generation=12 PASS
switch active alias -> v12
remove old alias=v11
State: VALIDATING -> ACTIVE_V12

七、最终结果要同时看“新可用”和“旧已清”
手机页面里:State ACTIVE_V12,Active Alias session_token_v12,Old Alias session_token_v11,Generation 12,Validation PASS,Cleanup Pending false,Old Removed 1。Slot A 已 REMOVED,Slot B 为 ACTIVE。
查询区保留 RETURN_TYPE=ALL、matches=1、userId=user_10086。

八、失败路径比成功路径更值得设计
新增失败、查询异常或 generation 不对时都不切 active。切到 v12 后若旧凭据删除失败,只保留清理任务,不再切回 v11。
九、双槽轮换的核心是不要过早破坏旧状态
最终链路是:
ACTIVE_V11 → STANDBY_V12 → VALIDATING → SWITCHED → ACTIVE_V12
Asset Store Kit 提供安全存储能力,稳定性取决于写入、查询、切换和清理顺序。对于Token,双槽轮换虽然更长,却更容易保住最后一个可用状态。
更多推荐




所有评论(0)