HarmonyOS鸿蒙Next中Account Kit怎样实现登出

HarmonyOS鸿蒙Next中Account Kit怎样实现登出 目前打算尝试通过撤回授权的方式实现登出,预期逻辑:点击登出->跳转至登录首页->点击登录拉起授权界面(正常无登出情况下点击登录界面后,无授权界面直接跳转至应用首页)

用云真机调试目前问题现象:点击登出->弹窗显示登出成功->跳转至登录页->点击后直接跳转首页(无法拉起授权页)

请各位大佬帮忙看看,怎么才能在这种情况下实现登出、拉起授权页哇🤔我的这套登录代码目前只第一次在云真机调试的时候拉起过授权页,后面再也没有拉起过了

登出代码:

async performLogout() {
  try {
    // 1. 创建一个取消授权的请求对象
    const provider = new authentication.HuaweiIDProvider();
    const cancelRequest: authentication.CancelAuthorizationRequest = provider.createCancelAuthorizationRequest();
    // 2. 执行该请求
    const controller = new authentication.AuthenticationController(getContext(this));
    await controller.executeRequest(cancelRequest);

    hilog.info(DOMAIN, TAG, 'Account Kit 登出成功');
    promptAction.showToast({ message: '已退出登录,即将执行clearLocalState' });
    this.clearLocalState();
  } catch (err) {
    let error = err as BusinessError;
    hilog.error(DOMAIN, TAG, `退出登录异常 - code: ${error.code}, msg: ${error.message}`);
    const errorTime = new Date().toISOString();
    const errObj = err as Record<string, Object>;
    const code = (errObj['code'] !== undefined) ? errObj['code'] + '' : '无code';
    const message = (errObj['message'] !== undefined) ? errObj['message'] + '' : '无message';
    let dataStr = '无data';
    if (errObj['data'] !== undefined) {
          try {
            dataStr = JSON.stringify(errObj['data']);
          } catch (e) {
            dataStr = 'data序列化失败';
          }
        }
        hilog.error(DOMAIN, TAG,
          `❌ CloudDB 操作失败 | 时间: %{public}s | code: %{public}s | message: %{public}s | data: %{public}s`,
          errorTime, code, message, dataStr
        );
        const dialogMsg = `时间: ${errorTime}\ncode: ${code}\nmessage: ${message}`;
        promptAction.showDialog({
          title: '退出失败',
          message: dialogMsg,
          buttons: [{ text: '知道了', color: '#FF6B6B' }]
        });
  }
}

// 清理本地状态并跳转登录页
clearLocalState() {
  userState.currentUserId = '';
  userState.currentNickname = '';
  router.replaceUrl({ url: 'pages/LoginPage' });
}

登录代码:

controller: loginComponentManager.LoginWithHuaweiIDButtonController = new loginComponentManager.LoginWithHuaweiIDButtonController();
@State isLoading: boolean = false;
@State errorMsg: string = '';
aboutToAppear(): void {
  // 监听登录结果,直接获取 UnionID
  this.controller.onClickLoginWithHuaweiIDButton(async (error: BusinessError, credential: authentication.LoginWithHuaweiIDCredential) => {
    if (error) {
      hilog.error(DOMAIN, TAG, `登录失败: code=${error.code}, msg=${error.message}`);
      promptAction.showToast({ message: '登录失败'+'code'+error.code+'message'+error.message, duration: 4000 });
      this.errorMsg = `登录失败: ${error.message}`;
      this.isLoading = false;
      return;
    }
    // 3. 登录成功,直接从 credential 中获取 UnionID 和 OpenID
    const unionId: string | undefined = credential?.unionID;
    const openId: string | undefined = credential?.openID;
    hilog.info(DOMAIN, TAG, `登录成功`);
    hilog.info(DOMAIN, TAG, `UnionID: ${unionId}`);
    hilog.info(DOMAIN, TAG, `OpenID: ${openId}`);
    if (unionId) {
      try {
        const profile = await getOrCreateUserProfile(unionId);
         userState.currentUserId = unionId;
         userState.currentNickname=profile.nickname;
        router.replaceUrl({ url: 'pages/Index' });
      } catch (err) {
        this.isLoading = false;
      }
    } else {
      this.errorMsg = '登录失败:未能获取到用户身份信息';
      promptAction.showToast({message:'登录失败:未能获取到用户身份信息', duration: 4000} );
      this.isLoading = false;
    }
  });
}

build() {
  Column() {
    // .....其他界面UI
    // 登录按钮及状态区域
    Column() {
      // 错误提示
      if (this.errorMsg) {
        Text(this.errorMsg)
          .fontSize(14)
          .fontColor('#FF6B6B')
          .textAlign(TextAlign.Center)
          .margin({ bottom: 20 })
          .width('80%')
      }

      //  华为登录按钮
      if (!this.isLoading) {
        LoginWithHuaweiIDButton({
          params: {
            loginType: loginComponentManager.LoginType.ID,   // 标准华为账号登录
            style: loginComponentManager.Style.BUTTON_CUSTOM,
            extraStyle: {
              buttonStyle: new loginComponentManager.ButtonStyle()
                //.backgroundColor($r('app.color.theme_purple'))
                .backgroundColor($r('app.color.theme_purple'))
            }
          },
          controller: this.controller
        })
          .height(50)
          .width('85%')
          .borderRadius(25)
          .shadow({ radius: 8, color: '#1A000000', offsetY: 2 })
      }
      // Loading 动画
      if (this.isLoading) {
        LoadingProgress()
          .width(36)
          .height(36)
          .margin({ top: 20 })
        Text('正在登录...')
          .fontSize(13)
          .fontColor('#AAAAAA')
          .margin({ top: 8 })
      }
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .margin({ bottom: 60 })
  }
  .width('100%')
  .height('100%')
  .backgroundColor($r('app.color.card_bg'))
}

#getOrCreateUserProfile方法

export async function getOrCreateUserProfile(uid: string): Promise<UserProfile> {
    const zone =await getAuthenticatedZone();
    const query = new cloudDatabase.DatabaseQuery(UserProfile);
    query.equalTo('uid', uid);
    const result = await zone.query(query);
    if (result.length > 0) {
      promptAction.showToast({ message: '✅ 用户资料已存在', duration: 7000 });
      return result[0] as UserProfile;
    }
    // ========== 如果用户资料不存在,先刷新凭证,再执行 upsert ==========
    const now = Date.now();
    let profile = new UserProfile();
    try {
      profile.uid = uid;
      profile.nickname = 'xxxx';
      profile.avatarUrl = '';
      profile.createAt = now;
      profile.updateAt = now;
      await zone.upsert(profile);
      return profile;
    }catch (err){
      // 1. 生成精确到毫秒的错误时间戳
      const errorTime = new Date().toISOString();
      // 2. 将错误对象转为Record,方便安全地提取字段
      const errObj = err as Record<string, Object>;
      // 3. 安全地提取 code 和 message
      const code = (errObj['code'] !== undefined) ? errObj['code'] + '' : '无code';
      const message = (errObj['message'] !== undefined) ? errObj['message'] + '' : '无message';

      // 4. 如果存在 data,提取出来
      let dataStr = '无data';
      if (errObj['data'] !== undefined) {
        try {
          dataStr = JSON.stringify(errObj['data']);
        } catch (e) {
          dataStr = 'data序列化失败';
        }
      }
      // 5. 使用 hilog.error 打印完整的错误信息
      // 注意:必须使用 %{public}s,否则真机日志会显示 <private>
      hilog.error(DOMAIN, TAG,
        `❌ CloudDB 操作失败 | 时间: %{public}s | code: %{public}s | message: %{public}s | data: %{public}s`,
        errorTime, code, message, dataStr
      );

      // 6. 同时保留弹窗,方便快速查看
      const dialogMsg = `时间: ${errorTime}\ncode: ${code}\nmessage: ${message}`;
      // 7. 抛出包含完整信息的 Error
      throw new Error(`[${errorTime}] code: ${code}, message: ${message}, data: ${dataStr}`);
    }
}

更多关于HarmonyOS鸿蒙Next中Account Kit怎样实现登出的实战教程也可以访问 https://www.itying.com/category-93-b0.html

9 回复

可以的,这里要把“登录”和“授权”分开看。

如果你的目标是重新弹出授权确认页,不建议只用 createLoginWithHuaweiIDRequest() 里的 forceLogin。forceLogin 主要影响“华为账号未登录时是否拉起登录页”,不等于每次都重新展示授权确认。

重新请求授权建议用 AuthorizationWithHuaweiIDRequest:

const provider = new authentication.HuaweiIDProvider();
const req = provider.createAuthorizationWithHuaweiIDRequest();
req.scopes = ['openid'];
req.permissions = ['idtoken', 'serviceauthcode'];
req.forceAuthorization = true;
req.state = util.generateRandomUUID();
req.idTokenSignAlgorithm = authentication.IdTokenSignAlgorithm.PS256;
const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());
const res = await controller.executeRequest(req) as authentication.AuthorizationWithHuaweiIDResponse;

这里 scopes/permissions 按你实际要取的数据来配:只要 OpenID/UnionID 关联用户,一般 openid 就够;如果服务端需要换取 token 或校验身份,再加 serviceauthcode、idtoken。不要把文档里所有项都堆进去,未申请或不支持的 scope/permission 可能报 1001502014 或 1001500003。

如果用户已经授权过同一组 scopes/permissions,仍想让他重新确认,可以先走 createCancelAuthorizationRequest() 取消本应用授权,再用上面的 AuthorizationWithHuaweiIDRequest + forceAuthorization 重新拉起。

更多关于HarmonyOS鸿蒙Next中Account Kit怎样实现登出的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


我拉起授权登录也是一样的,这是华为帐号登录的逻辑。

可以订阅到华为账号登出事件然后处理

import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError, commonEventManager } from '@kit.BasicServicesKit';
// 订阅者信息
const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
  events: [commonEventManager.Support.COMMON_EVENT_DISTRIBUTED_ACCOUNT_LOGIN,
    commonEventManager.Support.COMMON_EVENT_DISTRIBUTED_ACCOUNT_LOGOUT]
};
let subscriber: commonEventManager.CommonEventSubscriber;

// 创建订阅者
commonEventManager.createSubscriber(subscribeInfo)
  .then((commonEventSubscriber: commonEventManager.CommonEventSubscriber) => {
    subscriber = commonEventSubscriber;
    // 订阅公共事件
    commonEventManager.subscribe(subscriber,
      (error: BusinessError, data: commonEventManager.CommonEventData) => {
        if (error) {
          hilog.error(0x0000, 'testTag',
            `Failed to subscribe, code is ${error.code}, message is ${error.message}`);
        } else {
          hilog.info(0x0000, 'testTag', 'Succeeded in subscribing.');
          if (data.event === commonEventManager.Support.COMMON_EVENT_DISTRIBUTED_ACCOUNT_LOGIN) {
            // 订阅到华为账号登录事件
          }
          if (data.event === commonEventManager.Support.COMMON_EVENT_DISTRIBUTED_ACCOUNT_LOGOUT) {
            // 订阅到华为账号登出事件
          }
        }
      });
  })
  .catch((err: BusinessError) => {
    hilog.error(0x0000, 'testTag', `Failed to createSubscriber. Code: ${err.code}, message: ${err.message}`);
  });

可以参考一下官方demo

https://gitcode.com/HarmonyOS_Samples/accountkit-samplecode-clientdemo-arkts

cke_1189.png

cke_924.png

有要学HarmonyOS AI的同学吗,联系我:https://www.itying.com/goods-1206.html

这里要先区分“应用退出登录”和“退出设备上的华为账号”。createCancelAuthorizationRequest() 是取消当前应用的 Account Kit 授权,不等于把系统里的华为账号登出。

所以你看到的现象是可能的:本地状态清掉后回到登录页,但设备上的华为账号仍处于登录状态,下一次调用登录请求时仍可能直接返回登录结果,不一定每次都拉起授权页。

应用侧登出通常建议做三件事:清理本地业务登录态和用户缓存;通知服务端废弃自己的 session/refresh token;回到登录页。Account Kit 的 forceLogin 主要控制“华为账号未登录时是否拉起登录页”,不能保证每次都弹出授权确认界面。

如果你的目标是重新弹出授权确认,可以走授权请求 createAuthorizationWithHuaweiIDRequest(),设置所需 scopes/permissions,并按场景设置 forceAuthorization = true。如果目标是切换华为账号,则需要引导用户到系统账号侧切换,应用内的取消授权不能替用户退出系统账号。

感谢您的解答,我目标是重新弹出授权确认,但是不太懂怎么设置所需 scopes/permissions,可以麻烦您多指导一下吗,谢谢呀谢谢呀🤩

同问

你这个其实不是“登出失败”,而是:

Account Kit 做了单点登录缓存(SSO)

所以即使:

executeRequest(cancelRequest)

成功了,

系统里:

  • 华为账号仍然在线
  • 授权缓存仍然存在
  • 云真机环境尤其容易缓存

因此:

再次点击登录会直接静默登录

不会再弹授权页。


你现在这个行为:

第一次弹授权页
后面永远直接登录

其实是正常现象。

很多平台:

  • 华为 Account Kit
  • Google Sign In
  • Apple Sign In

都是这种逻辑。


关键点

CancelAuthorization
≠
退出系统华为账号

它只是:

撤销应用授权

但:

系统登录态还在

所以:

系统认为:

用户已经是可信用户

会直接返回 credential。


云真机尤其明显

云真机很多时候:

  • 账号环境固定
  • SSO缓存持久
  • 授权页不会频繁弹

所以:

云真机不适合验证“是否弹授权页”

真机表现通常更符合预期。


正确理解“登出”

你现在应该区分:

1. 应用层登出(推荐)

也就是:

clearLocalState()

清掉:

  • uid
  • token
  • 本地缓存
  • 用户状态

然后回登录页。

这个其实已经算:

完成登出

了。

很多应用实际上都这么干。


2. 强制重新授权(不一定能做到)

你期待的是:

每次登录都重新弹授权页

但:

Account Kit 默认不是这么设计的

它偏向:

静默登录体验

你现在代码其实没太大问题

你这里:

await controller.executeRequest(cancelRequest);

已经是官方标准登出方式。

问题不在代码。

而在:

系统SSO缓存

如果你真的想强制重新授权

目前只能尝试:

方法1(推荐)

在:

手机系统设置

里:

华为账号
-> 登录设备
-> 应用授权管理

手动撤销。


方法2

卸载应用重新安装。


方法3

换华为账号。


你现在真正推荐做法

其实是:

不要依赖:

“是否再次弹授权页”

来判断登录状态。

而是:

自己维护应用登录态

例如:

userState.isLogin

退出时:

clearLocalState()

即可。


你这个项目更适合这样

推荐:

首次登录:
拉授权
获取 unionId

后续:
直接静默登录

退出:
只清本地状态

这是目前最符合 Account Kit 的方式。


一句话总结

你现在不是代码有问题,而是:

Account Kit 默认支持SSO静默登录

即使:

cancelAuthorization

成功,

系统华为账号登录态仍然存在,

所以不会再次弹授权页。

目前:

“退出应用账号”

和:

“退出系统华为账号”

是两回事。

在HarmonyOS NEXT中,使用Account Kit实现登出:调用accountKit.logout()方法,传入当前用户ID。登出成功后清除本地token及用户缓存,并跳转至登录页。需在主线程执行,捕获异常。

你遇到的问题是因为 CancelAuthorizationRequest 只撤销了应用授权,但设备上的华为账号仍处于已登录状态,因此再次点击登录按钮时,SDK 直接使用已登录账号的缓存授权,不再拉起授权界面。

要实现“登出后重新拉起授权页”,需要做两件事之一:


方案一:登出时调用 signOut(推荐)

performLogout 中的取消授权请求改为 signOut,真正退出设备上的华为账号:

async performLogout() {
  const provider = new authentication.HuaweiIDProvider();
  // 调用 signOut 登出华为账号
  await provider.signOut();
  // 清理本地状态...
}

signOut 会让设备彻底退出华为账号,下次登录必然重新弹出授权页。


方案二:登录按钮强制授权

不修改登出逻辑,在 LoginWithHuaweiIDButtonparams 中增加 forceLogin: true

LoginWithHuaweiIDButton({
  params: {
    loginType: loginComponentManager.LoginType.ID,
    style: loginComponentManager.Style.BUTTON_CUSTOM,
    forceLogin: true,  // 每次点击都强制弹出授权页
    // ... 其他配置
  },
  controller: this.controller
})

但注意 forceLogin 可能在某些版本不生效,最佳实践仍是调用 signOut

回到顶部