Account Kit 华为账号服务介绍

Account Kit(华为账号服务)简介

场景介绍

Account Kit(华为账号服务)提供简单、快速、安全的登录功能,让用户快捷地使用华为账号登录应用。用户授权后,Account Kit可提供头像、昵称、手机号码等信息,帮助应用更了解用户。

能力范围

  • 登录:提供登录服务,让用户使用华为账号快速登录应用。
  • 获取华为账号用户信息:获取用户的基本开放信息,如头像、昵称、手机号、收货地址、发票抬头、风险等级。
  • 未成年人模式:获取未成年人模式的开启状态及年龄段信息以进行内容分级,调整未成年人相关设置时可增加家长验证,还可调用接口引导用户开启或关闭未成年人模式。

亮点/特征

一键登录

应用可以通过华为账号一键登录功能获取手机号授权并完成登录,帮助应用建立用户体系或者与现有用户体系对接。优点如下:

  • 便捷性:一键完成登录和手机号授权,为用户提供更加便捷易用的登录体验。
  • 全场景:Phone、Tablet、PC/2in1、TV设备登录体验一致,保障用户数据资产跨端延续。
  • 效率高:无需单独集成SDK,减少开发者开发和运营成本。
未成年人模式

应用可以通过未成年人模式的相关能力帮助家长快速开启未成年人模式,守护未成年人健康使用电子设备和应用。有以下优点:

  • 便捷性:统一管控未成年人模式入口,仅需一次设置,应用联动生效,避免各个应用内单独开启的繁琐操作,提升用户体验。
  • 全面守护:应用与系统联动,为孩子提供全面的守护措施,如仅允许访问适龄应用、增强隐私保护、限制设备使用时长等。

示例代码

Account Kit提供的SampleCode示例工程体现了Account Kit的华为账号一键登录、静默登录、获取头像昵称、获取手机号、收货地址、发票抬头、未成年人模式等特性,可参考该工程进行应用的相关内容开发。

约束与限制

Account Kit提供的能力 支持的设备类型
获取头像昵称 Phone、Tablet、PC/2in1、Wearable、TV
获取手机号 Phone、Tablet、PC/2in1、Wearable、TV
获取收货地址 Phone、Tablet、PC/2in1、TV
获取发票抬头 Phone、Tablet、PC/2in1
获取风险等级 Phone、Tablet、PC/2in1、Wearable、TV
获取实名年龄段 Phone、Tablet、PC/2in1、Wearable、TV
未成年人模式 Phone、Tablet、PC/2in1、TV
登录按钮组件 Phone、Tablet、PC/2in1、TV
登录面板组件 Phone、Tablet、PC/2in1、TV

支持的国家/地区

请参见支持的国家/地区。

模拟器支持情况

本Kit支持模拟器,但与真机存在部分能力差异,具体差异如下。

通用差异:请参见"模拟器与真机的差异"。

模拟器仅支持应用统一认证服务authentication的登录和授权能力、华为账号Button登录组件。

不支持Wearable设备模拟器。

开发者使用Account Kit的登录能力的管理细则

为了保护用户隐私信息,确保用户获得良好的登录体验,根据《华为开发者服务协议》、《华为APIs使用协议》、《应用审核指南》、《元服务审核指南》等相关协议条款及现行法律法规,平台制定了华为账号登录管理细则,使用华为账号登录的应用请遵照执行,具体要求如下:

上架审核要求

为了帮助用户省去多次输入不同应用账号登录的繁琐过程,我们为HarmonyOS应用和元服务开发者提供了使用华为账号快捷登录的能力。提交至华为应用市场的HarmonyOS应用和元服务,需要遵循如下规范:

(1)如果应用支持用户使用第三方账号登录,则该应用需提供华为账号登录选项。用户可通过华为账号快捷地登录您的应用,并获得华为账号的安全配置。

(2)如果元服务内需要构建账号体系时,必须使用华为账号登录能力。

登录规范

华为账号提供登录设计规范,保障HarmonyOS应用拥有简单易用、高效一致、快速安全的登录体验。帮助用户使用已有的华为账号登录所有的应用。有关设计规范,请参阅华为账号开放登录。

附则

本细则是一份动态更新的文档,我们会根据相关法律法规的变化以及行业发展,不定期对细则内容进行修改或更新,请您持续关注本细则,以便获得最新信息。

开发准备

配置签名和指纹

请参考"应用开发准备"章节,完成以下操作步骤:

创建项目和工程(如已完成,请跳过此步骤)。

配置签名信息。针对开发调试场景,从DevEco Studio 26.0.0 Beta2版本开始,新增了更高效的自动签名方案,开发者可以选择以下其中一种方式进行调试阶段的应用签名。

自动签名:

应用运行的HarmonyOS系统版本低于HarmonyOS 6.0.0(20)时,仅未成年人模式接口支持自动签名。

应用运行的HarmonyOS系统版本为HarmonyOS 6.0.0(20)及以上时,所有接口均支持使用自动签名方式进行配置。

手动签名:

所有接口均支持使用手动签名方式配置签名。

添加公钥指纹。

注意

发布阶段,请参考发布应用,重新配置用于应用发布的签名信息、添加公钥指纹(必选)。

检查是否需要配置公钥指纹:应用仅接入未成年人模式或compatibleSdkVersion>=20不需要配置公钥指纹,其他场景均需配置。

检查公钥指纹是否配置成功:请在开发与服务中选择对应的项目和应用,检查是否已成功配置该应用的公钥指纹。

公钥指纹最迟会在25小时后生效。

(可选) 配置公钥指纹10分钟后,您可通过修改应用工程中app.json5配置文件的versionCode触发公钥指纹生效。

图1 修改前

图2 修改后

配置Client ID

获取Client ID和APP ID

在 AppGallery Connect(简称AGC)的开发与服务中,选择对应的项目和对应的应用,在"常规 > 应用 "下,找到应用的Client ID和APP ID。

确认是否需要配置Client ID

如果上一步获取到的Client ID和APP ID相同,则无需配置Client ID,否则需要按下一步配置Client ID。

配置Client ID

在工程中entry模块的module.json5文件中,新增metadata,配置name为client_id,value为上一步获取的Client ID的值,如下所示:

说明

1.若工程中存在多个模块,需要在"type"为"entry"模块中的module.json5文件配置应用的Client ID。

2.请确认获取的Client ID是应用Client ID,错配成项目Client ID将导致接口调用报错。

json 复制代码
"module": {
  "name": "entry",
  "type": "entry",
  "description": "<description>",
  "mainElement": "<mainElement>",
  "deviceTypes": [
  ],
  // ...
  "pages": "$profile:main_pages",
  // ...
  "metadata": [
    // 配置信息如下
    // ...
    {
      "name": "client_id",
      // 将上一步获取到的Client ID赋值给value,请注意不要使用其他方式设置value值
      "value": "xxxxx"
    }
  ]
}

申请账号权限

请参考"应用开发准备"章节,创建应用、使用DevEco Studio创建应用工程。

说明

如需申请华为账号一键登录、获取您的手机号、获取收货地址权限,则需按照以下步骤完成权限申请,否则可跳过本章节。

申请前自检

申请权限前请参考表1,了解账号权限支持的能力和使用条件,并根据表2、表3、表4完成自检,确认您的应用类型、设备类型、开发者类型等是否符合申请条件,不符合条件的申请将被驳回。

说明

华为账号一键登录、获取您的手机号、获取收货地址权限,仅支持企业开发者申请,不支持个人开发者申请。个人开发者可使用华为账号登录或静默登录实现登录。

表1 账号权限说明

权限名称 权限描述 支持的应用类型 支持的开发者类型 支持的设备类型
华为账号一键登录 支持应用获取用户的Union ID、Open ID和华为账号绑定的手机号。 非游戏类应用、非银行类应用 企业开发者 Phone、Tablet、PC/2in1、TV
获取您的手机号 支持应用获取华为账号绑定的手机号或用户选择的其他手机号。 游戏类应用 企业开发者 Phone、Tablet、PC/2in1、Wearable、TV
获取收货地址 支持应用获取用户的地址,地址可以用做收货或者发货。 无限制 企业开发者 Phone、Tablet、PC/2in1、TV
获取您的年龄段 支持应用获取用户的实名年龄段信息 游戏类应用 企业开发者 Phone、Tablet、PC/2in1、Wearable、TV

表2 华为账号一键登录权限申请自检表

序号 自检项内容
1 开发者必须为企业开发者。
2 申请权限的应用类型必须为HarmonyOS NEXT应用(HarmonyOS应用不予审批)。
3 游戏类、银行类应用暂不开放。
4 应用是否上架应用市场,如不上架需要说明原因。
5 若应用近期存在违规记录,则不予审批或有权收回权限。
6 若用户举报或发现开发者不合理的使用,华为有权收回权限。

表3 获取您的手机号权限申请自检表

序号 自检项内容
1 开发者必须为企业开发者。
2 仅支持游戏类应用申请,其他类型应用暂不开放。
3 应用是否上架应用市场,如不上架需要说明原因。
4 若应用近期存在违规记录,则不予审批或有权收回权限。
5 若用户举报或发现开发者不合理的使用,华为有权收回权限。

表4 获取收货地址权限申请自检表

序号 自检项内容
1 开发者必须为企业开发者。
2 应用是否上架应用市场,如不上架需要说明原因。
3 若应用近期存在违规记录,则不予审批或有权收回权限。
4 若用户举报或发现开发者不合理的使用,华为有权收回权限。

表5 获取您的年龄段权限申请自检表

序号 自检项内容
1 开发者必须为企业开发者。
2 仅支持游戏类应用申请,其他类型应用暂不开放。
3 应用是否上架应用市场,如不上架需要说明原因。
4 若应用近期存在违规记录,则不予审批或有权收回权限。
5 若用户举报或发现开发者不合理的使用,华为有权收回权限。
申请步骤

在 AppGallery Connect(简称AGC)的开发与服务中,选择相应的项目,然后选择需要申请对应权限的HarmonyOS应用。

在"开放能力管理"中,选择想要申请的账号权限,并点击"申请"。

说明

权限申请入口目前仅对企业开发者开放,个人开发者不可见。

图示仅为示例,不同应用类型展示不同权限,请以实际页面显示为准。

点击申请后,请根据应用实际情况填写"申请原因"。

说明

申请原因填写模板:

应用介绍:说明应用类型,例如XXX应用属于XXX类型的应用。

使用场景:说明权限使用场景,例如在XXX场景下,需要使用XX能力。使用场景参见表5 使用场景类型,若为其他场景,请按实际类型填写。

申请用途:描述该权限的用途,例如用户使用XXX功能后,进行XXX操作,提供XXX服务。

表5 使用场景类型

使用场景类型 业务场景描述
网络约车类 基本功能服务为"网络预约出租汽车服务、巡游出租汽车电召服务"。
即时通信类 基本功能服务为"提供文字、图片、语音、视频等网络即时通信服务"。
网络社区类 基本功能服务为"博客、论坛、社区等话题讨论、信息分享和关注互动"。
网络支付类 基本功能服务为"网络支付、提现、转账等功能" 。
网上购物类 基本功能服务为"购买商品"。
餐饮外卖类 基本功能服务为"餐饮购买及外送"。
邮件快件寄递类 基本功能服务为"信件、包裹、印刷品等物品寄递服务"。
交通票务类 基本功能服务为"交通相关的票务服务及行程管理(如票务购买、改签、退票、行程管理等)"。
婚恋相亲类 基本功能服务为"婚恋相亲"。
求职招聘类 基本功能服务为"求职招聘信息交换"。
网络借贷类 基本功能服务为"通过互联网平台实现的用于消费、日常生产经营周转等的个人申贷服务"。
房屋租售类 基本功能服务为"个人房源信息发布、房屋出租或买卖"。
二手车交易类 基本功能服务为"二手车买卖信息交换"。
问诊挂号类 基本功能服务为"在线咨询问诊、预约挂号"。
旅游服务类 基本功能服务为"旅游服务产品信息的发布与订购"。
酒店服务类 基本功能服务为"酒店预订"。
网络游戏类 基本功能服务为"提供网络游戏产品和服务"。
学习教育类 基本功能服务为"在线辅导、网络课堂等"。
本地生活类 基本功能服务为"家政维修、家居装修、二手闲置物品交易等日常生活服务"。
用车服务类 基本功能服务为"共享单车、共享汽车、租赁汽车等服务"。
投资理财类 基本功能服务为"股票、期货、基金、债券等相关投资理财服务"。
手机银行类 基本功能服务为"通过手机等移动智能终端设备进行银行账户管理、信息查询、转账汇款等服务"。
邮箱云盘类 基本功能服务为"邮箱、云盘等"。
远程会议类 基本功能服务为"通过网络提供音频或视频会议"。
演出票务类 基本功能服务为"演出购票"。

提交申请成功后,自动跳转到互动中心,提示等待审核。

说明

3个工作日内审核结果会通过站内消息的形式发送到互动中心,请注意查收。

权限申请通过后最迟在25小时后生效。

(可选) 您可通过修改应用工程 > app.json5中的versionCode触发权限生效。

图1 修改前

图2 修改后

登录

概述

Account Kit提供了华为账号一键登录、华为账号登录、静默登录等多种登录方式,其中华为账号一键登录仅支持企业开发者使用,华为账号登录和静默登录既支持企业开发者也支持个人开发者使用,应用可根据实际场景选择使用其中一种或多种方式进行账号登录。

基础概念

华为账号用户身份标识包含UnionID和OpenID,具体格式要求请参考OpenID和UnionID的格式说明,注意OpenID和UnionID严格区分大小写。两者的定义与使用场景:

项目/ID类型 UnionID OpenID
定义 UnionID是华为账号用户同一开发者账号下的唯一标识。开发者有多个HarmonyOS应用时,同一个开发者账号下的HarmonyOS应用获取到用户的UnionID相同。 OpenID是华为账号用户在HarmonyOS应用的唯一标识。不同HarmonyOS应用(不管是否在同一个开发者账号下)获取到用户的OpenID不同。
使用场景 在同一个开发者账号下标识用户的唯一性。建议使用UnionID。 在同一个应用下标识用户的唯一性。

说明

在开发HarmonyOS应用时,您需要考虑同一用户在非HarmonyOS应用和HarmonyOS应用的用户数据是否互通。如果您之前使用OpenID来关联用户数据,我们建议将用户数据关系切换成UnionID,以确保用户使用HarmonyOS应用后可以继承老版本的用户数据。具体切换指导可以参考:通过OpenID获取UnionID。

场景介绍

获取手机号和UnionID登录,即华为账号一键登录

若应用需要同时获取手机号和UnionID,推荐使用此场景,用户仅需一次点击操作,应用即可获取用户手机号和UnionID。应用获取到用户手机号和UnionID后,可同时通过手机号和UnionID与应用原有用户体系进行关联。本场景仅支持企业开发者使用,个人开发者请使用华为账号登录或静默登录方式。

华为账号登录

若应用只需要获取UnionID可以使用此场景。应用获取到用户UnionID后,可通过UnionID与应用原有用户体系进行关联。

静默登录

在应用卸载重装、用户换机等场景,应用可通过Account Kit提供的静默登录方式即不需要用户点击登录/注册按钮,即可获取用户的身份标识UnionID,完成用户的静默登录。

订阅华为账号登录/登出事件

当应用需要跟随华为账号的登录状态进行登录登出时,可以通过订阅华为账号的登录登出事件进行判断,参考华为账号登录/登出事件。

华为账号一键登录(获取手机号和UnionID/OpenID)

概述

华为账号一键登录是基于OAuth 2.0协议标准和OpenID Connect协议标准构建的OAuth 2.0授权登录系统,应用可以通过华为账号一键登录能力快捷地获取华为账号用户的身份标识和手机号,快速建立应用内的用户体系。

优势:

利用系统账号的安全性和便利性,用户无需输入账号名和密码,无需复杂的安全验证,简化登录步骤,提高用户转化率。

提供系统验证过的手机号,关联应用已有用户。

实现Phone、Tablet、PC/2in1、TV设备一致的登录体验。

场景介绍

若应用需同时获取手机号和UnionID完成用户登录,Account Kit提供了同时获取手机号和UnionID的华为账号一键登录按钮。应用可以将华为账号一键登录按钮嵌入自有的登录页,使用登录按钮获取手机号和UnionID,实现用户登录。设备登录华为账号(该账号已绑定手机号)后,一键登录获取手机号可不依赖设备插SIM卡。

说明

儿童账号一键登录场景:

用户使用儿童账号进行登录,点击一键登录会触发Account Kit默认提供的家长验密流程(Account Kit提供的验证页,暂不可自定义),家长验密完成后可获取用户的身份标识和手机号。并且TV设备暂不支持儿童账号。

手机号验证机制说明:

Account Kit调用系统能力获取华为账号登录设备上的SIM卡手机号码,与华为账号绑定的手机号进行校验(有网络即可,无需使用SIM卡移动数据)。用户点击一键登录按钮后,结合华为账号使用过程中账号所绑定的手机号短信验证记录,90天内有验证通过的记录,则返回该华为账号绑定的手机号;若90天内没有验证通过的记录,则触发Account Kit默认提供的短信验证流程(Account Kit提供的验证页,暂不可自定义),确保返回的手机号经过验证。

约束与限制

应用满足《常见类型移动互联网应用程序必要个人信息范围规定》中使用手机号的必要业务场景。

使用华为账号一键登录功能用户必须同意《华为账号用户认证协议》,当用户点击《华为账号用户认证协议》,系统浅色模式下应用需跳转到如下链接https://privacy.consumer.huawei.com/legal/id/authentication-terms.htm?code=CN\&language=zh-CN,系统深色模式下跳转到https://privacy.consumer.huawei.com/legal/id/authentication-terms.htm?code=CN\&language=zh-CN\&bgmode=black。

应用在用户同意后获取到手机号,需要根据自身业务场景判断使用的方式,必要时增加其他安全验证手段,比如对二次放号的判断。

华为账号一键登录服务当前仅限中国境内(香港特别行政区、澳门特别行政区、中国台湾除外)用户可用。

华为账号一键登录支持Phone、Tablet、PC/2in1设备。并且从5.1.1(19)版本开始,新增支持TV设备。

仅支持企业开发者使用一键登录,个人开发者请使用华为账号登录或静默登录实现登录。

用户体验设计

登录页面UX设计规范

一键登录按钮的用户体验和UX设计需符合【华为账号一键登录】按钮规范,用户体验设计图2中的华为标志按钮可参考华为账号登录视觉规范中的样式三。不符合规范的UX设计可能会对应用上架和用户体验带来影响。一键登录按钮的样式设计具体可以参考华为账号登录按钮类型。

用户场景设计

用户使用华为账号一键登录能力,注册/登录应用时,可能存在多种场景,应用可参照以下流程,根据自身业务场景进行设计。

说明

将UnionID/OpenID和手机号同时与应用账号建立关联,可以为用户带来更多便利的功能。如:实现静默登录、获取华为账号用户信息、获取华为账号风险等级等。实现免用户操作登录,获得安全快捷的应用登录体验。

业务流程

用户首次登录应用

若应用未接入过华为账号登录,不存在使用华为账号登录过的应用账号,请参照以下流程接入华为账号一键登录。

图1 华为账号一键登录(用户首次登录应用)流程图

流程说明:

预取号阶段(序号1-4):

用户打开应用后,应用scope传quickLoginAnonymousPhone调用AuthorizationWithHuaweiIDRequest授权请求获取匿名手机号。如果获取到匿名手机号为空,应用需要展示其他登录方式。

说明

获取匿名手机号时建议设置超时处理,推荐设置5秒以保证用户体验。

若华为账号未登录,调用AuthorizationWithHuaweiIDRequest授权请求会返回1001502001 用户未登录华为账号错误码,此时应用需要展示其他登录方式进行应用登录。

展示一键登录页面阶段(序号5):

获取到的匿名手机号需要展示在页面上并设置好隐私协议,设置登录按钮类型为LoginType.QUICK_LOGIN,展示包含LoginWithHuaweiIDButton组件的一键登录页面。应用可结合实际登录风控场景,通过组件参数传入风险等级标识以获取华为账号风险等级,从而对恶意账号进行风控,提升应用的安全等级。

点击一键登录关联用户账号阶段(序号6-16):

用户同意协议后,点击华为账号一键登录按钮,应用可以通过HuaweiIDCredential获取到Authorization Code等数据。

将获取的Authorization Code数据传给应用服务端,应用服务端通过Authorization Code调用/oauth2/v6/quickLogin/getPhoneNumber接口获取用户完整手机号和UnionID、OpenID。

应用通过关联用户手机号和UnionID、OpenID完成用户登录。

用户非首次登录应用(可选)

应用接入过华为账号登录,存在使用华为账号登录过的用户账号,即根据UnionID/OpenID判断用户已关联过应用系统数据库,则需要参照以下流程开发。

图2 华为账号一键登录(用户非首次登录应用)流程图

流程说明:

应用调用AuthorizationWithHuaweiIDRequest授权请求获取AuthorizationWithHuaweiIDResponse响应结果中的Authorization Code。

应用服务端通过Authorization Code调用/oauth2/v6/quickLogin/getPhoneNumber接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。

应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,结合风控、安全因素及自身业务场景判断,可展示已关联的账号,由用户选择是否使用华为账号登录应用,或免用户操作,静默登录应用。

接口说明

华为账号一键登录按钮关键接口如下表所示:

接口名 描述
createAuthorizationWithHuaweiIDRequest(): AuthorizationWithHuaweiIDRequest 获取授权接口,通过AuthorizationWithHuaweiIDRequest传入一键登录的scope:quickLoginAnonymousPhone,即可在授权结果中获取到用户的匿名手机号和Authorization Code。
constructor(context?: common.Context) 创建授权请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行授权操作。
LoginWithHuaweiIDButton 华为账号Button登录组件。该组件仅纯文本样式支持华为账号一键登录功能。开发者可以通过调整按钮的大小、圆角等参数以适配HarmonyOS应用登录界面。如果仍然不能满足开发者的诉求,可以使用Style的BUTTON_CUSTOM值定义按钮的文字颜色和背景色。
onClickLoginWithHuaweiIDButton(callback: AsyncCallback): LoginWithHuaweiIDButtonController 注册华为账号一键登录按钮的结果回调。
setAgreementStatus(agreementStatus: AgreementStatus): LoginWithHuaweiIDButtonController 设置协议状态方法。用户未同意协议前设置协议状态为NOT_ACCEPTED,用户同意协议后设置协议状态为ACCEPTED,才可以完成华为账号登录。
onClickEvent(callback: AsyncCallback): LoginWithHuaweiIDButtonController 注册华为账号一键登录按钮的点击事件回调。
continueLogin(callback: AsyncCallback): LoginWithHuaweiIDButtonController 用户点击协议弹框的同意并登录按钮结果回调。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请先确认已完成开发准备工作。

若未配置签名和指纹,将报错1001500001 应用指纹证书校验失败。

若未申请"华为账号一键登录"权限,将报错1001502014 应用未申请scopes或permissions权限。

若应用开启了代码混淆,应用工程代码中获取到的quickLoginAnonymousPhone(匿名手机号)属性需要配置混淆白名单防止编译release包时被混淆,否则无法获取到匿名手机号。在调用获取匿名手机号方法工程模块的混淆文件obfuscation-rules.txt中添加:

text 复制代码
# 开发者开启属性混淆需要配置quickLoginAnonymousPhone属性白名单防止其被混淆
-enable-property-obfuscation
-keep-property-name
quickLoginAnonymousPhone
客户端开发

开发者可参考下述内容自行开发,也可使用Account Kit为常见的三方开发框架(Flutter、H5、React-Native、uni-app)提供的SampleCode示例工程,用于接入华为账号一键登录能力,具体可参考三方开发框架接入华为账号一键登录进行开发。

用户首次登录应用

导入模块。

导入Account Kit的authentication模块及相关公共模块。

typescript 复制代码
import { authentication } from '@kit.AccountKit';
import { util } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

获取匿名手机号。

调用authentication模块的AuthorizationWithHuaweiIDRequest请求获取华为账号用户的匿名手机号。匿名手机号用于登录页面展示。

注意

该场景下forceAuthorization参数需设置为false。

根据获取的响应结果判断,可能存在以下场景:

1)返回ArkTS错误码,开发者可参考下表针对不同错误码进行处理:

表1 获取匿名手机号错误码处理

错误码 错误描述 处理建议
1001502001 用户未登录华为账号 应用展示其他登录方式
1001502005 网络错误 提示用户检查当前网络状态后重试
1001502009 内部错误 应用展示其他登录方式
1001502014 应用未申请scopes或permissions权限 请参考1001502014 应用未申请scopes或permissions权限的可能原因和解决方法解决该报错
1001500001 应用指纹证书校验失败 请参考1001500001 应用指纹证书校验失败的可能原因和解决办法解决该报错
1001500002 重复请求 重复请求,应用无需处理
1001500003 不支持该scopes或permissions 1. 华为账号用户注册地可能为中国境外、香港特别行政区、澳门特别行政区或中国台湾,应用展示其他登录方式 2. 5.1.1(19)起支持TV设备,其他版本应用可以通过华为账号登录进行登录
12300001 系统服务异常 应用展示其他登录方式

2)获取到的匿名手机号为空,说明华为账号没有绑定手机号、权限未申请或未生效,上述异常场景应用需要展示其他登录方式。

3)若开发者开启了代码混淆,需将quickLoginAnonymousPhone(匿名手机号)属性加入混淆白名单,防止其被混淆。

typescript 复制代码
  async getQuickLoginAnonymousPhone(): Promise<string> {
    // 创建授权请求,并设置参数
    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
    // 获取匿名手机号需传quickLoginAnonymousPhone这个scope,传参之前需要先申请"华为账号一键登录"权限,否则会返回1001502014错误码
    authRequest.scopes = ['quickLoginAnonymousPhone'];
    // 建议使用generateRandomUUID生成state,可用于一致性比对,防止跨站攻击
    authRequest.state = util.generateRandomUUID();
    // 一键登录场景该参数必须设置为false
    authRequest.forceAuthorization = false;
    const controller = new authentication.AuthenticationController();
    let quickLoginAnonymousPhone: string = '';
    // ...
    try {
      await controller.executeRequest(authRequest)
        .then((response: authentication.AuthorizationWithHuaweiIDResponse) => {
          // 获取到匿名手机号
          quickLoginAnonymousPhone = response.data?.extraInfo?.quickLoginAnonymousPhone as string;
          // ...
          if (quickLoginAnonymousPhone) {
            hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');
            // ...
            return quickLoginAnonymousPhone;
          }
          hilog.info(0x0000, 'testTag', 'Succeeded in authentication. AnonymousPhone is empty.');
          // ...
          // 未获取到匿名手机号,应用需要跳转到其他方式登录页面
          return quickLoginAnonymousPhone;
        })
        .catch((error: BusinessError) => {
          this.dealAllError(error);
        })
      return quickLoginAnonymousPhone;
    } catch (error) {
      this.dealAllError(error);
      return quickLoginAnonymousPhone;
    }
  }

  // 错误处理
  dealAllError(error: BusinessError): void {
    hilog.error(0x0000, 'testTag',
      `Failed to get quickLoginAnonymousPhone, errorCode is ${error.code}, errorMessage is ${error.message}`);
    // 在应用登录涉及UI交互场景下,建议按照如下错误码指导提示用户
    if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
      // 华为账号未登录,应用需要展示其他登录方式
    } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
      // 网络错误,请检查当前网络状态并重试
    } else if (error.code === ErrorCode.ERROR_CODE_INTERNAL_ERROR) {
      // 登录失败,应用需要展示其他登录方式
    } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
      // 系统服务异常,应用需要展示其他登录方式
    } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
      // 重复请求,应用无需处理
    } else {
      // 应用登录失败,应用需要展示其他登录方式
    }
  }
  // ...
  export enum ErrorCode {
    // 账号未登录
    ERROR_CODE_LOGIN_OUT = 1001502001,
    // 网络错误
    ERROR_CODE_NETWORK_ERROR = 1001502005,
    // 内部错误
    ERROR_CODE_INTERNAL_ERROR = 1001502009,
    // 系统服务异常
    ERROR_CODE_SYSTEM_SERVICE = 12300001,
    // 重复请求
    ERROR_CODE_REQUEST_REFUSE = 1001500002
  }

展示一键登录页面并获取Authorization Code

将获取到的匿名手机号设置给下面QuickLoginButtonComponent组件示例代码中的quickLoginAnonymousPhone变量,调用LoginWithHuaweiIDButton组件,实现应用自己的登录页面,并展示华为账号一键登录按钮和华为账号用户认证协议(Account Kit提供跳转链接,应用需实现协议跳转,参见约束与限制第2点),用户同意协议并点击一键登录按钮后,可获取到Authorization Code,将该值传给应用服务端用于获取用户信息(完整手机号、UnionID、OpenID)。通过code凭证获取用户信息可以有效避免因数据遍历、身份伪造、重放攻击导致的安全风险。

typescript 复制代码
import { loginComponentManager, LoginWithHuaweiIDButton } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { connection } from '@kit.NetworkKit';

@Component
struct QuickLoginComponent {
  // 第二步获取的匿名手机号传到此处
  @State quickLoginAnonymousPhone: string = '';

  build() {
    if (this.quickLoginAnonymousPhone) {
      QuickLoginButtonComponent({
        quickLoginAnonymousPhone: this.quickLoginAnonymousPhone
      })
    } else {
      // 授权获取匿名手机号为空时,请应用自行实现其他方式登录页面
    }
  }
}

@Component
struct QuickLoginButtonComponent {
  logTag: string = 'QuickLoginButtonComponent';
  domainId: number = 0x0000;
  @State quickLoginAnonymousPhone: string = '';
  // 是否勾选协议
  @State isSelected: boolean = false;
  // 华为账号用户认证协议链接,此处仅为示例,实际开发过程中,出于可维护性、安全性等方面考虑,域名不建议硬编码在本地
  private static USER_AUTHENTICATION_PROTOCOL: string =
    'https://privacy.consumer.huawei.com/legal/id/authentication-terms.htm?code=CN&language=zh-CN';
  private static USER_SERVICE_TAG = '用户服务协议';
  private static PRIVACY_TAG = '隐私协议';
  private static USER_AUTHENTICATION_TAG = '华为账号用户认证协议';
  // 定义LoginWithHuaweiIDButton展示的隐私文本,展示应用的用户服务协议、隐私协议和华为账号用户认证协议
  privacyText: loginComponentManager.PrivacyText[] = [{
    text: '已阅读并同意',
    type: loginComponentManager.TextType.PLAIN_TEXT
  }, {
    text: '《用户服务协议》',
    tag: QuickLoginButtonComponent.USER_SERVICE_TAG,
    type: loginComponentManager.TextType.RICH_TEXT
  }, {
    text: '《隐私协议》',
    tag: QuickLoginButtonComponent.PRIVACY_TAG,
    type: loginComponentManager.TextType.RICH_TEXT
  }, {
    text: '和',
    type: loginComponentManager.TextType.PLAIN_TEXT
  }, {
    text: '《华为账号用户认证协议》',
    tag: QuickLoginButtonComponent.USER_AUTHENTICATION_TAG,
    type: loginComponentManager.TextType.RICH_TEXT
  }, {
    text: '。',
    type: loginComponentManager.TextType.PLAIN_TEXT
  }];
  // 构造LoginWithHuaweiIDButton组件的控制器
  controller: loginComponentManager.LoginWithHuaweiIDButtonController =
    new loginComponentManager.LoginWithHuaweiIDButtonController()
      /**
       * 当应用使用自定义的登录页时,如果用户未同意协议,需要设置协议状态为NOT_ACCEPTED,当用户同意协议后再设置
       * 协议状态为ACCEPTED,才可以使用华为账号一键登录功能
       */
      .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED)
      .onClickLoginWithHuaweiIDButton((error: BusinessError | undefined,
        response: loginComponentManager.HuaweiIDCredential) => {
        this.handleLoginWithHuaweiIDButton(error, response);
      })
      .onClickEvent((error: BusinessError, clickEvent: loginComponentManager.ClickEvent) => {
        if (error) {
          hilog.error(this.domainId, this.logTag,
            `onClickEvent error. errCode is ${error.code}, errMessage is ${error.message}`);
          return;
        }
        hilog.info(this.domainId, this.logTag, `onClickEvent clickEvent: ${clickEvent}`);
        // 设置按钮为不可点击态,待业务逻辑处理完成后,再设置为可点击态
        this.controller.setEnabled(false);
      });
  agreementDialog: CustomDialogController = new CustomDialogController({
    builder: AgreementDialog({
      privacyText: this.privacyText,
      cancel: () => {
        this.agreementDialog.close();
        this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED);
      },
      confirm: () => {
        this.agreementDialog.close();
        this.isSelected = true;
        this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED);
        // 调用此方法,同意协议与登录一并完成,无需再次点击登录按钮
        this.controller.continueLogin((error: BusinessError) => {
          if (error) {
            hilog.error(this.domainId, this.logTag,
              `Failed to login with agreementDialog. errCode is ${error.code}, errMessage is ${error.message}`);
          } else {
            hilog.info(this.domainId, this.logTag,
              'Succeeded in clicking agreementDialog continueLogin.');
          }
        });
      },
      clickHyperlinkText: () => {
        this.agreementDialog.close();
        this.jumpToPrivacyWebView();
      }
    }),
    autoCancel: false,
    alignment: DialogAlignment.Center
  });

  // Toast提示
  showToast(resource: string) {
    try {
      this.getUIContext().getPromptAction().showToast({
        message: resource,
        duration: 2000
      });
    } catch (error) {
      const message = (error as BusinessError).message;
      const code = (error as BusinessError).code;
      hilog.error(this.domainId, this.logTag, `showToast args  errCode is ${code}, errMessage is ${message}`);
    }
  }

  // 跳转华为账号用户认证协议页,该页面需在工程main_pages.json文件配置
  jumpToPrivacyWebView() {
    try {
      // 需在module.json5中配置"ohos.permission.GET_NETWORK_INFO"权限
      const checkNetConn = connection.hasDefaultNetSync();
      if (!checkNetConn) {
        this.showToast('服务或网络异常,请稍后重试');
        return;
      }
    } catch (error) {
      const message = error.message as string;
      const code = error.code as string;
      hilog.error(0x0000, 'testTag', `Failed to hasDefaultNetSync, errCode is ${code}, errMessage is ${message}`);
    }
    this.getUIContext().getRouter().pushUrl({
      // 需在module.json5配置"ohos.permission.INTERNET"网络权限
      url: 'pages/WebPage',
      params: {
        isFromDialog: true,
        url: QuickLoginButtonComponent.USER_AUTHENTICATION_PROTOCOL
      }
    }, (err) => {
      if (err) {
        hilog.error(this.domainId, this.logTag,
          `Failed to jumpToPrivacyWebView, errCode is ${err.code}, errMessage is ${err.message}`);
      }
    });
  }

  handleLoginWithHuaweiIDButton(error: BusinessError | undefined,
    response: loginComponentManager.HuaweiIDCredential) {
    if (error) {
      hilog.error(this.domainId, this.logTag,
        `Failed to login with LoginWithHuaweiIDButton. errCode is ${error.code}, errMessage is ${error.message}`);
      if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
        this.getUIContext().showAlertDialog(
          {
            message: '网络未连接,请检查网络设置。',
            offset: { dx: 0, dy: -12 },
            alignment: DialogAlignment.Bottom,
            autoCancel: false,
            confirm: {
              value: '知道了',
              action: () => {
                // 用户点击"知道了"按钮,可于此处补充业务逻辑
              }
            }
          }
        );
      } else if (error.code === ErrorCode.ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED) {
        // 未同意协议,弹出协议弹框,推荐使用该回调方式
        this.agreementDialog.open();
      } else if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
        // 华为账号未登录提示
        this.showToast('华为账号未登录,请重试');
      } else if (error.code === ErrorCode.ERROR_CODE_NOT_SUPPORTED) {
        // 不支持该scopes或permissions提示
        this.showToast('该scopes或permissions不支持');
      } else if (error.code === ErrorCode.ERROR_CODE_PARAMETER_ERROR) {
        // 参数错误提示
        this.showToast('参数错误');
      } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
        // 用户取消,无需特别处理
      } else {
        // 其他提示系统或服务异常
        this.showToast('服务或网络异常,请稍后重试');
      }
      this.controller.setEnabled(true);
      return;
    }
    try {
      if (this.isSelected) {
        if (response) {
          hilog.info(this.domainId, this.logTag, 'Succeeded in clicking LoginWithHuaweiIDButton.');
          // 开发者根据实际业务情况使用以下信息
          const authCode = response.authorizationCode;
        }
      } else {
        this.agreementDialog.open();
      }
    } catch (err) {
      hilog.error(this.domainId, this.logTag,
        `Failed to login with LoginWithHuaweiIDButton, errCode: ${err.code}, errMessage: ${err.message}`);
      this.getUIContext().showAlertDialog(
        {
          message: '服务或网络异常,请稍后重试',
          offset: { dx: 0, dy: -12 },
          alignment: DialogAlignment.Bottom,
          autoCancel: false,
          confirm: {
            value: '知道了',
            action: () => {
              // 用户点击"知道了"按钮,可于此处补充业务逻辑
            }
          }
        }
      );
    } finally {
      this.controller.setEnabled(true);
    }
  }

  build() {
    Scroll() {
      Column() {
        Column() {
          Column() {
            // 此处为示例资源,开发者可使用应用图标进行替换,以保证正常编译运行
            Image($r('app.media.app_icon'))
              .width(48)
              .height(48)
              .draggable(false)
              .copyOption(CopyOptions.None)
              .onComplete(() => {
                hilog.info(this.domainId, this.logTag, 'appIcon loading success.');
              })
              .onError(() => {
                hilog.error(this.domainId, this.logTag, 'appIcon loading fail.');
              })

            Text($r('app.string.app_name'))
              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
              .fontWeight(FontWeight.Bold)
              .maxFontSize($r('sys.float.ohos_id_text_size_headline8'))
              .minFontSize($r('sys.float.ohos_id_text_size_body1'))
              .maxLines(1)
              .fontColor($r('sys.color.ohos_id_color_text_primary'))
              .constraintSize({ maxWidth: '100%' })
              .margin({
                top: 12
              })

            Text('应用描述')
              .fontSize($r('sys.float.ohos_id_text_size_body2'))
              .fontColor($r('sys.color.ohos_id_color_text_secondary'))
              .fontFamily($r('sys.string.ohos_id_text_font_family_regular'))
              .fontWeight(FontWeight.Regular)
              .constraintSize({ maxWidth: '100%' })
              .margin({
                top: 8
              })
          }.margin({
            top: 100
          })

          Column() {
            Text(this.quickLoginAnonymousPhone)
              .fontSize(36)
              .fontColor($r('sys.color.ohos_id_color_text_primary'))
              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
              .fontWeight(FontWeight.Bold)
              .lineHeight(48)
              .textAlign(TextAlign.Center)
              .maxLines(1)
              .constraintSize({ maxWidth: '100%', minHeight: 48 })

            Text('华为账号绑定号码')
              .fontSize($r('sys.float.ohos_id_text_size_body2'))
              .fontColor($r('sys.color.ohos_id_color_text_secondary'))
              .fontFamily($r('sys.string.ohos_id_text_font_family_regular'))
              .fontWeight(FontWeight.Regular)
              .lineHeight(19)
              .textAlign(TextAlign.Center)
              .maxLines(1)
              .constraintSize({ maxWidth: '100%' })
              .margin({
                top: 8
              })
          }.margin({
            top: 64
          })

          Column() {
            LoginWithHuaweiIDButton({
              params: {
                // LoginWithHuaweiIDButton支持的样式
                style: loginComponentManager.Style.BUTTON_RED,
                // 账号登录按钮在登录过程中展示加载态
                extraStyle: {
                  buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({
                    show: true
                  })
                },
                // LoginWithHuaweiIDButton的边框圆角半径
                borderRadius: 24,
                // LoginWithHuaweiIDButton支持的登录类型
                loginType: loginComponentManager.LoginType.QUICK_LOGIN,
                // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换
                supportDarkMode: true
              },
              controller: this.controller
            })
          }
          .height(40)
          .margin({
            top: 56
          })

          Column() {
            Button({
              type: ButtonType.Capsule,
              stateEffect: true
            }) {
              Text('其他方式登录')
                .fontColor($r('sys.color.ohos_id_color_text_primary_activated'))
                .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
                .fontWeight(FontWeight.Medium)
                .fontSize($r('sys.float.ohos_id_text_size_button1'))
                .focusable(true)
                .focusOnTouch(true)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .maxLines(1)
                .padding({ left: 8, right: 8 })
            }
            .fontColor($r('sys.color.ohos_id_color_text_primary_activated'))
            .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
            .fontWeight(FontWeight.Medium)
            .backgroundColor($r('sys.color.ohos_id_color_button_normal'))
            .focusable(true)
            .focusOnTouch(true)
            .constraintSize({ minHeight: 40 })
            .width('100%')
            .onClick(() => {
              hilog.info(this.domainId, this.logTag, 'click optionalLoginButton.');
            })
          }.margin({ top: 16 })
        }.width('100%')

        Row() {
          Row() {
            Checkbox({ name: 'privacyCheckbox', group: 'privacyCheckboxGroup' })
              .width(24)
              .height(24)
              .focusable(true)
              .focusOnTouch(true)
              .margin({ top: 0 })
              .select(this.isSelected)
              .onChange((value: boolean) => {
                if (value) {
                  this.isSelected = true;
                  this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.ACCEPTED);
                } else {
                  this.isSelected = false;
                  this.controller.setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED);
                }
                hilog.info(this.domainId, this.logTag, `agreementChecked: ${value}`);
              })
          }

          Row() {
            Text() {
              ForEach(this.privacyText, (item: loginComponentManager.PrivacyText) => {
                if (item?.type === loginComponentManager.TextType.PLAIN_TEXT && item?.text) {
                  Span(item?.text)
                    .fontColor($r('sys.color.ohos_id_color_text_secondary'))
                    .fontFamily($r('sys.string.ohos_id_text_font_family_regular'))
                    .fontWeight(FontWeight.Regular)
                    .fontSize($r('sys.float.ohos_id_text_size_body3'))
                } else if (item?.type === loginComponentManager.TextType.RICH_TEXT && item?.text) {
                  Span(item?.text)
                    .fontColor($r('sys.color.ohos_id_color_text_primary_activated'))
                    .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
                    .fontWeight(FontWeight.Medium)
                    .fontSize($r('sys.float.ohos_id_text_size_body3'))
                    .onClick(() => {
                      // 应用需要根据item.tag实现协议页面的跳转逻辑
                      hilog.info(this.domainId, this.logTag, `click privacy text tag: ${item.tag}`);
                      // 华为账号用户认证协议
                      if (item.tag === QuickLoginButtonComponent.USER_AUTHENTICATION_TAG) {
                        this.jumpToPrivacyWebView();
                      }
                    })
                }
              }, (item: loginComponentManager.PrivacyText) => item.text.toString())
            }
            .width('100%')
          }
          .margin({ left: 12 })
          .layoutWeight(1)
          .constraintSize({ minHeight: 24 })
        }
        .alignItems(VerticalAlign.Top)
        .margin({
          top: 16,
          bottom: 16
        })
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .constraintSize({ minHeight: '100%' })
      .margin({
        left: 16,
        right: 16
      })
    }
    .width('100%')
    .height('100%')
  }
}

@CustomDialog
export struct AgreementDialog {
  logTag: string = 'AgreementDialog';
  domainId: number = 0x0000;
  dialogController?: CustomDialogController;
  cancel: () => void = () => {
    // 用户点击"取消"按钮,可于此处补充业务逻辑
  };
  confirm: () => void = () => {
    // 用户点击"同意并登录"按钮,可于此处补充业务逻辑
  };
  clickHyperlinkText: () => void = () => {
    // 用户点击超链接文本,可于此处补充业务逻辑
  };
  privacyText: loginComponentManager.PrivacyText[] = [];
  private static USER_AUTHENTICATION_TAG = '华为账号用户认证协议';

  build() {
    Column() {
      Row() {
        Text('用户协议与隐私条款')
          .id('loginPanel_agreement_dialog_privacy_title')
          .maxFontSize($r('sys.float.ohos_id_text_size_headline8'))
          .minFontSize($r('sys.float.ohos_id_text_size_body1'))
          .fontColor($r('sys.color.ohos_id_color_text_primary'))
          .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .maxLines(2)
      }
      .alignItems(VerticalAlign.Center)
      .constraintSize({ minHeight: 56, maxWidth: 400 })
      .margin({
        left: $r('sys.float.ohos_id_max_padding_start'),
        right: $r('sys.float.ohos_id_max_padding_start')
      })

      Row() {
        Text() {
          ForEach(this.privacyText, (item: loginComponentManager.PrivacyText) => {
            if (item?.type === loginComponentManager.TextType.PLAIN_TEXT && item?.text) {
              Span(item?.text)
                .fontSize($r('sys.float.ohos_id_text_size_body1'))
                .fontColor($r('sys.color.ohos_id_color_text_primary'))
                .fontFamily($r('sys.string.ohos_id_text_font_family_regular'))
                .fontWeight(FontWeight.Regular)
            } else if (item?.type === loginComponentManager.TextType.RICH_TEXT && item?.text) {
              Span(item?.text)
                .fontSize($r('sys.float.ohos_id_text_size_body1'))
                .fontColor('#CE0E2D')
                .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
                .fontWeight(FontWeight.Medium)
                .onClick(() => {
                  // 应用需要根据item.tag实现协议页面的跳转逻辑
                  hilog.info(this.domainId, this.logTag, `click privacy text tag: ${item.tag}`);
                  // 华为账号用户认证协议
                  if (item.tag === AgreementDialog.USER_AUTHENTICATION_TAG) {
                    hilog.info(this.domainId, this.logTag, 'AgreementDialog click.');
                    this.clickHyperlinkText();
                  }
                })
            }
          }, (item: loginComponentManager.PrivacyText) => item.text.toString())
        }
        .width('100%')
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .maxLines(10)
        .textAlign(TextAlign.Start)
        .focusable(true)
        .focusOnTouch(true)
        .padding({ left: 24, right: 24 })
      }.width('100%')

      Flex({
        direction: FlexDirection.Row
      }) {
        Button('取消',
          { type: ButtonType.Capsule, stateEffect: true })
          .id('loginPanel_agreement_cancel_btn')
          .fontColor($r('sys.color.ohos_id_color_text_primary'))
          .fontSize($r('sys.float.ohos_id_text_size_button1'))
          .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
          .backgroundColor(Color.Transparent)
          .fontWeight(FontWeight.Medium)
          .focusable(true)
          .focusOnTouch(true)
          .constraintSize({ minHeight: 40, maxWidth: 400 })
          .width('50%')
          .onClick(() => {
            hilog.info(this.domainId, this.logTag, 'AgreementDialog cancel.');
            this.cancel();
          })

        Button('同意并登录',
          { type: ButtonType.Capsule, stateEffect: true })
          .id('loginPanel_agreement_dialog_huawei_id_login_btn')
          .fontColor(Color.White)
          .backgroundColor('#CE0E2D')
          .fontSize($r('sys.float.ohos_id_text_size_button1'))
          .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
          .fontWeight(FontWeight.Medium)
          .focusable(true)
          .focusOnTouch(true)
          .constraintSize({ minHeight: 40, maxWidth: 400 })
          .width('50%')
          .onClick(() => {
            hilog.info(this.domainId, this.logTag, 'AgreementDialog confirm.');
            this.confirm();
          })
      }
      .margin({
        top: 8,
        left: $r('sys.float.ohos_id_elements_margin_horizontal_l'),
        right: $r('sys.float.ohos_id_elements_margin_horizontal_l'),
        bottom: 16
      })
    }.backgroundColor($r('sys.color.ohos_id_color_dialog_default_bg'))
    .padding({
      left: 16,
      right: 16
    })
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 该账号不支持一键登录,如海外账号
  ERROR_CODE_NOT_SUPPORTED = 1001500003,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 内部错误
  ERROR_CODE_INTERNAL_ERROR = 1001502009,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 用户未同意用户协议
  ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED = 1005300001,
  // 参数错误
  ERROR_CODE_PARAMETER_ERROR = 401,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002
}

以下是华为账号用户认证协议展示页示例代码:

typescript 复制代码
import { webview } from '@kit.ArkWeb';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 华为账号用户认证协议展示页
@Entry
@Component
struct WebPage {
  @State webUrl?: string = '';
  @State progress: number = 0;
  logTag: string = 'WebPage';
  domainId: number = 0x0000;
  controller: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Column() {
        Button({ type: ButtonType.Normal }) {
          Image($r('sys.media.ohos_ic_compnent_titlebar_back'))
            .backgroundColor(Color.Transparent)
            .borderRadius(20)
            .width(24)
            .height(24)
            .draggable(false)
            .autoResize(false)
            .focusable(true)
            .fillColor($r('sys.color.ohos_id_color_titlebar_icon'))
            .matchTextDirection(true)
        }
        .alignSelf(ItemAlign.Start)
        .backgroundColor($r('sys.color.ohos_id_color_button_normal'))
        .borderRadius(20)
        .width(40)
        .height(40)
        .onClick(() => {
          this.getUIContext().getRouter().back();
        })
      }
      .height(56)
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({
        top: 36,
        left: 16
      })

      Progress({ value: this.progress, type: ProgressType.Linear })
        .width('100%')
        .visibility(this.progress <= 99 ? Visibility.Visible : Visibility.None)

      Web({ src: this.webUrl ?? '', controller: this.controller })
        .backgroundColor(Color.Transparent)
        .margin({ bottom: 60 })
        .onProgressChange((event) => {
          hilog.info(this.domainId, this.logTag,
            'onProgressChange: ', (event ? event.newProgress : -1));
          this.progress = event ? event.newProgress : 0;
        })
        .darkMode(WebDarkMode.Auto)
        .forceDarkAccess(true)
        .onLoadIntercept(() => {
          hilog.info(this.domainId, this.logTag, 'onLoadIntercept');
          return false;
        })
        .onErrorReceive((event) => {
          if (event) {
            hilog.error(this.domainId, this.logTag, `onErrorReceive,errorInfo: ${event?.error?.getErrorInfo()}`);
          }
        })
    }
    .alignItems(HorizontalAlign.Start)
    .padding({ left: 12, right: 12, bottom: 60 })
    .width('100%')
    .height('100%')
  }

  aboutToAppear(): void {
    hilog.info(0x0000, 'testTag', 'aboutToAppear');
    const params = this.getUIContext().getRouter().getParams() as Record<string, string>;
    this.webUrl = params.url ?? '';
    hilog.info(0x0000, 'testTag', `webUrl: ${this.webUrl}`);
  }

  aboutToDisappear(): void {
    hilog.info(0x0000, 'testTag', 'aboutToDisappear');
    if (this.webUrl) {
        try {
            this.controller.stop();
          } catch (error) {
            hilog.error(0x0000, 'testTag',
              `ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
      }
    }
  }
}
用户非首次登录应用(可选)

用户非首次登录应用流程请参考首次登录应用开发流程中的导入模块及获取匿名手机号,获取AuthorizationWithHuaweiIDResponse响应结果中的Authorization Code。可能存在的异常场景及处理方法,可参考表1 获取匿名手机号错误码处理。

正确获取到Authorization Code,开发者可将Authorization Code传给应用服务端用于获取用户身份标识(UnionID、OpenID),即可查询该用户是否已关联。

1)如已关联,结合风控、安全因素及自身业务场景判断,可展示已关联的账号,由用户选择是否使用华为账号登录应用,或免用户操作,静默登录应用,客户端开发结束。

2)如未关联,则参考首次登录应用开发流程中的展示一键登录页面并获取Authorization Code继续开发。

借助DevEco Studio辅助开发(可选)

打开需要提供一键登录功能的页面,在页面的build()中创建一个容器(如Column)。

在DevEco Studio菜单栏点击View > Tool Windows > Kit Assistant,或使用快捷键Alt + K,进入Kit Assistant页面。

在左侧目录中点击选中AccountKit > QuickLoginButton,并拖拽至新创建的容器中。即可在当前位置插入相应的代码片段。

若代码片段插入失败,可查询快速插入场景化代码片段的说明排查原因。

在自动生成的代码段的getQuickLoginAnonymousPhone函数中,执行executeRequest函数可获取响应结果。

根据获取的响应结果判断,可能存在以下场景:

已正确获取到用户匿名手机号及Authorization Code,开发者可将Authorization Code传给应用服务端用于获取用户身份标识(UnionID、OpenID),即可查询该用户是否已关联。

1)如已关联,结合风控、安全因素及自身业务场景判断,可展示已关联的账号,由用户选择是否使用华为账号登录应用,或免用户操作,静默登录应用,客户端开发结束。

2)如未关联,再判断是否存在下面的异常场景,如无,则参考下面步骤5继续开发。

存在如下异常场景:

1)返回1001502001 用户未登录华为账号错误码,说明华为账号未登录。

2)返回1001500003 不支持该scopes或permissions错误码,说明华为账号用户注册地为中国境外、香港特别行政区、澳门特别行政区或中国台湾。

3)获取到的匿名手机号为空,说明华为账号没有绑定手机号、权限未申请或未生效。

上述异常场景应用需要展示其他登录方式。

根据上述代码实现应用的登录页面,并展示华为账号一键登录按钮和华为账号用户认证协议(Account Kit提供跳转链接,应用需实现协议跳转,参见约束与限制第2点),用户同意协议并点击一键登录按钮后,可获取到Authorization Code,将该值传给应用服务端用于获取用户信息(完整手机号、UnionID、OpenID)。

服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用/oauth2/v6/quickLogin/getPhoneNumber接口获取完整手机号和华为账号用户标识UnionID。

应用通过获取到的完整手机号或UnionID查询该用户是否已关联应用系统数据库。如已关联,则绑定获取的UnionID与手机号到已有用户上(如已绑定,则可忽略),完成用户登录;如未关联,则创建新用户并绑定手机号与UnionID到该用户上。

客户端与服务端交互开发
应用客户端到应用服务端的开发

业务流程:

准备:

请先完成应用客户端一键登录的相关开发,相关开发指导参考客户端开发。

参考使用fetch发送网络请求完成客户端到服务端的接口请求,开发步骤如下:

在应用客户端调用应用服务端提供的接口,将Authorization Code传输给应用的服务端。

注意

应用客户端与应用服务端的交互安全需要应用自行保证。

typescript 复制代码
import { rcp } from '@kit.RemoteCommunicationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

// 客户端请求接口示例代码
export function rcpRequest(authCode: string) {
  // 定义请求头
  const headers: rcp.RequestHeaders = {
    'accept': 'application/json'
  };
  // 定义要传递的参数
  const postMessage: Record<string, string> = {
    'authorizationCode': authCode
  };
  const securityConfig: rcp.SecurityConfiguration = {
    tlsOptions: {
      tlsVersion: 'TlsV1.3'
    }
  };
  // 假设"http://localhost:8080"为应用服务端地址
  const baseUrl = 'http://localhost:8080/login';
  // 定义请求对象
  const req = new rcp.Request(baseUrl, 'POST', headers, postMessage);
  try {
    // 创建通信会话对象
    const session = rcp.createSession({ requestConfiguration: { security: securityConfig } });
    // 发起请求
    session.fetch(req).then((response) => {
      hilog.info(0x0000, 'getRcpResult', 'Succeeded in getting result from server.');
      if (response.body) {
        const decoder = util.TextDecoder.create('utf-8');
        const result = JSON.parse(decoder.decodeToString(new Uint8Array(response.body))) as Record<string, Object>;
        // 此为代码示例,具体实现请以业务服务端实际返回数据结构为准
        const phoneNumber: string = JSON.stringify(result['phone'] ?? '');
        if (phoneNumber) {
          // 应用处理相关逻辑
        }
      } else {
        hilog.error(0x0000, 'getRcpResult', 'Failed to get response body.');
      }
    }).catch((err: BusinessError) => {
      hilog.error(0x0000, 'getRcpResult', `err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);
    });
  } catch (err) {
    hilog.error(0x0000, 'getRcpResult', `err: err code is ${err.code}, err message is ${JSON.stringify(err)}`);
  }
}

应用服务端提供接口用于接收应用客户端获取到的Authorization Code。

应用服务端获取到Authorization Code之后,对接华为账号服务器,可以参考服务端开发,调用/oauth2/v6/quickLogin/getPhoneNumber接口获取完整手机号、UnionID、OpenID,同时也可以使用华为账号一键登录服务端Skill来进行代码开发,Skill使用方式详见Account Kit Skill能力开放。

根据获取的UnionID、OpenID、完整手机号,判断登录用户是否为新用户、是否已关联等等(根据实际业务开发)。

保存或更新用户信息到应用服务端,完成处理后,返回登录用户的信息至应用客户端。

客户端与服务端联调

前提:根据应用登录方案设计及实现,完成客户端和服务端开发,开发指导参见客户端开发、服务端开发和应用客户端到应用服务端的开发。

在客户端获取到Authorization Code之后,传送给服务端接口;在服务端使用Authorization Code获取华为账号绑定的手机号、UnionID、OpenID。

根据应用登录方案使用华为账号绑定的手机号、UnionID、OpenID登录成功后,应用服务端返回用户信息给应用客户端,应用客户端可根据需要进行本地持久化存储,例如:登录状态、用户账号名、手机号、用户身份标识等。

在应用客户端首页或个人信息页等位置,对当前登录用户信息进行展示,举例如下图:

点击放大

开发后验证

集成华为账号一键登录能力应用用户体验质量建议

应用完成开发后,可参照以下标准检查集成华为账号一键登录后的用户体验是否符合预期:

标准编号 标准项名称 类型 标准详细描述
1 满足华为账号提供登录设计规范 规则 需满足华为账号开放登录中 【华为账号一键登录】按钮 规范,保障HarmonyOS应用拥有简单易用、高效一致、快速安全的登录体验。
2 业务安全验证原则 建议 应用在用户同意后获取到手机号,需结合自身业务风控体系进行必要的安全验证,以保障用户数据安全。
3 用户交互体验原则 建议 (1)登录页面的用户协议与隐私协议、华为账号用户认证协议可展示、可点击;(2)当用户点击协议后,回退页面,须回到点击前的页面;(3)只有用户勾选并同意所有协议后,才可继续进行登录操作,若用户未勾选协议时直接点击华为账号登录按钮,须有明确的同意协议提醒;(4)点击登录按钮须直接完成登录流程,可出现头像、昵称授权页,但取消场景须不影响登录流程;若出现处理异常,须及时终止页面,不应出现应用无法操作的现象。
4 登录页面内容用户体验原则 建议 (1)若未提供其他登录方式,不应显示"其他方式登录"的入口;(2)若使用华为账号一键登录,页面匿名手机号须展示从华为账号侧获取的匿名手机号,不应展示其他来源的手机号;(3)用户协议中,必须包含《华为账号用户认证协议》,且协议必须可点击、可加载,加载后支持回退页面,且回到点击前的页面。
5 异常处理用户体验原则 建议 登录页面需进行异常处理保证:(1)若登录异常(如网络异常、海外账号不支持等情况),勿将错误码等原始信息直接透传给用户;(2)若登录时触发了华为侧的短信验证码校验,则在校验成功之后,应用不应再展示额外的验证码验证页面。
6 应用生命周期变化的华为账号用户体验原则 建议 应用更新后,其登录状态须与更新前一致。

华为账号登录(获取UnionID/OpenID)

使用"华为账号登录"按钮登录
场景介绍

应用可以使用Account Kit提供的华为账号登录按钮组件及服务端交互获取华为账号用户身份标识UnionID、OpenID,通过UnionID、OpenID完成用户登录;或者与应用账号完成绑定,绑定后用于登录或者验证。

华为账号登录按钮包含文本、标志和文本、标志三种样式,以满足应用对界面风格一致性和灵活性的要求。

约束与限制

华为账号按钮登录能力支持Phone、Tablet、PC/2in1设备。并且从5.1.1(19)版本开始,新增支持TV设备。

用户体验设计

账号登录按钮的用户体验和UX设计需符合【华为账号登录】按钮规范,不符合规范的UX设计可能会对应用上架和用户体验带来影响。

业务流程

流程说明:

调用登录按钮展示登录页阶段(序号1-3):

用户打开应用进行登录,应用设置LoginType类型为LoginType.ID后拉起应用自己的登录页并展示"华为账号登录"按钮,用户点击按钮,请求华为账号授权信息。

用户点击登录阶段(序号4-6):

如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。

如华为账号已登录,将直接返回Authorization Code等数据给应用。

用户关联应用账号阶段(序号7-16):

应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。

应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。

应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

接口说明
接口名 描述
LoginWithHuaweiIDButton 华为账号Button登录组件。当前该组件支持Icon类型按钮、纯文本按钮、Icon和文本混合按钮,如果仍然不能满足开发者的诉求,可以使用Style的BUTTON_CUSTOM值定义按钮的文字颜色和背景色。
onClickLoginWithHuaweiIDButton(callback: AsyncCallback): LoginWithHuaweiIDButtonController 注册华为账号登录按钮的登录事件结果回调。使用callback异步回调。
setAgreementStatus(agreementStatus: AgreementStatus): LoginWithHuaweiIDButtonController 设置协议状态方法。如果需要用户同意协议才能完成华为账号登录,请先设置协议状态为NOT_ACCEPTED,当用户同意协议后设置协议状态为ACCEPTED,才可以完成华为账号登录。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。此场景无需申请账号权限。

客户端开发

导入LoginWithHuaweiIDButton模块及相关公共模块。

typescript 复制代码
import { LoginWithHuaweiIDButton, loginComponentManager } from '@kit.AccountKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

调用LoginWithHuaweiIDButton组件,展示华为账号登录按钮,用户点击华为账号登录按钮后,应用获取到Authorization Code、ID Token,将数据传给应用服务端,可参考客户端与服务端交互开发的开发步骤a和b,完成服务端开发。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。应用可以通过公开的网址获取到华为账号服务器发布的公钥,对签名和ID Token中的必要信息进行验证,以证明其没有被篡改过。解析ID Token可参考ID Token解析与验证。

typescript 复制代码
@Entry
@Component
struct PreviewLoginButtonPage {
  // 构造LoginWithHuaweiIDButton组件的控制器
  controller: loginComponentManager.LoginWithHuaweiIDButtonController =
    new loginComponentManager.LoginWithHuaweiIDButtonController()
      .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {
        if (error) {
          this.dealAllError(error);
          return;
        }

        if (response) {
          hilog.info(0x0000, 'testTag', 'Succeeded in getting response.');
          const authorizationCode = response.authorizationCode;
          // 开发者处理authorizationCode
        }
      });

  // 错误处理
  dealAllError(error: BusinessError): void {
    hilog.error(0x0000, 'testTag',
      `Failed to login, errorCode is ${error.code}, errorMessage is ${error.message}`);
    // 在应用登录涉及UI交互场景下,建议按照如下错误码指导提示用户
    if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
      // 用户未登录华为账号,请登录华为账号并重试或者尝试使用其他方式登录
    } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
      // 网络错误,请检查当前网络状态并重试
    } else if (error.code === ErrorCode.ERROR_CODE_INTERNAL_ERROR) {
      // 登录失败,请尝试使用其他方式登录
    } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
      // 用户取消授权
    } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
      // 系统服务异常,请稍后重试或者尝试使用其他方式登录
    } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
      // 重复请求,应用无需处理
    } else if (error.code === ErrorCode.ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED) {
      // 用户未同意协议
    } else {
      // 应用登录失败,请尝试使用其他方式登录
    }
  }

  build() {
    Column() {
      Column() {
        Column() {
          LoginWithHuaweiIDButton({
            params: {
              // LoginWithHuaweiIDButton支持的样式
              style: loginComponentManager.Style.BUTTON_RED,
              // 账号登录按钮在登录过程中展示加载态
              extraStyle: {
                buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({
                  show: true
                })
              },
              // LoginWithHuaweiIDButton的边框圆角半径
              borderRadius: 24,
              // LoginWithHuaweiIDButton支持的登录类型
              loginType: loginComponentManager.LoginType.ID,
              // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换
              supportDarkMode: true
            },
            controller: this.controller
          })
        }
        .height(40)
      }.width('100%')
    }
    .justifyContent(FlexAlign.Center)
    .constraintSize({ minHeight: '100%' })
    .margin({
      left: 16,
      right: 16
    })
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 内部错误
  ERROR_CODE_INTERNAL_ERROR = 1001502009,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002,
  // 用户未同意用户协议
  ERROR_CODE_AGREEMENT_STATUS_NOT_ACCEPTED = 1005300001
}
服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用解析凭证接口获取用户的UnionID。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若应用不使用Refresh Token向华为账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,应用可重新登录授权获取Authorization Code,向华为账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

应用在自己的用户体系通过查询获取的UnionID判断该用户是否已关联。如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID,完成用户登录。

使用自定义按钮登录
场景介绍

应用应遵照【华为账号登录】按钮使用规则,在登录页面嵌入自定义华为账号登录按钮。当用户点击该按钮后,应用调用华为账号登录API获取Authorization Code,并通过服务端交互获取用户的UnionID、OpenID以完成登录。

约束与限制

自定义按钮登录能力支持Phone、Tablet、PC/2in1设备。并且从5.1.0(18)版本开始,新增支持Wearable设备;从5.1.1(19)版本开始,新增支持TV设备。

业务流程

流程说明:

展示自定义按钮调用登录API阶段(序号1-4):

用户打开应用进行登录,点击自定义登录按钮,应用传forceLogin等参数后调用华为账号登录API,请求华为账号授权信息。

如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。

如华为账号已登录,将直接返回Authorization Code等数据给应用。

用户关联应用账号阶段(序号5-14):

应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。

应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。

应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

接口说明
接口名 描述
createLoginWithHuaweiIDRequest(): LoginWithHuaweiIDRequest 创建账号登录请求。LoginWithHuaweiIDRequest中的forceLogin参数用来控制当用户未登录华为账号时,是否强制拉起华为账号登录界面。
constructor(context?: common.Context) 创建登录请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行登录操作。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。此场景无需申请账号权限。

客户端开发

根据【华为账号登录】按钮规范开发自定义登录图标按钮,参考如下步骤在点击事件中完成华为账号登录API调用。

导入authentication模块及相关公共模块。

typescript 复制代码
import { authentication } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

创建登录请求并设置参数。

typescript 复制代码
// 创建登录请求,并设置参数
const loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
// 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
loginRequest.forceLogin = true;
// 建议使用generateRandomUUID生成state,可用于一致性比对,防止跨站攻击
loginRequest.state = util.generateRandomUUID();

调用AuthenticationController对象的executeRequest方法执行登录请求,并处理登录结果,获取到Authorization Code及ID Token。之后将Authorization Code传给应用服务端处理,可参考客户端与服务端交互开发的开发步骤a和b。通过code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。应用可以通过公开的网址获取到华为账号服务器发布的公钥,对签名和ID Token中的必要信息进行验证,以证明其没有被篡改过。解析ID Token可参考ID Token解析与验证。

typescript 复制代码
// 执行登录请求
try {
  // 此示例为代码片段,实际需在自定义组件实例中使用,并传入有效的Context上下文对象
  const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());
  controller.executeRequest(loginRequest).then((response: authentication.LoginWithHuaweiIDResponse) => {
    const loginWithHuaweiIDResponse = response as authentication.LoginWithHuaweiIDResponse;
    const state = loginWithHuaweiIDResponse.state;
    // state为空时,归一化处理为空字符串
    const normalizedRequestState = loginRequest.state || '';
    const normalizedState = state || '';
    if (normalizedRequestState !== normalizedState) {
      hilog.error(0x0000, 'testTag', `Failed to login. The state is different, response state: ${state}`);
      return;
    }
    hilog.info(0x0000, 'testTag', 'Succeeded in logging in.');
    const loginWithHuaweiIDCredential = loginWithHuaweiIDResponse?.data;
    const authorizationCode = loginWithHuaweiIDCredential?.authorizationCode;
    // 开发者处理authorizationCode
  }).catch((error: BusinessError) => {
    dealAllError(error);
  });
} catch (error) {
  dealAllError(error);
}
// 错误处理
function dealAllError(error: BusinessError): void {
  hilog.error(0x0000, 'testTag', `Failed to login. Code: ${error.code}, message: ${error.message}`);
  // 在应用登录涉及UI交互场景下,建议按照如下错误码指导提示用户
  if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
    // 用户未登录华为账号,请登录华为账号并重试或者尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
    // 网络错误,请检查当前网络状态并重试
  } else if (error.code === ErrorCode.ERROR_CODE_INTERNAL_ERROR) {
    // 登录失败,请尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
    // 用户取消授权
  } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
    // 系统服务异常,请稍后重试或者尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
    // 重复请求,应用无需处理
  } else {
    // 应用登录失败,请尝试使用其他方式登录
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 内部错误
  ERROR_CODE_INTERNAL_ERROR = 1001502009,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002
}
服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用解析凭证接口获取用户的UnionID。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若应用不使用Refresh Token向华为账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,应用可重新登录授权获取Authorization Code,向华为账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

应用在自己的用户体系通过查询获取的UnionID判断该用户是否已关联。如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID,完成用户登录。

静默登录
场景介绍

在应用卸载重装、用户换机等场景,如登录的华为账号与应用重装、换机前一致,应用可通过Account Kit提供的静默登录方式即不需要用户点击登录/注册按钮,即可获取用户的身份标识UnionID/OpenID,完成用户的静默登录。

约束与限制

静默登录能力支持Phone、Tablet、PC/2in1设备。并且从5.1.0(18)版本开始,新增支持Wearable设备;从5.1.1(19)版本开始,新增支持TV设备。

业务流程

流程说明:

调用登录API阶段(序号1-3):

用户使用华为账号登录过应用,应用卸载重装、用户换机后再进入应用时,应用传forceLogin = false等参数调用登录API。

如华为账号已登录,且API调用成功,应用能获取到Authorization Code等登录结果。注意:如华为账号未登录,应用会获取到1001502001 用户未登录华为账号错误码,再根据需要自行处理。

用户关联应用账号阶段(序号4-13):

应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。

应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。

应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行安全认证后完成静默登录。

接口说明
接口名 描述
createLoginWithHuaweiIDRequest(): LoginWithHuaweiIDRequest 创建账号登录请求。LoginWithHuaweiIDRequest中的forceLogin参数用来控制当用户未登录华为账号时,是否强制拉起华为账号登录界面,静默登录场景设置为false。
constructor(context?: common.Context) 创建登录请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行登录操作。
开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。此场景无需申请账号权限。

客户端开发

导入authentication模块及相关公共模块。

typescript 复制代码
import { authentication } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

创建登录请求并设置参数。

typescript 复制代码
// 创建登录请求,并设置参数
const loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
// false表示静默登录
loginRequest.forceLogin = false;
// 建议使用generateRandomUUID生成state,可用于一致性比对,防止跨站攻击
loginRequest.state = util.generateRandomUUID();

调用AuthenticationController对象的executeRequest方法执行登录请求,并处理登录结果,获取到Authorization Code及ID Token。之后将Authorization Code传给应用服务端处理,可参考客户端与服务端交互开发的开发步骤a和b。应用可以通过公开的网址获取到华为账号服务器发布的公钥,对签名和ID Token中的必要信息进行验证,以证明其没有被篡改过。解析ID Token可参考ID Token解析与验证。

typescript 复制代码
// 执行登录请求
try {
  const controller = new authentication.AuthenticationController();
  controller.executeRequest(loginRequest).then((response: authentication.LoginWithHuaweiIDResponse) => {
    const loginWithHuaweiIDResponse = response as authentication.LoginWithHuaweiIDResponse;
    const state = loginWithHuaweiIDResponse.state;
    // state为空时,归一化处理为空字符串
    const normalizedRequestState = loginRequest.state || '';
    const normalizedState = state || '';
    if (normalizedRequestState !== normalizedState) {
      hilog.error(0x0000, 'testTag', `Failed to login. The state is different, response state: ${state}`);
      return;
    }
    hilog.info(0x0000, 'testTag', 'Succeeded in logging in.');
    const loginWithHuaweiIDCredential = loginWithHuaweiIDResponse?.data;
    const authorizationCode = loginWithHuaweiIDCredential?.authorizationCode;
    // 开发者处理authorizationCode
  }).catch((error: BusinessError) => {
    dealAllError(error);
  });
} catch (error) {
  dealAllError(error);
}
typescript 复制代码
// 错误处理
function dealAllError(error: BusinessError): void {
  hilog.error(0x0000, 'testTag', `Failed to login. Code: ${error.code}, message: ${error.message}`);
  // 在应用登录涉及UI交互场景下,建议按照如下错误码指导提示用户
  if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
    // 用户未登录华为账号,请登录华为账号并重试或者尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
    // 网络错误,请检查当前网络状态并重试
  } else if (error.code === ErrorCode.ERROR_CODE_INTERNAL_ERROR) {
    // 登录失败,请尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
    // 用户取消授权
  } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
    // 系统服务异常,请稍后重试或者尝试使用其他方式登录
  } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
    // 重复请求,应用无需处理
  } else {
    // 应用登录失败,请尝试使用其他方式登录
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 内部错误
  ERROR_CODE_INTERNAL_ERROR = 1001502009,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002
}
服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用解析凭证接口获取用户的UnionID。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若应用不使用Refresh Token向华为账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,应用可重新登录授权获取Authorization Code,向华为账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

应用在自己的用户体系通过查询获取的UnionID判断该用户是否已关联。如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID,完成用户登录。

扫码授权登录
场景介绍

对于在缺乏便捷触控界面或输入受限的设备上登录应用的场景,推荐使用扫码授权登录方式接入Account Kit。用户使用已登录华为账号的设备(例如:Phone、Tablet等)扫描应用二维码并完成授权后,应用即可获取用户标识UnionID/OpenID等信息,实现扫码授权登录。

业务流程

扫码授权登录的整体接入流程如下:

步骤说明

展示二维码阶段(序号1-5)

用户选择华为账号扫码授权方式登录,应用调用华为账号服务器获取二维码信息。

应用根据华为账号服务器返回的二维码信息生成二维码图片。

等待用户扫码授权登录阶段(序号6-9)

应用将获取到的设备码传给应用服务端,应用服务端使用设备码轮询扫码授权登录-获取用户级凭证接口。

针对轮询扫码授权登录-获取用户级凭证接口响应的结果,应用请自行进行处理。

用户扫码授权登录阶段(序号10-21)

用户使用已登录华为账号的设备(例如:Phone、Tablet等)扫描应用二维码并完成授权。此时应用服务端轮询扫码授权登录-获取用户级凭证接口获取到Access Token,再使用Access Token调用获取用户信息接口获取用户UnionID、OpenID等信息。

应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录。

应用对用户标识UnionID/OpenID、业务登录凭证SessionId信息进行安全认证后完成扫码授权登录。

接口说明
接口名称 描述
获取二维码信息接口 应用通过调用该接口获取二维码信息。
扫码授权登录-获取用户级凭证接口 通过轮询该接口,获取用户级凭证。
获取用户信息接口 在获取到用户级凭证后,通过该接口获取用户UnionID/OpenID等信息。
开发步骤
获取二维码信息

应用调用华为账号的获取二维码信息接口获取二维码信息。

请求消息示例

请通过POST方式调用,示例如下:

http 复制代码
POST /oauth2/v3/device/code HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

client_id=<client_id>&scope=openid profile

响应消息示例

http 复制代码
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8

{
    "create_time": 1569813512,
    "device_code": "AgEAAN7qd*****U0TvQ",
    "expire_in": 1800,
    "interval": 3,
    "scan_expire_in": 120,
    "user_code": "ABCDEFGH",
    "verification_url": "https://oauth-login1.cloud.huawei.com/oauth2/v3/device"
}

请求参数、响应参数及错误码等信息详见获取二维码信息接口。

生成二维码

应用根据响应中的verification_url和user_code生成二维码并展示。

二维码URL内容如下

text 复制代码
https://oauth-login1.cloud.huawei.com/oauth2/v3/device?user_code=<user_code>

应用可以通过QRCode来生成二维码并在应用内展示,等待用户扫码。

说明

获取二维码信息接口返回的user_code存在有效期(以接口响应结果中的scan_expire_in字段实际值为准),如果用户未在此时间内扫码,需要重新调用获取二维码信息接口生成新的二维码。

轮询获取用户级凭证

应用服务端轮询调用扫码授权登录-获取用户级凭证接口(轮询间隔时间可参考获取二维码信息接口响应结果中的interval字段值),直到轮询次数超过限制、设备码超过有效期或成功获取Access Token为止。

请求消息示例

请通过POST方式调用,示例如下:

http 复制代码
POST /oauth2/v3/token HTTP/1.1
Host: oauth-login.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

grant_type=device_code&code=<device_code>&client_id=<client_id>&client_secret=<client_secret>

响应消息示例

用户未扫码时

http 复制代码
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "sub_error": 20411,
    "error_description": "user code not scan",
    "error": 1101
}

用户已扫码并完成授权时

http 复制代码
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8

{
    "access_token": "DgEAAN7qd*****U0TvQ/eXpE4x+gvhoYh5/UuzL",
    "refresh_token": "DgECAL++vCn******NQ/UOL8+wm0jJi+o4NI793H",
    "expires_in": 3600,
    "id_token": "eyJraW*****ifQ.eyJhdF9oYX*****Q2fQ.TT05lFYe*****vDwb_Gj1ccR59yyB2Ig",
    "scope": "openid profile",
    "token_type": "Bearer",
    "open_id": "AQAxrBzThFv*****lv9tV_4rMCc"
}

请求参数、响应参数及错误码等信息详见扫码授权登录-获取用户级凭证接口。

说明

获取二维码信息接口返回的device_code存在有效期(以接口响应结果中的expire_in字段实际值为准),超时后需要重新调用获取二维码信息接口生成新的二维码。

应用服务端在轮询扫码授权登录-获取用户级凭证接口时,同一个设备码轮询次数不得超过获取二维码信息接口响应结果中的expire_in/interval的值,轮询超过限制后请求会被拒绝,需要重新调用获取二维码信息接口生成新的二维码。

用户扫码授权登录

用户使用已登录华为账号的设备(例如:Phone、Tablet等)扫描应用的二维码,弹出授权页面,显示应用名称、图标及申请的权限列表,用户点击允许后完成授权流程。

在用户完成授权后,此时应用服务端轮询扫码授权登录-获取用户级凭证接口获取到Access Token,再使用Access Token调用获取用户信息接口获取用户UnionID、OpenID等信息。

应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录。

应用对用户标识UnionID/OpenID、业务登录凭证SessionId信息进行安全认证后完成扫码授权登录。

说明

若Access Token过期,可以使用Refresh Token调用刷新用户级凭证获取新的Access Token。

获取华为账号用户信息

概述

当应用需要获取用户身份标识或者完善用户个人资料(头像昵称、收货地址、发票抬头)时,或需要获取用户风险等级判断用户风险时,可通过Account Kit提供的相关能力,引导用户填写、管理相关信息并完成授权。获取头像昵称、收货地址、发票抬头等详细接入体验可参考Account Kit提供的SampleCode示例工程。

典型场景:

应用需要完善用户头像昵称信息,参见获取头像昵称。

应用提供的服务依赖用户收货地址,需要获取用户收货地址,参见获取收货地址。

应用提供的服务依赖用户发票抬头信息,需要获取用户发票抬头,参见获取发票抬头。

应用提供的服务依赖用户风险等级信息,需要获取用户风险等级,参见获取风险等级。

应用提供的服务依赖用户手机号,需要获取用户手机号,参见获取手机号。

应用提供的服务依赖用户实名年龄段,需要获取用户实名年龄段信息,参见获取实名年龄段。

获取头像昵称

场景介绍

当应用需要获取用户头像昵称信息,可使用Account Kit提供的头像昵称授权能力,用户允许应用获取头像昵称后,可快速完成个人信息填写。以下对Account Kit提供的头像昵称授权能力进行介绍。此外,开发者也可通过场景化控件中的选择头像Button获取用户头像。

图1 手机端获取头像昵称(请以实际效果为准)

点击放大

图2 Wearable设备获取头像昵称(请以实际效果为准)

点击放大

约束与限制

获取头像昵称能力支持Phone、Tablet、PC/2in1设备。并且从5.1.0(18)版本开始,新增支持Wearable设备;从5.1.1(19)版本开始,新增支持TV设备。

业务流程

流程说明:

应用传对应scope调用授权API请求获取用户头像昵称。

如用户已给应用授权,则开发者能直接获取用户头像昵称。

如用户未授权,则授权请求会拉起授权页面,在用户确认授权后,开发者能获取到用户头像昵称。

获取到头像url信息,开发者可以通过该url下载并使用用户头像。

接口说明
接口名 描述
createAuthorizationWithHuaweiIDRequest(): AuthorizationWithHuaweiIDRequest 获取授权请求对象接口,通过在AuthorizationWithHuaweiIDRequest对象中传入头像昵称的scope:profile及Authorization Code的permission:serviceauthcode,即可在授权结果中获取到用户头像昵称和Authorization Code。
constructor(context?: common.Context) 创建授权请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行授权操作。头像昵称,可从AuthenticationResponse的子类AuthorizationWithHuaweiIDResponse中解析,具体解析方法请参考客户端开发的示例代码。

注意

1.上述接口需在页面或自定义组件生命周期内调用。

2.未设置昵称默认返回华为账号绑定的匿名手机号/邮箱。

3.当用户更新头像后,原用户头像链接会立即失效。为确保头像正常显示,建议先将头像下载保存后再使用,避免因用户头像链接失效而影响业务流程。

开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。此场景无需申请账号权限。

注意

若未正确配置公钥指纹,将报错1001500001 应用指纹证书校验失败。

开发步骤
客户端开发

导入authentication模块及相关公共模块。

typescript 复制代码
import { authentication } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

创建授权请求并设置参数。

typescript 复制代码
// 创建授权请求,并设置参数
const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
// 获取头像昵称需要传如下scope
authRequest.scopes = ['profile'];
// 若开发者需要进行服务端开发以获取头像昵称,则需传如下permission获取authorizationCode
authRequest.permissions = ['serviceauthcode'];
// 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
authRequest.forceAuthorization = true;
// 建议使用generateRandomUUID生成state,可用于一致性比对,防止跨站攻击
authRequest.state = util.generateRandomUUID();

调用AuthenticationController对象的executeRequest方法执行授权请求,并处理授权结果,从授权结果中解析出头像昵称和Authorization Code。

typescript 复制代码
// 执行授权请求
try {
  // 此示例为代码片段,实际需在自定义组件实例中使用,并传入有效的Context上下文对象
  const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());
  controller.executeRequest(authRequest).then((data) => {
    const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;
    const state = authorizationWithHuaweiIDResponse?.state;
    // state为空时,归一化处理为空字符串
    const normalizedRequestState = authRequest.state || '';
    const normalizedState = state || '';
    if (normalizedRequestState !== normalizedState) {
      hilog.error(0x0000, 'testTag', `Failed to authorize. The state is different, response state: ${state}`);
      return;
    }
    hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');
    const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;
    const avatarUri = authorizationWithHuaweiIDCredential?.avatarUri;
    const nickName = authorizationWithHuaweiIDCredential?.nickName;
    // 开发者处理avatarUri, nickName
    const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;
    // 涉及服务端开发以获取头像昵称场景,开发者处理authorizationCode
    // ...
  }).catch((err: BusinessError) => {
    // ...
    dealAllError(err);
  });
} catch (error) {
  dealAllError(error);
}
typescript 复制代码
// 错误处理
function dealAllError(error: BusinessError): void {
  hilog.error(0x0000, 'testTag', `Failed to obtain userInfo. Code: ${error.code}, message: ${error.message}`);
  // 在应用获取头像昵称场景下,涉及UI交互时,建议按照如下错误码指导提示用户
  if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
    // 用户未登录华为账号,请登录华为账号并重试
  } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
    // 网络错误,请检查当前网络状态并重试
  } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
    // 用户取消授权
  } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
    // 系统服务异常,请稍后重试
  } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
    // 重复请求,应用无需处理
  } else {
    // 获取用户信息失败,请稍后重试
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002
}
服务端开发(可选)

开发者根据业务需要选择是否进行服务端开发,客户端返回的头像昵称数据同步存在延迟,如果对头像昵称时效性要求较高,建议通过服务端获取。

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用获取用户信息接口获取用户信息,从用户信息中获取用户头像昵称。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若您不使用Refresh Token向账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,业务可重新登录授权获取Authorization Code,向账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

获取手机号

场景介绍

当应用需要获取用户手机号时,可使用Account Kit提供的手机号授权能力,向用户发起手机号授权申请,经用户同意授权后,获取到手机号并为用户提供相应服务。以下对Account Kit提供的手机号授权能力进行介绍,获取手机号功能还可使用场景化控件快速验证手机号Button进行实现。

说明

对用户选择的华为账号绑定的手机号或者新增的手机号进行验证,不保证是实时的验证,仅首次需要用户授权。

在应用账号已登录并绑定用户华为账号UnionID的情况下,应用可以在获取手机号等用户信息时根据实际业务场景判断是否需要提前校验当前应用账号绑定的UnionID与系统账号当前的UnionID是否一致。

图1 手机端获取手机号(请以实际效果为准)

点击放大

图2 Wearable设备获取手机号(请以实际效果为准)

点击放大

约束与限制

应用满足《常见类型移动互联网应用程序必要个人信息范围规定》(对第三方网站的内容,华为不承担任何责任)中使用手机号的必要业务场景。

儿童账号无法通过该能力获取到手机号。

获取手机号能力支持Phone、Tablet、PC/2in1设备。并且从5.1.0(18)版本开始,新增支持Wearable设备;从5.1.1(19)版本开始,新增支持TV设备。

获取手机号能力目前仅支持游戏类应用申请使用。

业务流程

流程说明:

应用通过传对应scope和permission调用授权API,如果已授权则直接返回临时登录凭证Authorization Code;如果未授权则拉起授权页,在用户确认授权后,返回Authorization Code。

将Authorization Code传给应用服务端,使用Client ID、Client Secret、Authorization Code从华为服务器中获取Access Token,再使用Access Token请求获取用户信息。

从用户信息中获取到手机号、UnionID、OpenID。

接口说明
接口名 描述
createAuthorizationWithHuaweiIDRequest(): AuthorizationWithHuaweiIDRequest 获取授权接口,通过AuthorizationWithHuaweiIDRequest传入返回手机号的scope:phone及返回Authorization Code的permission:serviceauthcode,即可获取到Authorization Code。
constructor(context?: common.Context) 创建授权请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行授权操作。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请先确认您已完成开发准备工作。

若未配置签名和指纹,将报错1001500001 应用指纹证书校验失败。

若未完成"获取您的手机号"权限申请,将报错1001502014 应用未申请scopes或permissions权限。

设备需要登录华为账号,若未登录则拉起登录页面。

开发步骤
客户端开发

导入authentication模块及相关公共模块。

typescript 复制代码
import { authentication } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

创建授权请求并设置参数。

typescript 复制代码
// 创建授权请求,并设置参数
const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
// 获取手机号需要传如下scope,传参数之前需要先申请对应scope权限,否则会返回1001502014错误码
authRequest.scopes = ['phone'];
// 获取authorizationCode需传如下permission
authRequest.permissions = ['serviceauthcode'];
// 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
authRequest.forceAuthorization = true;
// 建议使用generateRandomUUID生成state,可用于一致性比对,防止跨站攻击
authRequest.state = util.generateRandomUUID();

调用AuthenticationController对象的executeRequest方法执行授权请求,并处理授权结果,从授权结果中解析出Authorization Code,之后将Authorization Code传给应用服务端处理。

typescript 复制代码
// 执行请求
try {
  // 此示例为代码片段,实际需在自定义组件实例中使用,并传入有效的Context上下文对象
  const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());
  controller.executeRequest(authRequest).then((data) => {
    const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;
    const state = authorizationWithHuaweiIDResponse.state;
    // state为空时,归一化处理为空字符串
    const normalizedRequestState = authRequest.state || '';
    const normalizedState = state || '';
    if (normalizedRequestState !== normalizedState) {
      hilog.error(0x0000, 'testTag', `Failed to authorize. The state is different, response state: ${state}`);
      return;
    }
    hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');
    const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;
    const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;
    // 开发者处理authorizationCode
    // ...
  }).catch((err: BusinessError) => {
    // ...
    dealAllError(err);
  });
} catch (error) {
  dealAllError(error);
}
typescript 复制代码
// 错误处理
function dealAllError(error: BusinessError): void {
  hilog.error(0x0000, 'testTag', `Failed to obtain userInfo. Code: ${error.code}, message: ${error.message}`);
  // 在应用获取手机号场景下,涉及UI交互时,建议按照如下错误码指导提示用户
  if (error.code === ErrorCode.ERROR_CODE_LOGIN_OUT) {
    // 用户未登录华为账号,请登录华为账号并重试
  } else if (error.code === ErrorCode.ERROR_CODE_NETWORK_ERROR) {
    // 网络错误,请检查当前网络状态并重试
  } else if (error.code === ErrorCode.ERROR_CODE_USER_CANCEL) {
    // 用户取消授权
  } else if (error.code === ErrorCode.ERROR_CODE_SYSTEM_SERVICE) {
    // 系统服务异常,请稍后重试
  } else if (error.code === ErrorCode.ERROR_CODE_REQUEST_REFUSE) {
    // 重复请求,应用无需处理
  } else {
    // 获取用户信息失败,请尝试使用其他方式登录
  }
}

export enum ErrorCode {
  // 账号未登录
  ERROR_CODE_LOGIN_OUT = 1001502001,
  // 网络错误
  ERROR_CODE_NETWORK_ERROR = 1001502005,
  // 用户取消授权
  ERROR_CODE_USER_CANCEL = 1001502012,
  // 系统服务异常
  ERROR_CODE_SYSTEM_SERVICE = 12300001,
  // 重复请求
  ERROR_CODE_REQUEST_REFUSE = 1001500002
}
服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用获取用户信息接口获取用户信息,从用户信息中获取用户手机号、UnionID、OpenID。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若您不使用Refresh Token向账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,业务可重新登录授权获取Authorization Code,向账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

获取收货地址

场景介绍

当应用需要获取用户收货地址时,可使用Account Kit提供的获取收货地址的能力,引导用户添加或选择已有的收货地址,并最终获取用户的收货地址。以下对Account Kit提供的获取收货地址能力进行介绍,获取收货地址功能还可使用场景化控件选择收货地址Button进行实现。

点击放大

约束与限制

收货地址中的手机号信息仅支持输入中国境内(香港特别行政区、澳门特别行政区、中国台湾除外)手机号、地址信息只支持填写中国境内(香港特别行政区、澳门特别行政区、中国台湾除外)。

获取收货地址的能力支持Phone、Tablet、PC/2in1设备。并且从26.0.0版本开始,新增支持TV设备。

业务流程

流程说明:

用户需要使用收货地址时,应用程序调用选择收货地址API,打开华为账号收货地址管理页面。

用户可以在收货地址管理页面添加新的收货地址或者选择已有收货地址,点击确认后,选择的收货地址将返回给应用。

接口说明
接口名 描述
chooseAddress(context: common.Context): Promise 拉起收货地址管理页面并返回用户所选择的收货地址。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请先确认以下准备工作是否完成:

1、是否完成申请账号权限,未申请通过调用获取收货地址API,将返回1008100005 应用未申请对应permissions权限错误码,无法获取收货地址。

说明

如果在权限申请前已完成"配置签名和指纹",则需要重新申请调试Profile,并重新手动配置签名信息。

2、是否完成配置签名和指纹、配置Client ID,未配置调用获取收货地址API,将返回 1008100004 应用指纹证书校验失败错误码,无法获取收货地址。

开发步骤

导入shippingAddress模块及相关公共模块。

typescript 复制代码
import { shippingAddress } from '@kit.AccountKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

调用chooseAddress方法打开收货地址管理页面,引导用户添加或选择收货地址后,应用即可获取用户收货地址。

typescript 复制代码
// 执行请求
try {
  // 此示例为代码片段,实际需在自定义组件实例中使用,并传入有效的Context上下文对象
  shippingAddress.chooseAddress(this.getUIContext().getHostContext())
    .then((data: shippingAddress.AddressInfo) => {
      hilog.info(0x0000, 'testTag', 'Succeeded in choosing address.');
      const userName: string = data.userName;
      const mobileNumber: string = data.mobileNumber;
      const countryCode: string = data.countryCode;
      const provinceName: string = data.provinceName;
      const cityName: string = data.cityName;
      const districtName: string = data.districtName;
      const streetName: string = data.streetName;
      const detailedAddress: string = data.detailedAddress;
      // 开发者处理获取的收货地址信息
      // ...
    }).catch((error: BusinessError) => {
    // ...
    dealAllError(error);
  });
} catch (error) {
  // ...
  dealAllError(error);
}
typescript 复制代码
// 错误处理
function dealAllError(error: BusinessError): void {
  hilog.error(0x0000, 'testTag', `Failed to chooseAddress. Code: ${error.code}, message: ${error.message}`);
}

获取发票抬头

场景介绍

当应用需要获取用户发票抬头时,可使用Account Kit提供的发票助手能力,打开发票抬头选择页面,帮助用户快速选择或管理发票抬头。以下对Account Kit提供的发票助手能力进行介绍,获取发票抬头功能还可使用场景化控件选择发票抬头Button进行实现。

点击放大

约束与限制

Wearable、TV设备暂不支持使用获取发票抬头功能。

业务流程

流程说明:

用户需要使用发票抬头时,应用程序调用选择发票抬头API,打开华为账号发票抬头选择页。

用户可以在发票抬头选择页选择已有发票抬头或者跳转到发票抬头管理页进行增加,点击确认后可将选择的发票抬头返回给应用。

接口说明
接口名 描述
selectInvoiceTitle(context: common.Context): Promise 调用该方法打开发票抬头选择页面,使用Promise异步回调返回选择的发票抬头。

注意

上述接口需在页面或自定义组件生命周期内调用。

开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。此场景无需申请账号权限。

开发步骤

导入invoiceAssistant模块及相关公共模块。

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';
import { invoiceAssistant } from '@kit.AccountKit';
import { BusinessError } from '@kit.BasicServicesKit';

调用selectInvoiceTitle方法选择发票抬头页面。

typescript 复制代码
try {
  if (canIUse('SystemCapability.HuaweiID.InvoiceAssistant')) {
    invoiceAssistant.selectInvoiceTitle(context).then((data: invoiceAssistant.InvoiceTitle) => {
      // ...
    }).catch((error: BusinessError) => {
      hilog.error(domainId, logTag,
        `Failed to selectInvoiceTitle. BusinessError errCode: ${error.code}, errMessage: ${error.message}`);
    });
  } else {
    hilog.error(domainId, logTag, 'The API is not supported on this device.');
  }
} catch (error) {
  hilog.error(domainId, logTag,
    `Failed to selectInvoiceTitle. errCode: ${error.code}, errMessage: ${error.message}`);
}

获取风险等级

概述

当应用需要获取用户风险等级时,可使用Account Kit提供的获取用户风险等级能力,用于以下恶意场景识别:

应用登录风控场景:应用使用华为账号关联登录时,获取华为账号风险等级,对高风险等级账号进行风控,提升应用的安全等级。

营销活动反作弊场景:应用营销活动期间,如进行商户补贴、优惠券发放等商业营销活动时获取华为账号风险等级,协助开发者有效识别"薅羊毛"风险;保护营销资源合理使用,降低业务安全问题给营销方带来的损失,为相关活动保驾护航。

Account Kit提供的风险等级如下:

风险等级 说明 建议处置方案
0 未发现显著风险项。例如通过合法途径注册,正常使用华为手机,无批量操作、使用自动机、养号等异常行为。 建议确认无风险后放通。
1 低风险。发现风险因素,结合总体评分后风险较低。 建议进行简单验证(如验证码、短信等),或人工审核。
2 中风险。发现风险因素,结合总体评分后风险中等。 建议根据业务场景采取一定措施规避伤害。例如,营销活动可降低高等级奖励的概率、打榜类活动对此类投票降低权重、登录注册要求二次验证等。
3 高风险。发现风险因素,结合总体评分后风险较高。 建议业务逻辑直接拦截。例如,红包类活动返回不中奖或最小额红包、打榜类活动不计算票数、登录/注册操作要求二次验证、高危业务可选择限制本次操作。
4 风险未知。暂无明确风险等级。 建议结合账号历史行为及业务场景做出最终决策。
通过华为账号一键登录获取用户风险等级
场景介绍

应用登录风控场景,可以通过华为账号一键登录获取用户风险等级,对恶意账号进行风控,提升应用的安全等级。

约束与限制

通过华为账号一键登录获取用户风险等级能力支持Phone、Tablet、PC/2in1设备。并且从5.1.1(19)版本开始,新增支持TV设备。

业务流程

图1 华为账号一键登录(用户首次登录应用)获取华为账号风险等级流程图

参考华为账号一键登录业务流程,确保系统账号已登录,匿名手机号获取成功,且用户首次使用华为账号登录应用。(如用户非首次使用华为账号登录,可通过华为账号其他方式登录获取用户风险等级来查询华为账号的风险等级)

调用LoginWithHuaweiIDButton组件,在LoginWithHuaweiIDButtonParams参数中设置风险等级字段标识riskLevel,拉起应用登录页。

用户同意协议后,点击华为账号一键登录按钮,应用可以通过HuaweiIDCredential获取到Authorization Code等数据。

将获取的Authorization Code数据传给应用服务端,应用服务端通过调用获取用户风险等级接口查询当前登录用户的华为账号风险等级。

应用基于用户风险等级判断继续登录流程或者返回对应风控措施。

接口说明

一键登录接口遵循华为账号一键登录接口说明,当应用需要获取用户风险等级时,在LoginWithHuaweiIDButton组件参数LoginWithHuaweiIDButtonParams中传入riskLevel字段,通过一键登录返回Authorization Code查询用户的风险等级。

接口名 描述
LoginWithHuaweiIDButtonParams LoginWithHuaweiIDButton组件参数,支持传入riskLevel字段(可选),标识一键登录后可查询用户风险等级。
开发前提

在进行代码开发前,请确认已完成一键登录开发前提工作。

应用在使用获取风险等级能力之前,需要完成对应的scope权限申请。

scope权限申请审批未完成或未通过,将报错1001502014 应用未申请scopes或permissions权限。当前可通过发送邮件至accountkit@huawei.com进行申请。

请提供如下信息进行申请,我们会在1-2个工作日内回复申请结果,请您留意邮箱消息。

邮件主题:【获取风险等级】权限申请

邮件正文:(请在正文中描述下具体希望申请的权限)

企业名称:***

应用名称:***

应用包名:com..

APP ID:1****12

Client ID:1****14

背景介绍:(请提供应用简单介绍,便于快速了解)

使用场景:(请提供相关使用场景的文字描述、交互流程图或参考交互视频等,可提供类似应用的使用场景进行说明)

使用该权限的必要性:(请提供应用需要该权限和信息的必要性)

客户端开发

一键登录前置流程(获取系统账号登录状态,获取系统账号匿名手机号)请参考一键登录开发流程中的导入模块及获取匿名手机号,确保系统账号已登录,匿名手机号获取成功,且用户首次通过华为账号登录该应用。

参考一键登录开发流程中展示一键登录页面并获取Authorization Code的示例代码,在LoginWithHuaweiIDButton组件参数params中设置riskLevel标识为true,其余示例代码保持不变,拉起应用登录页。

typescript 复制代码
LoginWithHuaweiIDButton({
  params: {
    // LoginWithHuaweiIDButton支持的样式
    style: loginComponentManager.Style.BUTTON_RED,
    // 账号登录按钮在登录过程中展示加载态
    extraStyle: {
      buttonStyle: new loginComponentManager.ButtonStyle().loadingStyle({
        show: true
      })
    },
    // LoginWithHuaweiIDButton的边框圆角半径
    borderRadius: 24,
    // LoginWithHuaweiIDButton支持的登录类型
    loginType: loginComponentManager.LoginType.QUICK_LOGIN,
    // LoginWithHuaweiIDButton支持按钮的样式跟随系统深浅色模式切换
    supportDarkMode: true,
    // verifyPhoneNumber:如果华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面
    verifyPhoneNumber: true,
    // riskLevel:标识应用期望在登录后获取华为账号的风险等级
    riskLevel: true,
  },
  controller: this.controller
})

用户同意协议并点击一键登录按钮后,可获取到Authorization Code,将该值传给应用服务端用于获取用户风险等级。

服务端开发

应用服务端使用Client ID、Client Secret、Authorization Code调用获取用户级凭证接口向华为账号服务器请求获取Access Token、Refresh Token。

使用Access Token调用获取用户风险等级接口获取用户的风险等级。

Access Token过期处理

由于Access Token的有效期仅为60分钟,当Access Token失效或者即将失效时(可通过REST API错误码判断),可以使用Refresh Token(有效期180天)通过刷新用户级凭证接口向华为账号服务器请求获取新的Access Token。

说明

当Access Token失效时,若应用不使用Refresh Token向华为账号服务器请求获取新的Access Token,账号的授权信息将会失效,导致使用Access Token的功能都会失败。

当Access Token非正常失效(如修改密码、退出账号、删除设备)时,应用可重新登录授权获取Authorization Code,向华为账号服务器请求获取新的Access Token。

Refresh Token过期处理

由于Refresh Token的有效期为180天,当Refresh Token失效后(可通过REST API错误码判断),应用服务端需要通知客户端,重新调用授权接口,请求用户重新授权。

应用基于风险等级判别用户风险程度,决定是否需要对用户进行额外验证或拦截用户行为。

华为账号其他方式登录获取用户风险等级
场景介绍

应用已使用华为账号关联登录场景下,开展商户补贴、优惠券发放等商业营销活动时获取华为账号风险等级,有效识别"薅羊毛"风险,保护营销资源合理使用,降低业务安全问题给营销方带来的损失,为相关活动保驾护航。以下对Account Kit提供的获取用户风险等级能力进行介绍,如果需要同时获取风险等级和手机号还可参考场景化控件获取手机号和风险等级Button进行实现。

约束与限制

获取用户风险等级scope仅支持与openid、phone、profile组合使用,接口支持的全量scopes见scope列表。

获取风险等级能力支持Phone、Tablet、PC/2in1设备。并且从5.1.0(18)版本开始,新增支持Wearable设备;从5.1.1(19)版本开始,新增支持TV设备。

业务流程

流程说明:

应用通过传对应scope和permission调用授权API,如果已授权则直接返回临时登录凭证Authorization Code,如果未授权:

scopes传入riskLevel,则授权API直接返回Authorization Code。

scopes传入riskLevel、profile/phone,则拉起授权页,用户点击允许后授权API返回Authorization Code。

将Authorization Code传给应用服务端,使用Client ID、Client Secret、Authorization Code从华为账号服务器中获取Access Token,再使用Access Token请求获取用户的风险等级。

接口说明
接口名 描述
createAuthorizationWithHuaweiIDRequest(): AuthorizationWithHuaweiIDRequest 获取授权接口,通过AuthorizationWithHuaweiIDRequest传入风险等级的scope:riskLevel及Authorization Code的permission:serviceauthcode,即可在授权结果中获取到Authorization Code。
constructor(context?: common.Context) 创建授权请求Controller。
executeRequest(request: AuthenticationRequest): Promise 通过Promise方式执行授权操作。可从AuthenticationResponse的子类AuthorizationWithHuaweiIDResponse中解析AuthorizationWithHuaweiIDCredential,其中包含authorizedScopes,可确认风险等级是否授权成功。具体解析方法请参考客户端开发的示例代码。
开发前提

在进行代码开发前,请确保已按照"开发准备"章节中的指导完成配置签名和指纹、配置Client ID。

应用在使用获取风险等级能力之前,需要完成对应的scope权限申请。

scope权限申请审批未完成或未通过,将报错1001502014 应用未申请scopes或permissions权限。当前可通过发送邮件至accountkit@huawei.com进行申请。

请提供如下信息进行申请,我们会在1-2个工作日内回复申请结果,请您留意邮箱消息。

邮件主题:【获取风险等级】权限申请

邮件正文:(请在正文中描述下具体希望申请的权限)

企业名称:

相关推荐
EatFan2 小时前
Flutter 鸿蒙适配实践 2026:从环境搭建、混合渲染到 OpenTelemetry 性能基建
flutter·华为·harmonyos·openharmony·opentelemetry·鸿蒙适配·impeller
m0_738185822 小时前
Flutter 鸿蒙化实战:flutter_secure_storage 适配 OpenHarmony,敏感数据安全存储
安全·flutter·华为·harmonyos·鸿蒙
李游Leo2 小时前
HarmonyOS 7 HiAppEvent + AppGallery Connect:审核复现链路的脱敏日志切片与证据校验【鸿蒙心迹】
华为·harmonyos
李游Leo2 小时前
HarmonyOS 7 + Core Vision Kit:文搜图索引代际切换与模型升级回滚【鸿蒙心迹】
华为·harmonyos
李游Leo2 小时前
HarmonyOS 7 + EasyGo-ArkUI VisibleArea:平行视界双窗曝光事件的去重与停留门禁【鸿蒙心迹】
华为·harmonyos
HwJack2014 小时前
【共创稿事节】HarmonyOS 7空间信息层级:焦点、景深与注意力引导
3d·华为·harmonyos·空间计算
zhangfeng113317 小时前
Reward Hacking 奖励钻空子 / 奖励作弊 Specification Gaming(规范博弈)
人工智能·华为·ai编程·npu
特立独行的猫a19 小时前
用仓颉写一个 Tauri:IPC 的每次往返实现原理(web层到仓颉层的触发过程)
前端·ui·harmonyos·tauri·鸿蒙·仓颉
2501_9197490319 小时前
华为鸿蒙桌面时钟与悬浮时钟APP—小羊时钟
华为·harmonyos