131 lines
5.1 KiB
Plaintext
131 lines
5.1 KiB
Plaintext
/**
|
||||
|
|
* 家书 FamilyBook · API 令牌存储(鸿蒙端)
|
|||
|
|
*
|
|||
|
|
* 只存一件事:移动端 API 令牌(`api_tokens` 表签发的那一串)。
|
|||
|
|
*
|
|||
|
|
* 🔴 为什么用 `preferences` 而不是本地库 RDB:
|
|||
|
|
* 这是**单个键值**(一把令牌),不是"多行记录"。
|
|||
|
|
* KV 存储天生适合,也不必为它建表、迁移。
|
|||
|
|
* 令牌**不参与同步**(服务端 v33 的 53 张业务表里也不含 api_tokens)——
|
|||
|
|
* 它本来就是"这台设备与服务器之间的凭据",各设备各自一份。
|
|||
|
|
*
|
|||
|
|
* ⚠️ 令牌是敏感凭证:
|
|||
|
|
* · 放 `preferences` 会落在应用沙箱内(其它应用读不到),这对家庭应用够用;
|
|||
|
|
* · 若将来要更严(防 root 提取),应迁到 `@ohos.security.asset`(系统密钥库)。
|
|||
|
|
* 现在不做的原因:Asset Store 的 API 面更复杂,而本应用的数据敏感度
|
|||
|
|
* (家庭日程/观影)还不至于需要它。**这是个有意识的取舍,不是遗漏。**
|
|||
|
|
* · ⚠️ 无论如何**不要**把令牌写进日志或错误信息里往外抛。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { preferences } from '@kit.ArkData';
|
|||
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|||
|
|
import { common } from '@kit.AbilityKit';
|
|||
|
|
|
|||
|
|
const DOMAIN: number = 0x0000;
|
|||
|
|
const TAG: string = 'FbToken';
|
|||
|
|
|
|||
|
|
/** preferences 实例名(沙箱内的文件名) */
|
|||
|
|
const STORE_NAME: string = 'fb_auth';
|
|||
|
|
/** 令牌的键名 */
|
|||
|
|
const KEY_TOKEN: string = 'api_token';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 令牌存储(同步读、异步写的门面)。
|
|||
|
|
*
|
|||
|
|
* 🔴 为什么提供一个**同步的 `get()`**:
|
|||
|
|
* 页面渲染时经常需要立刻知道"有没有令牌"(决定要不要发起同步)。
|
|||
|
|
* 若只能异步读,每个调用点都要 await,把简单逻辑搞得支离破碎。
|
|||
|
|
* ⇒ 用"启动时预加载进内存 + 写时同时更新内存"的方式提供同步读。
|
|||
|
|
* ⚠️ 前提是**启动时必须先 `init()`**,否则内存里是空的(会误判成"未登录")。
|
|||
|
|
*/
|
|||
|
|
class FbTokenStoreImpl {
|
|||
|
|
private cached: string = '';
|
|||
|
|
private store: preferences.Preferences | null = null;
|
|||
|
|
private ready: boolean = false;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 预加载令牌进内存。应在应用启动时调用一次(见 EntryAbility)。
|
|||
|
|
* ⚠️ 幂等:重复调用直接返回,不会重复读盘。
|
|||
|
|
*/
|
|||
|
|
async init(ctx: common.Context): Promise<void> {
|
|||
|
|
if (this.ready) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
|||
|
|
const v: preferences.ValueType = await this.store.get(KEY_TOKEN, '');
|
|||
|
|
this.cached = typeof v === 'string' ? v : '';
|
|||
|
|
this.ready = true;
|
|||
|
|
hilog.info(DOMAIN, TAG, '令牌预加载完成,%{public}s',
|
|||
|
|
this.cached === '' ? '未设置' : '已设置');
|
|||
|
|
} catch (err) {
|
|||
|
|
// ⚠️ 读不到就当"没有令牌",**不要**让启动流程挂掉 ——
|
|||
|
|
// 用户仍能用本地数据(本地优先的好处)。
|
|||
|
|
hilog.warn(DOMAIN, TAG, '令牌预加载失败:%{public}s', JSON.stringify(err));
|
|||
|
|
this.ready = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 同步取令牌(未设置返回空串)。⚠️ 需先 init(),否则会误判为未登录 */
|
|||
|
|
get(): string {
|
|||
|
|
if (!this.ready) {
|
|||
|
|
hilog.warn(DOMAIN, TAG, '令牌还没预加载完,本次读到的可能是空值');
|
|||
|
|
}
|
|||
|
|
return this.cached;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 是否已设置令牌(同步) */
|
|||
|
|
has(): boolean {
|
|||
|
|
return this.cached !== '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 预加载是否已完成(同步)。
|
|||
|
|
*
|
|||
|
|
* 🔴 为什么需要它:`get()` 在**没预加载完**时会返回空串(看起来像"未登录")。
|
|||
|
|
* 启动流程里若据此判断,就会出现"明明有令牌却显示登录页"。
|
|||
|
|
* ⇒ 调用方应先用 `isReady()` 确认预加载完成,再做登录态判断。
|
|||
|
|
* (见 Index.bootCheck 里的等待循环。)
|
|||
|
|
*/
|
|||
|
|
isReady(): boolean {
|
|||
|
|
return this.ready;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 保存令牌(同时更新内存缓存与磁盘) */
|
|||
|
|
async set(ctx: common.Context, token: string): Promise<boolean> {
|
|||
|
|
try {
|
|||
|
|
if (this.store === null) {
|
|||
|
|
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
|||
|
|
}
|
|||
|
|
await this.store.put(KEY_TOKEN, token);
|
|||
|
|
// ⚠️ `put` 只写内存,**必须 `flush`** 才落盘 ——
|
|||
|
|
// 不 flush 的话进程被杀就丢了(表现为"登录后又变未登录")。
|
|||
|
|
await this.store.flush();
|
|||
|
|
this.cached = token;
|
|||
|
|
this.ready = true;
|
|||
|
|
hilog.info(DOMAIN, TAG, '令牌已保存');
|
|||
|
|
return true;
|
|||
|
|
} catch (err) {
|
|||
|
|
hilog.error(DOMAIN, TAG, '令牌保存失败:%{public}s', JSON.stringify(err));
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 清除令牌(登出) */
|
|||
|
|
async clear(ctx: common.Context): Promise<void> {
|
|||
|
|
try {
|
|||
|
|
if (this.store === null) {
|
|||
|
|
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
|||
|
|
}
|
|||
|
|
await this.store.delete(KEY_TOKEN);
|
|||
|
|
await this.store.flush();
|
|||
|
|
this.cached = '';
|
|||
|
|
hilog.info(DOMAIN, TAG, '令牌已清除');
|
|||
|
|
} catch (err) {
|
|||
|
|
hilog.error(DOMAIN, TAG, '令牌清除失败:%{public}s', JSON.stringify(err));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 全局单例 */
|
|||
|
|
export const FB_TOKEN_STORE: FbTokenStoreImpl = new FbTokenStoreImpl();
|