一个看似简单的需求,却遇上了API的限制

最近在开发一个需要用户登录的HarmonyOS应用时,我遇到了一个看似简单但实际很棘手的问题。产品需求很明确:用户通过华为账号一键登录时,需要同时获取用户的匿名手机号和头像昵称信息。作为一个有经验的开发者,我想这应该就是配置两个scope权限的事——一个获取匿名手机号,一个获取头像昵称。但现实却给了我一个响亮的耳光。

问题现象

按照华为账号服务的文档,我写了如下代码:

// ❌ 错误示范:千万别这样写
const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
authRequest.scopes = ['quickLoginAnonymousPhone', 'profile']; // 同时申请匿名手机号和头像昵称

运行后,应用直接报错:错误码 1001500003,不支持该scopes或permissions

这让我十分困惑,明明两个scope都是官方文档中列出的,为什么不能同时使用?经过深入研究,我发现quickLoginAnonymousPhone这个scope有一个特殊的限制:它只能与openid同时使用,不能与其他scope(如profile)混用

问题分析

要理解这个限制,我们需要先了解华为账号服务中scope的设计理念:

  1. scope的权限隔离:华为账号服务对不同用户信息进行了严格的权限隔离。匿名手机号属于用户隐私级别较高的信息,而头像昵称属于公开的个人资料。这种隔离是为了保护用户隐私,防止应用一次性获取过多用户信息。

  2. quickLoginAnonymousPhone的特殊性:这个scope专门用于华为账号一键登录场景,它会返回一个经过匿名化处理的手机号(非真实手机号),主要用于用户标识。由于其特殊性,华为对其使用做了严格限制。

  3. API设计考虑:从API设计的角度,quickLoginAnonymousPhoneprofile属于不同的授权维度。一个用于登录标识,一个用于用户资料展示。将它们分开申请,也符合最小权限原则,让用户可以更清楚地知道应用在申请什么权限。

解决方案

既然不能同时申请,那该怎么办?华为官方文档给出了明确的解决方案:分两个独立的授权请求

方案一:分两次调用(推荐)

这是最稳妥的方法,虽然需要两次授权,但完全符合API规范,且用户体验影响不大。

import { authentication } from '@kit.AccountKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct LoginPage {
  // 第一步:获取头像和昵称
  async getProfileInfo(): Promise<void> {
    try {
      const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
      authRequest.scopes = ['profile']; // 只申请头像昵称
      authRequest.forceAuthorization = true; // 需要用户授权
      
      const controller = new authentication.AuthenticationController();
      const response = await controller.executeRequest(authRequest);
      
      const data = response.data!;
      const avatarUri = data.avatarUri; // 用户头像
      const nickName = data.nickName;   // 用户昵称
      
      hilog.info(0x0000, 'LoginPage', `获取到用户信息: 昵称=${nickName}, 头像=${avatarUri}`);
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(0x0000, 'LoginPage', `获取用户信息失败: ${err.code}, ${err.message}`);
    }
  }
  
  // 第二步:获取匿名手机号
  async getAnonymousPhone(): Promise<string | undefined> {
    try {
      const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
      authRequest.scopes = ['quickLoginAnonymousPhone']; // 只申请匿名手机号
      authRequest.forceAuthorization = false; // 一键登录场景必须为false
      
      const controller = new authentication.AuthenticationController();
      const response = await controller.executeRequest(authRequest);
      
      // 从extraInfo中获取匿名手机号
      const anonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string;
      hilog.info(0x0000, 'LoginPage', `获取到匿名手机号: ${anonymousPhone}`);
      
      return anonymousPhone;
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(0x0000, 'LoginPage', `获取匿名手机号失败: ${err.code}, ${err.message}`);
      return undefined;
    }
  }
  
  // 完整的登录流程
  async handleLogin() {
    // 先获取用户资料
    await this.getProfileInfo();
    
    // 再获取匿名手机号(用于用户标识)
    const phone = await this.getAnonymousPhone();
    
    if (phone) {
      // 使用匿名手机号作为用户标识
      this.loginWithPhone(phone);
    } else {
      hilog.warn(0x0000, 'LoginPage', '未获取到匿名手机号,使用其他登录方式');
    }
  }
  
  build() {
    Column() {
      Button('一键登录')
        .onClick(() => {
          this.handleLogin();
        })
    }
  }
}

方案二:优化用户体验的渐进式授权

如果觉得两次授权影响用户体验,可以采用渐进式授权策略:先完成一键登录,再在需要时申请其他权限。

@Component
struct ProgressiveAuthDemo {
  @State isLoggedIn: boolean = false;
  @State userAvatar: string = '';
  @State userName: string = '';
  
  // 首次登录:只获取匿名手机号
  async firstTimeLogin() {
    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
    authRequest.scopes = ['quickLoginAnonymousPhone'];
    authRequest.forceAuthorization = false;
    
    const controller = new authentication.AuthenticationController();
    const response = await controller.executeRequest(authRequest);
    const anonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string;
    
    if (anonymousPhone) {
      this.isLoggedIn = true;
      // 使用匿名手机号完成登录
      this.completeLogin(anonymousPhone);
      
      // 提示用户完善资料
      this.showProfileTips();
    }
  }
  
  // 用户主动完善资料时再申请头像昵称
  async requestProfile() {
    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
    authRequest.scopes = ['profile'];
    authRequest.forceAuthorization = true; // 需要用户明确授权
    
    const controller = new authentication.AuthenticationController();
    const response = await controller.executeRequest(authRequest);
    const data = response.data!;
    
    this.userAvatar = data.avatarUri;
    this.userName = data.nickName;
  }
  
  build() {
    Column() {
      if (!this.isLoggedIn) {
        Button('一键登录')
          .onClick(() => this.firstTimeLogin())
      } else {
        if (this.userAvatar) {
          // 显示用户头像和昵称
          Image(this.userAvatar)
            .width(50)
            .height(50)
            .borderRadius(25)
          Text(this.userName)
        } else {
          // 提示用户完善资料
          Button('完善资料')
            .onClick(() => this.requestProfile())
        }
      }
    }
  }
}

关键注意事项

1. 权限申请必须在开发前完成

// 开发前必须先在华为开发者平台申请以下权限:
// 1. quickLoginMobilePhone(华为账号一键登录)
// 2. profile(用户头像昵称)
// 否则会报错 1001502014:应用未申请scopes或permissions权限

2. forceAuthorization 参数的正确使用

// 获取头像昵称时需要用户授权
authRequest.scopes = ['profile'];
authRequest.forceAuthorization = true;  // ✅ 正确:会拉起授权页面

// 一键登录获取匿名手机号
authRequest.scopes = ['quickLoginAnonymousPhone'];
authRequest.forceAuthorization = false; // ✅ 正确:一键登录场景必须为false

3. 错误处理要完善

try {
  const response = await controller.executeRequest(authRequest);
  // 处理成功逻辑
} catch (error) {
  const err = error as BusinessError;
  
  // 常见错误码处理
  switch (err.code) {
    case 1001500003:
      hilog.error(0x0000, 'Login', '不支持的scope,请检查scope组合');
      break;
    case 1001502014:
      hilog.error(0x0000, 'Login', '应用未申请该权限,请先在开发者平台申请');
      break;
    case 1001502012:
      hilog.error(0x0000, 'Login', '用户取消了授权');
      break;
    default:
      hilog.error(0x0000, 'Login', `其他错误: ${err.code}, ${err.message}`);
  }
}

4. 用户标识的最佳实践

文档的FAQ中明确提到:推荐使用华为账号的UnionID/OpenID作为用户的主要标识,而不是手机号。原因如下:

// ✅ 推荐:使用UnionID/OpenID作为用户标识
const unionID = response.data?.unionID;  // 不会变化,即使换绑手机号
const openID = response.data?.openID;    // 不会变化,应用唯一

// ⚠️ 注意:匿名手机号可能变化
const anonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string;

常见FAQ解答

Q1: 为什么不能同时获取匿名手机号和头像昵称?

A1: 这是华为账号服务的安全设计。quickLoginAnonymousPhone用于一键登录标识,profile用于获取用户公开资料。两者权限级别不同,分开申请符合最小权限原则,更好地保护用户隐私。

Q2: 两次授权会影响用户体验吗?

A2: 合理的实现不会明显影响体验。建议采用渐进式授权:先完成一键登录,在用户进入个人中心等场景时,再申请头像昵称权限。用户通常能理解这种"需要时才申请"的设计。

Q3: 如果用户拒绝了头像昵称授权,还能使用应用吗?

A3: 可以。头像昵称是非必要信息,用户拒绝后,应用应能继续使用。可以提供默认头像和昵称,或在适当时候再次提示用户授权。

Q4: Access Token 和 Refresh Token 的有效期是多久?

A4: 根据华为官方文档:

  • Access Token 有效期为 1 小时

  • Refresh Token 有效期为 180 天

    应用需要妥善管理Token的刷新逻辑。

写在最后

这次踩坑经历让我深刻体会到,在对接第三方服务时,仔细阅读官方文档是多么重要。华为账号服务的这个限制,初看似乎不合理,但深入了解后才发现其背后的安全考量:

  1. 隐私保护:分开申请让用户更清楚应用在获取什么信息

  2. 最小权限:避免应用一次性获取过多不必要的信息

  3. 安全设计:不同级别的用户信息需要不同的授权流程

作为开发者,我们应该:

  • 充分理解API的设计意图

  • 遵循最佳实践,保护用户隐私

  • 设计优雅的降级方案,即使部分授权失败也不影响核心功能

最后分享一个实用建议:在开发涉及用户授权的功能时,一定要在真机上充分测试各种授权场景(同意、拒绝、取消),确保应用在各种情况下都能给用户提供清晰的反馈和流畅的体验。

技术虽有边界,但合理的架构和贴心的设计能让应用在边界内游刃有余。这次的经验也提醒我,在技术选型和方案设计时,提前了解第三方服务的限制,往往能避免后期的重构和调整。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐