以前做 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,双槽轮换虽然更长,却更容易保住最后一个可用状态。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐