标签 Account Kit 下的文章

当应用平台组织诸如秒杀、抽奖等营销活动时,经常会遭遇"薅羊毛"行为,给业务方带来不小的经费损失。比如通过虚假手机号进行批量注册,多次参加活动;又比如,当应用商户进行红包补贴、优惠券发放等营销活动时,使用脚本或模拟器"薅羊毛"。

为避免该问题,HarmonyOS SDK华为账号服务(Account Kit)提供了获取用户风险等级的能力,能够有效识别恶意场景,提前防范业务风险。

应用场景

一、应用登录风控场景:

当用户使用华为账号关联登录应用时,开发者可通过华为账号获取用户风险等级的能力获取用户账号的风险等级,对高风险等级账号进行风控,提升应用的安全等级。

二、营销活动反作弊场景:

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

风险等级

获取用户风险等级方式

一、 通过华为账号一键登录获取用户风险等级。

在应用登录风控场景中,开发者可以通过华为账号一键登录获取用户风险等级,对恶意账号进行风控,提升应用的安全等级。
大致业务流程如下:

通过华为账号一键登录获取用户风险等级的开发,需要建立在一键登录的开发基础上。在进行代码开发前,请确认已经完成一键登录的开发准备工作,然后申请对应的scope权限,接着就可以进行客户端部分的开发。

在客户端开发部分,需要参考一键登录开发流程步骤1及步骤2,确保系统账号已登录,匿名手机号获取成功,且用户首次通过华为账号登录该应用。接着再参考步骤3的示例代码,在LoginWithHuaweiIDButton组件参数params中设置riskLevel标识为true,其余示例代码保持不变,拉起应用登录页。

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,最后使用Access Token调用获取用户风险等级接口获取用户的风险等级。

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 获取用户风险等级
 */
@Slf4j
public class GetUserRiskLevelDemo {
    public static void main(String[] args) throws IOException {
        // 获取用户风险等级的接口URL
        String url = "https://account.cloud.huawei.com/user/getuserrisklevel";
        // 替换为您实际的Client ID
        String clientID = "<Client ID>";
        // 替换为您实际的transactionID
        String transactionID = "<transactionID>";
        // 替换为您实际的获取到的用户级凭证Access Token
        String accessToken = "<Access Token>";
        // 替换为您实际的scene
        String scene = "<scene>";
        JSONObject result = getUserRiskLevel(url, clientID, transactionID, accessToken, scene);
        // 解析获取errCode
        Integer errCode = result.getInteger("errCode");
        // 解析获取errMsg
        String errMsg = result.getString("errMsg");
        // 解析获取riskLevel
        Integer riskLevel = result.getInteger("riskLevel");
        // 解析获取riskTag
        JSONArray riskTag = result.getJSONArray("riskTag");
    }

    private static JSONObject getUserRiskLevel(String url, String clientID, String transactionID,
        String accessToken, String scene) throws IOException {
        HttpPost httpPost = new HttpPost(url + "?" + "clientID=" + clientID + "&transactionID=" + transactionID);
        Map<String, String> reqBody = new HashMap<>();
        reqBody.put("accessToken", accessToken);
        reqBody.put("scene", scene);
        httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
        httpPost.setEntity(CallUtils.wrapJsonEntity(reqBody));
        return CallUtils.toJsonObject(CallUtils.remoteCall(httpPost, (CloseableHttpResponse response, String rawBody) -> {
            int statusCode = response.getStatusLine().getStatusCode();
            // http状态码不是200,请求失败
            if (statusCode != 200) {
                return new IOException("call failed! http status code: " + statusCode + ", response data: " + rawBody);
            }
            // http状态码为200,解析响应的body,判断业务错误码
            JSONObject errorResponseBody = CallUtils.toJsonObject(rawBody);
            // 错误码
            Integer errCode = errorResponseBody.getInteger("errCode");
            // errCode为0表示成功,非0表示失败
            if (Objects.nonNull(errCode) && errCode != 0) {
                return new IOException("call failed! http status code: " + statusCode + ", response data: " + rawBody);
            }
            return null;
        }));
    }
}

二、 通过华为账号其他方式登录获取用户风险等级。

在应用已使用华为账号关联登录的场景中,开展商户补贴、优惠券发放等商业营销活动时,开发者可通过华为账号其他方式登录获取华为账号风险等级,有效识别"薅羊毛"风险,保护营销资源合理使用。

大致业务流程如下:

通过华为账号其他方式登录获取用户风险等级的开发步骤同样分为客户端开发和服务端开发。客户端开发步骤如下:

  1. 首先导入authentication模块及相关公共模块。

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

  2. 然后创建授权请求并设置参数。

    // 创建授权请求,并设置参数
    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
    // 获取风险等级需要传如下scope
    authRequest.scopes = ['riskLevel'];
    // 获取authorizationCode需传如下permission
    authRequest.permissions = ['serviceauthcode'];
    // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
    authRequest.forceAuthorization = true;
    // 用于防跨站点请求伪造
    authRequest.state = util.generateRandomUUID();

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

    // 执行授权请求
    try {
    // 此示例为代码片段,实际需在自定义组件实例中使用,以获取UIContext对象作为函数入参
    const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());
    controller.executeRequest(authRequest).then((data) => {

     const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;
     const state = authorizationWithHuaweiIDResponse.state;
     if (state && authRequest.state !== state) {
       hilog.error(0x0000, 'testTag', `Failed to authorize. The state is different, response state: ${state}`);
       return;
     }
     hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');
     let riskLevelAuthorized: boolean = false;
     const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;
     const authorizedScopes = authorizationWithHuaweiIDCredential?.authorizedScopes;
     // 判断授权成功scopes中是否包含riskLevel
     if (authorizedScopes?.includes("riskLevel")) {
         riskLevelAuthorized = true;
     }
     const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;
     // 开发者处理riskLevelAuthorized, authorizationCode

    }).catch((err: BusinessError) => {

     dealAllError(err);

    });
    } catch (error) {
    dealAllError(error);
    }
    // 错误处理
    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
    }

  4. 在客户端开发完成后,同样需要调用获取用户级凭证接口向华为账号服务器请求获取Access Token,并使用Access Token调用获取用户风险等级接口获取用户的风险等级。

了解更多详情\>\>

访问华为账号服务联盟官网

获取获取风险等级开发指导文档

背景

在开发“智能带办”应用时涉及到用户体系,开发阶段使用固定验证码形式跑通,在上线前准备接入短信服务时却遇到了难题,短信服务目前只对企业开发者开放了,个人开发者没办法再使用短信服务。为了顺利上架,退后求其次,改为了使用邮箱验证码等了。

邮箱验证码登录有两个弊端,一是不方便,很多用户进来发现是邮箱验证码登录不方便直接就退出应用了;二是合规风险,在申请安全评估报告时如果涉及到用户体系要求实名,邮箱没办法保证实名,还得再加入额外的实名体系,不仅麻烦而且很多都限制个人开发者没法使用。

其实最开始也考虑过要接入华为登录,看了一键登录文档发现也是只针对企业开发者,以为也是只有企业开发者可以使用,后面看了“华为账号登录”后发现个人开发者也可以使用,只是取不到手机号,正好不使用手机号可以规避合规方面的风险。
image.png

华为登录能力介绍

华为账号服务简介

Account Kit(华为账号服务)提供简单、快速、安全的登录功能,让用户快捷地使用华为账号登录应用。用户授权后,Account Kit可提供头像、昵称、手机号码等信息,帮助应用更了解用户。华为账号服务提供了登录、获取华为账号用户信息、未成年模式等。在开发过程中涉及下面几个概念:

  • OpenID:应用维度用户标识符,是华为账号用户在应用/元服务的唯一标识。不同应用/元服务(不管是否在同一个开发者账号下)获取到用户的OpenID不同。
  • UnionID:开发者维度用户标识符,华为账号用户同一开发者账号下的唯一标识。开发者有多个应用/元服务时,同一个开发者账号下的应用/元服务获取到用户的UnionID相同。
  • GroupUnionID:关联主体账号组维度用户标识符,是华为账号用户在关联主体账号组内的唯一标识。不同开发者账号加入同一关联主体账号组后,其组内所有开发者的应用/元服务获取到用户的GroupUnionID相同。
  • permission:数据或接口权限,通过该权限判断应用是否能获取对应数据或调用对应接口。
  • scopes:scope列表,用于获取用户数据。开发者向华为账号服务申请不同类型用户数据的标识。比如头像昵称(profile)、匿名手机号(quickLoginAnonymousPhone)等。
  • Authorization Code:授权码,用户使用华为账号登录成功之后,可通过返回的凭据解析出授权码,通过授权码可获取Access Token、Refresh Token、ID Token等。
  • Access Token:访问凭证,是访问被权限管控资源的应用级凭证。可使用Access Token调用获取用户信息接口获取用户信息。
  • ID Token:用户身份凭证,是OIDC (OpenID Connect) 协议相对于OAuth 2.0 协议扩展的一个用户身份凭证,包含用户信息。用户使用华为账号登录成功之后,可通过返回的凭据解析出Authorization Code、ID Token等数据。

在我们接口华为用户服务后,可以使用OpenId和UnionID绑定我们自己的账号体系。

华为账号服务交互流程

由于个人开发者无法使用“一键登录”,本文主要介绍 “华为账号登录”按钮登录。使用按钮登录我们可以使用Account Kit提供的华为账号登录按钮及服务端交互获取华为账号用户身份标识UnionID、OpenID,通过UnionID、OpenID完成用户登录;或者与应用账号完成绑定,绑定后用于登录或者验证。

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

账号服务开发者与华为能力交互流程如下图所示:
image.png

交互流程说明如下:
流程说明:

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

    1. 用户打开应用进行登录,应用设置LoginType类型为LoginType.ID后拉起应用自己的登录页并展示“华为账号登录”按钮,用户点击按钮,请求华为账号授权信息。
  2. 用户点击登录阶段(序号4-6):

    1. 如华为账号未登录,将拉起华为账号登录页,用户登录后,将返回Authorization Code等数据给应用。
    2. 如华为账号已登录,将直接返回Authorization Code等数据给应用。
  3. 用户关联应用账号阶段(序号7-16):

    1. 应用服务端通过Authorization Code获取到Access Token,再使用Access Token调用解析凭证接口获取用户相关信息。通过Authorization Code凭证获取用户信息可以有效避免黑客通过数据遍历、身份伪造、重放攻击等手段导致的安全风险。
    2. 应用服务端将业务登录凭证SessionId、UnionID/OpenID传给应用,应用获取到UnionID/OpenID可用于判断华为账号是否登录等功能。
    3. 应用对用户身份标识UnionID/OpenID、业务登录凭证SessionId信息进行认证后,通过UnionID/OpenID判断用户是否已关联应用系统数据库,如已关联,则完成用户登录;如未关联,则创建新用户,绑定UnionID/OpenID。

华为账号服务提供了LoginWithHuaweiIDButton组件,构造中需要传入LoginWithHuaweiIDButtonParams类型和 LoginWithHuaweiIDButtonController类型的参数,LoginWithHuaweiIDButtonParams属性如下:

名称类型只读可选说明
styleStyleLoginWithHuaweiIDButton组件的样式。支持样式包括:BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、ICON_RED、ICON_WHITE、ICON_WHITE_OUTLINE、ICON_BLACK、ICON_GRAY、BUTTON_GRAY、BUTTON_CUSTOM。
borderRadiusnumber按钮边框圆角半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:height属性取值的一半。单位:vp。
iconRadiusnumberIcon类型按钮的半径。取值范围:[0,+∞),值小于0时,按0处理。默认值:24。单位:vp。
supportDarkModeboolean表示按钮的样式是否随系统深浅色模式变化。true:按钮的样式会随着系统深浅色模式变化。false:按钮的样式不会随着系统深浅色模式变化。默认值:true。
loginTypeLoginType华为账号登录类型。默认值:LoginType.ID。一键登录请使用LoginType.QUICK_LOGIN。
textAndIconStyleboolean是否展示图文混合样式的华为账号登录按钮。true:按钮支持Icon和文字混合样式。false:按钮仅支持文本样式。默认值:false。当loginType不等于LoginType.QUICK_LOGIN且style等于BUTTON_RED、BUTTON_WHITE、BUTTON_WHITE_OUTLINE、BUTTON_BLACK、BUTTON_GRAY时该参数生效。起始版本:5.0.0(12)
customButtonParamsCustomButtonParamsBUTTON_CUSTOM按钮样式参数。起始版本:5.0.0(12)
verifyPhoneNumberboolean华为账号用户在过去90天内未进行短信验证,是否拉起Account Kit提供的短信验证码页面。true:拉起Account Kit提供的短信验证码页面。false:不拉起Account Kit提供的短信验证码页面。需要应用验证手机号时效性。默认值:true。起始版本:5.0.0(12)
extraStyleExtraStyle如果应用想使用华为账号提供的固定样式之外的效果,可使用此接口自定义按钮样式。起始版本:5.0.0(12)
loginButtonTextTypeLoginButtonTextType当loginType为LoginType.QUICK_LOGIN时,可传入此参数,控制按钮文本内容显示。默认值:LoginButtonTextType.QUICK_LOGIN。当该参数为LoginButtonTextType.QUICK_LOGIN时,按钮文本内容显示“华为账号一键登录”。当该参数为LoginButtonTextType.QUICK_REGISTRATION时,按钮文本内容显示“华为账号一键注册”。起始版本:5.0.0(12)
riskLevelboolean是否需要获取华为账号用户风险等级。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:需要获取用户风险等级。false:不获取用户风险等级。默认值:false。起始版本:5.1.0(18)
securityVerificationboolean用户开启华为账号一键登录增强身份验证后,应用会在登录过程中通过华为账号使用生物识别或短信进行身份验证。如果需要获取用户一键登录增强身份验证的开关状态,需设置该字段为false。仅登录类型为LoginType.QUICK_LOGIN时需要设置该参数。true:响应结果HuaweiIDCredential将不会返回 enableSecurityVerification。false:响应结果HuaweiIDCredential将返回 enableSecurityVerification。默认值:true。起始版本:6.0.0(20)

智能带办接入过程

目前应用只支持华为登录,页面UI如下:
image.png

在页面中配置红色的LoginWithHuaweiIDButton:

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%')  
.margin({top:50})  
.padding({left:25, right:25})

控制器controller定义如下:

controller: loginComponentManager.LoginWithHuaweiIDButtonController =  
  new loginComponentManager.LoginWithHuaweiIDButtonController()  
    .setAgreementStatus(loginComponentManager.AgreementStatus.NOT_ACCEPTED)  
    .onClickLoginWithHuaweiIDButton((error: BusinessError, response: loginComponentManager.HuaweiIDCredential) => {  
      if (error) {  
        this.dealAllError(error);  
        return;  
      }  
  
      if (response) {  
        Logger.i(TAG, 'Succeeded in getting response.');  
        const authCode = response.authorizationCode;  
        // 开发者处理authCode  
        this.getUserInfoPermission(authCode)  
      }  
    });

在controller中获取回调,如果登录成功则通过authorizationCode继续申请用户华为头像和昵称授权:

getUserInfoPermission(authCode:string){  
  // 创建授权请求,并设置参数  
  const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();  
  // 获取头像昵称需要传如下scope  
  authRequest.scopes = ['profile'];  
  // 若开发者需要进行服务端开发以获取头像昵称,则需传如下permission获取authorizationCode  
  authRequest.permissions = ['serviceauthcode'];  
  // 用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面  
  authRequest.forceAuthorization = true;  
  // 用于防跨站点请求伪造  
  authRequest.state = util.generateRandomUUID();  
  // 执行授权请求  
  try {  
    const controller = new authentication.AuthenticationController(this.getUIContext().getHostContext());  
    controller.executeRequest(authRequest).then((data) => {  
      const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;  
      const state = authorizationWithHuaweiIDResponse.state;  
      if (state && authRequest.state !== state) {  
        Logger.i(TAG, `Failed to authorize. The state is different, response state: ${state}`);  
        return;  
      }  
      Logger.i(TAG,'Succeeded in authentication.');  
      const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse?.data;  
      const avatarUri = authorizationWithHuaweiIDCredential?.avatarUri;  
      const nickName = authorizationWithHuaweiIDCredential?.nickName;  
      // 开发者处理avatarUri, nickName  
      const authorizationCode = authorizationWithHuaweiIDCredential?.authorizationCode;  
      Logger.i(TAG, 'getUserInfoPermission:' + JsonUtils.toJSONString(authorizationWithHuaweiIDCredential))  
      this.sendLoginRequest(authorizationCode??authCode)  
      // 涉及服务端开发以获取头像昵称场景,开发者处理authorizationCode  
    }).catch((err: BusinessError) => {  
      this.dealAllError(err);  
    });  
  } catch (error) {  
    this.dealAllError(error);  
  }  
}

用户授权成功后请求服务端接口,服务端通过authorizationCode调用华为服务获取accessToken,接着获取用户信息,绑定自己的账号体系返回自己账号体系的token即可。通过下面接口获取用户级凭证:

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

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

接着通过下面示例获取用户昵称和头像:

POST /rest.php?nsp_svc=GOpen.User.getInfo HTTP/1.1
Host: account.cloud.huawei.com
Content-Type: application/x-www-form-urlencoded

access_token=<Access Token>

必须在手机上调起授权获取用户授权后这里才可以请求到用户头像和昵称。

总结

本次“智能带办”应用的登录体系接入实践,源于上线前短信服务仅对企业开发者开放的限制,迫使我们从固定验证码、邮箱验证码转向华为账号登录方案。初期因误判“一键登录”仅限企业开发者而忽略“华为账号登录”,后发现个人开发者虽无法获取手机号,但恰好规避了邮箱登录的用户体验差(用户因不便退出)与实名合规风险(需额外实名体系),成为关键破局点。

华为账号服务(Account Kit)通过OpenID(应用唯一标识)、UnionID(开发者唯一标识)等核心概念,为个人开发者提供了安全高效的登录能力:既支持自定义样式的登录按钮(如本文配置的红色BUTTON_RED按钮),又通过Authorization CodeAccess Token→用户信息的流程保障安全,避免身份伪造等风险。接入过程中,我们通过LoginWithHuaweiIDButton组件实现前端交互,结合服务端解析凭证绑定自有账号体系,最终完成用户登录闭环。

此次实践的核心启示在于:面对企业级服务限制时,需深度挖掘平台对个人开发者的差异化能力——华为账号登录虽不提供手机号,却以“去实名化”特性解决了合规痛点,同时依托成熟的OAuth 2.0/OIDC协议与丰富组件(如支持深色模式、自定义圆角的按钮),兼顾了开发效率与用户体验。未来,可进一步探索UnionID在多应用间的用户打通能力,或结合GroupUnionID拓展关联主体场景,持续完善登录体系的灵活性与扩展性。