HarmonyOS 登录态治理实战:授权码、会话、登出与隐私边界
HarmonyOS 登录态治理实战:授权码、会话、登出与隐私边界
登录功能看起来只是一个按钮,真正上线后却会牵出一串问题:用户换设备后会话不同步,退出登录后消息页还显示头像,授权范围过大导致隐私提示难解释,Token 过期后页面反复跳登录。登录态治理要先把“华为账号授权”“应用服务端会话”“本地用户资料缓存”“模块登出响应”拆开,而不是把所有逻辑写成一个 isLogin。
本文围绕四个结果展开:
- 授权请求只申请当前业务需要的范围。
- 客户端不把授权码、访问凭据和用户资料混在一起存。
- Token 失效、账号切换、用户登出都有统一事件。
- 隐私资料读取前有明确边界,便于审核和用户解释。
1. 登录态先拆成四层
很多应用登录混乱,是因为把平台授权结果当成应用会话,把用户头像昵称当成登录凭据。更稳的拆法是四层:授权层负责拿授权码,会话层负责应用服务端换票,资料层保存展示字段,事件层通知业务模块清理状态。

| 层级 | 保存内容 | 风险点 |
|---|---|---|
| 授权层 | 授权码、授权结果、错误码 | 授权码不应长期保存 |
| 会话层 | 应用自己的 sessionId、过期时间 |
失效后要能刷新或退登 |
| 资料层 | 头像、昵称、账号绑定状态 | 不要当作安全凭据 |
| 事件层 | 登录成功、会话过期、用户登出 | 各模块必须响应清理 |
这样拆完,页面问的是“当前会话是否有效”,资料页问的是“资料缓存是否可展示”,订单页问的是“会话失效后如何恢复”。每个问题都有对应模块,不会互相抢职责。
2. Account Kit 资料边界和工程目录
Account Kit 提供华为账号登录、授权、用户信息获取、取消授权等能力。工程落地时,建议以官方登录能力作为入口,但应用自己的用户体系仍由服务端维护。
| 资料入口 | 工程落点 |
|---|---|
| Account Kit 简介 | 明确账号登录、授权和用户信息能力边界 |
| 华为账号一键登录 | 登录页发起授权,不直接生成业务会话 |
| 获取用户信息 | 只在用户授权范围内读取资料 |
| 取消授权 | 用户解绑或注销时同步撤销授权 |
建议目录:
entry/src/main/ets/
common/account/AuthRequest.ets
common/account/SessionStore.ets
common/account/AccountProfileStore.ets
common/account/LogoutBus.ets
common/account/PrivacyGate.ets
pages/account/LoginPage.ets
登录页只发起动作,SessionStore 管会话,LogoutBus 通知其他模块,隐私资料读取必须经过 PrivacyGate。
3. AuthRequest 封装授权请求
授权请求需要包含来源、业务场景和授权范围。不要在所有入口都申请同一组权限;比如只为了收藏文章,不一定需要读取头像和昵称。
export type AuthScene = 'login_page' | 'checkout_required' | 'profile_bind'
export interface AuthRequestOptions {
scene: AuthScene
scopes: string[]
traceId: string
}
export class AuthRequestBuilder {
build(scene: AuthScene): AuthRequestOptions {
const baseScopes = ['openid']
const profileScopes = scene === 'profile_bind' ? ['profile'] : []
return {
scene,
scopes: [...baseScopes, ...profileScopes],
traceId: `auth_${Date.now()}_${Math.floor(Math.random() * 10000)}`
}
}
}
这段代码的重点是最小化授权范围。用户只是为了下单而登录时,先拿到应用会话即可;需要完善资料时,再解释为什么读取头像昵称。
4. 授权结果不要直接变成应用会话
客户端拿到授权码后,应交给服务端换取应用自己的会话。这样服务端可以完成账号绑定、风控、设备识别和会话过期策略。
export interface PlatformAuthResult {
code?: string
errorCode?: string
errorMessage?: string
}
export interface AppSessionDTO {
sessionId: string
accountId: string
expiredAt: number
}
export class SessionExchangeService {
async exchange(auth: PlatformAuthResult): Promise<AppSessionDTO> {
if (!auth.code) {
throw new Error(`授权失败:${auth.errorCode ?? 'UNKNOWN'}`)
}
// 实际项目中这里请求应用服务端,由服务端校验授权码并签发应用会话。
return {
sessionId: `sid_${auth.code.slice(0, 8)}`,
accountId: 'account_10001',
expiredAt: Date.now() + 2 * 60 * 60 * 1000
}
}
}
客户端不要把授权码当作长期凭据。授权码只用于换取应用会话,换票完成后应清理临时值。
5. SessionStore 只保存会话摘要
本地会话存储要克制。能由服务端判断的,不要在客户端长期保存;必须保存的,也要带上过期时间和刷新时机。
export type SessionState = 'ANONYMOUS' | 'ACTIVE' | 'EXPIRED' | 'REFRESHING'
export interface SessionSnapshot {
state: SessionState
sessionId?: string
accountId?: string
expiredAt?: number
}
export class SessionStore {
private current: SessionSnapshot = { state: 'ANONYMOUS' }
save(dto: AppSessionDTO): void {
this.current = {
state: 'ACTIVE',
sessionId: dto.sessionId,
accountId: dto.accountId,
expiredAt: dto.expiredAt
}
}
getSnapshot(): SessionSnapshot {
if (this.current.expiredAt && Date.now() >= this.current.expiredAt) {
return { ...this.current, state: 'EXPIRED' }
}
return { ...this.current }
}
clear(): void {
this.current = { state: 'ANONYMOUS' }
}
}
页面不要直接读取底层持久化,而是读取快照。这样 Token 过期时,页面能展示“登录已过期,请重新登录”,不是直接请求失败。
6. LogoutBus 让各模块同步退登
用户退出登录时,订单、消息、会员、缓存都要响应。最危险的做法是只清理登录页状态,其他页面仍然拿旧账号展示数据。

export type LogoutReason = 'user_click' | 'session_expired' | 'account_switch' | 'privacy_revoke'
export interface LogoutEvent {
reason: LogoutReason
occurredAt: number
}
export class LogoutBus {
private listeners: Array<(event: LogoutEvent) => void> = []
subscribe(listener: (event: LogoutEvent) => void): void {
this.listeners.push(listener)
}
publish(reason: LogoutReason): void {
const event: LogoutEvent = { reason, occurredAt: Date.now() }
this.listeners.forEach(listener => listener(event))
}
}
业务模块订阅这个事件后,应清理自己的账号缓存。例如消息模块清空未读数,订单模块重新拉取匿名状态,会员模块刷新权益快照。
7. PrivacyGate 控制资料读取
用户资料不是登录凭据,也不是每个页面都能随便读。读取头像、昵称、手机号、绑定状态前,要明确业务目的和授权状态。
export type ProfileField = 'avatar' | 'nickname' | 'phoneMask' | 'bindStatus'
export interface ProfileReadRequest {
field: ProfileField
scene: 'profile_page' | 'comment_box' | 'customer_service'
}
export class PrivacyGate {
canRead(req: ProfileReadRequest, session: SessionSnapshot): boolean {
if (session.state !== 'ACTIVE') {
return false
}
if (req.field === 'phoneMask' && req.scene !== 'customer_service') {
return false
}
return true
}
}
这段逻辑可以帮助通过隐私说明审查。资料读取要能解释“为什么读、在哪展示、用户退出后如何清理”。
8. 登录页状态要给用户明确反馈
登录页不要只有成功失败。授权取消、网络异常、服务端换票失败、会话过期,都应该给不同文案。
export function buildLoginMessage(state: SessionState, errorCode?: string): string {
if (state === 'ACTIVE') {
return '登录成功,正在同步账号数据'
}
if (state === 'REFRESHING') {
return '正在恢复登录状态,请稍候'
}
if (state === 'EXPIRED') {
return '登录状态已过期,请重新登录'
}
if (errorCode === 'USER_CANCEL') {
return '你已取消授权,可稍后重新登录'
}
return '请使用华为账号登录以同步数据'
}
可解释的登录状态能减少误会。尤其是用户主动取消授权时,不应弹出“系统错误”。
9. 登录链路验收动作
| 场景 | 操作 | 预期结果 |
|---|---|---|
| 首次登录 | 登录页发起账号授权 | 授权后由服务端签发应用会话 |
| 取消授权 | 用户在授权页返回 | 页面展示取消说明,不创建会话 |
| 会话过期 | 修改过期时间后进入订单页 | 触发重新登录或刷新,不展示旧数据 |
| 主动登出 | 点击退出登录 | 各业务模块收到事件并清理账号态 |
| 隐私撤销 | 取消授权或注销账号 | 本地资料缓存清空,页面不再展示头像昵称 |
可以在调试阶段加入状态保护:
export function assertSessionSafe(snapshot: SessionSnapshot): void {
if (snapshot.state === 'ACTIVE' && (!snapshot.sessionId || !snapshot.accountId)) {
throw new Error('有效会话必须包含 sessionId 和 accountId')
}
if (snapshot.state === 'ANONYMOUS' && snapshot.sessionId) {
throw new Error('匿名态不能保留会话标识')
}
}
这个断言可以提前发现退登后残留账号信息的问题。
10. 登录态异常排查表
| 现象 | 优先查看 | 处理建议 |
|---|---|---|
| 退出后仍显示头像 | 资料缓存、登出事件订阅 | 所有资料页统一响应 LogoutBus |
| 频繁跳登录 | 会话过期时间、刷新接口 | 增加前台恢复和过期提示 |
| 授权失败文案不清楚 | 错误码映射 | 区分用户取消、网络失败、服务端换票失败 |
| 隐私说明难解释 | 资料读取场景 | 每个字段绑定业务用途 |
| 换账号后数据串号 | accountId 和本地缓存键 |
缓存键必须包含账号维度 |
登录态复现场景:给读者一组可执行核验
登录态问题常发生在授权码过期、刷新失败和多端登出之间。补充这组核验可以帮助读者验证会话状态不会在页面间漂移。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface SessionReplayCase {
sessionId: any
refreshAt: any
logoutAt: any
source: any
}
const replay76: SessionReplayCase = {
sessionId: 'sample',
refreshAt: 'sample',
logoutAt: 'sample',
source: 'sample',
}
function assertReplay76(item: SessionReplayCase): void {
if (item.logoutAt > 0 && item.refreshAt > item.logoutAt) throw new Error('登出后仍刷新会话')
}
这组核验覆盖会话刷新、登出和来源,能帮助读者定位登录态在页面切换中是否漂移。
11. 小结:登录不是一个布尔值
HarmonyOS 应用的登录态治理,关键是把平台授权、应用会话、资料缓存和登出事件拆开。客户端只保留必要摘要,敏感凭据交给服务端;业务页面不直接判断授权结果,而读取会话快照和资料权限。这样用户取消授权、Token 过期、切换设备、退出登录时,应用都能给出一致且可解释的表现。
更多推荐



所有评论(0)