Harmony应用集成SDK

您需要在应用中集成SDK,才能在控制台BOT管理中配置App防爬场景化规则。本文介绍了如何为Harmony应用集成WAF App防护SDK(以下简称SDK)。

背景信息

App防护SDK主要用于对通过App客户端发起的请求进行签名。WAF服务端通过校验App请求签名,识别App业务中的风险、拦截恶意请求,实现App防护的目的。

关于App防护提供的SDK所涉及的隐私政策,请参见Web应用防火墙App防护SDK隐私政策

使用限制

  • 需要在Harmony Next 4.1及以上版本的系统运行,API版本最低支持12。

  • init初始化接口存在耗时操作,调用后不能立即同步调用vmpSign接口,请确保SDK的初始化接口和签名接口调用时间间隔2以上。

  • 滑块创建cptCreate涉及UI操作,需要在主线程中进行调用。

  • 不支持模拟器模式调试。

  • 仅支持开启字节码打包方案。

前提条件

  • 已获取Harmony应用对应的SDK。

    获取方法:请提交工单,联系产品技术专家获取SDK。

    说明

    Harmony应用对应的SDK包含1HAR文件,文件名为HarmonyOS-AliTigerTally-X.Y.Z-date.har,其中X.Y.Z表示版本号,date表示打包日期。

  • 已获取SDK认证密钥(即appkey)。

    开启BOT管理后,即可在BOT管理 > App防护列表中,单击获取并复制appkey,获取SDK认证密钥。该密钥用于发起SDK初始化请求,需要在集成代码中使用。

    image

    说明

    每个阿里云账号拥有唯一的appkey(适用于所有接入WAF防护的域名),且Android、iOSHarmony应用集成SDK时都使用该appkey。

    认证密钥示例:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。

步骤一:新建工程

DevEco Studio工具为例,新建一个Harmony工程,并按照配置向导完成创建。创建好的工程目录如下图所示。image.png

步骤二:集成HAR

  1. HarmonyOS-AliyunTigerTally-X.Y.Z-date.tgzSDK包解压,将获取到的HAR文件拷贝到工程中存放HAR包的目录。

    建议参考鸿蒙官方文档放置于libs目录下。image.png

  2. 打开Appoh-package.json5文件,在dependencies中添aliyuntigertally、captcha编译依赖,示例如下:

    重要

    您需要将aliyuntigertally-X.Y.Z-date.har文件的版本号X.Y.Z替换成您获取的HAR文件的版本号。

    {
      ...
      "dependencies": {
        "aliyuntigertally": "file:../libs/aliyuntigertally-X.Y.Z-date.har",
        "captcha": "file:../libs/aliyuncaptcha-X.Y.Z-date.har",
        ...
      }
    }

步骤三:为应用申请权限

为增强SDK的防护效果,当前需要以下权限:

权限

是否必须

说明

ohos.permission.INTERNET

联网权限。SDK需要联网才能使用。

ohos.permission.GET_NETWORK_INFO

网络状态确认。SDK可以根据网络状态提供更好的服务。

ohos.permission.STORE_PERSISTENT_DATA

否(推荐赋予)

允许应用存储持久化的数据。SDK可以增加设备指纹稳定性。

ohos.permission.DISTRIBUTED_DATASYNC

否(推荐赋予)

多设备协同。SDK可以检测多设备状态,增强安全效果。

ohos.permission.APP_TRACKING_CONSENT

否(推荐赋予)

获取广告标识符权限。SDK获取IDFA信息,增强设备ID稳定性。

步骤四:添加集成代码

1. 添加头文件

import { TigerTallyAPI, TTCode } from 'aliyuntigertally';
import { TTCaptcha, TTCaptchaOption, TTCaptchaListener} from 'aliyuntigertally';

2. 设置数据签名

  1. 设置业务自定义的终端用户标识,方便您更灵活地配置WAF防护策略。

    /**
     * 设置用户账户
     *
     * @param account 账户
     * @return 错误码
     */
    public static setAccount(account: string): number
    • 参数说明

      • account,string类型,表示标识一个用户的字符串,建议您使用脱敏后的格式。

    • 返回值:number类型,返回是否设置成功,0表示成功,-1表示失败。

    • 示例代码

      // 游客身份可以暂时先不setAccount,直接初始化;登录以后调用setAccount和重新初始化let account: string = "user001";
      TigerTallyAPI.setAccount(account);
  2. 初始化SDK,执行一次初始化采集。

    一次初始化采集表示采集一次终端设备信息,您可以根据业务的不同,重新调用init函数进行初始化采集。

    /**
     * 初始化回调
     */
    export interface TTInitListener {
      /**
       * SDK状态码回调
       * @param code
       */
      onInitFinish: (code: number) => void;
    }
    
    /**
     * SDK 初始化,带 callback
     *
     * @param ctx
     * @param userAppKey
     * @param options 各类参数选项
     * @param listener
     */
    public static init(ctx: Context, userAppKey: string,
                       options: Map<string, string> | null,
                       listener: TTInitListener | null): number
    • 参数说明

      • ctx:Context类型,传入您应用的上下文。

      • userAppkey:string类型,设置为您的SDK认证密钥。

      • options:Map<String,String>类型,信息采集可选项,默认可以为null。可选参数如下

        字段名

        说明

        示例

        IPv6

        是否使用IPv6域名上报设备信息。

        1. 0:使用IPv4域名(默认值)。

        2. 1:使用IPv6域名。

        1

        Intl

        是否使用国际域名上报设备信息。

        1. 0:中国内地上报(默认值)。

        2. 1:非中国内地上报。

        1

      • listener:TTInitListener类型,SDK初始化回调接口,可在回调中判断初始化结果的具体状态,默认可以传null。

        TTCode

        Code

        备注

        TT_SUCCESS

        0

        SDK初始化成功

        TT_NOT_INIT

        -1

        SDK未调用初始化

        TT_NOT_PERMISSION

        -2

        SDK需要的Harmony基础权限未完全授权

        TT_UNKNOWN_ERROR

        -3

        系统未知错误

        TT_NETWORK_ERROR

        -4

        网络错误

        TT_NETWORK_ERROR_EMPTY

        -5

        网络错误,返回内容为空串

        TT_NETWORK_ERROR_INVALID

        -6

        网络返回的格式非法

        TT_PARSE_SRV_CFG_ERROR

        -7

        服务端配置解析失败

        TT_NETWORK_RET_CODE_ERROR

        -8

        网关返回失败

        TT_APPKEY_EMPTY

        -9

        AppKey为空

        TT_PARAMS_ERROR

        -10

        其他参数错误

        TT_FGKEY_ERROR

        -11

        密钥计算错误

        TT_APPKEY_ERROR

        -12

        SDK版本和AppKey版本不匹配

    • 返回值:number类型,返回初始化结果,0表示成功,-1表示失败。

    • 示例代码

      // appkey代表阿里云客户平台分配的认证密钥
      const appkey: string = "******";
      // 可选参数, 可配置IPv6与国际上报
      let options: Map<string, string> = new Map<string, string>();
      options.set("IPv6", "0");// 配置为IPv6
      options.set("Intl", "0");// 配置为中国内地上报
      
      // 一次初始化采集,代表一次设备信息采集,可以根据业务的不同,重新调用函数init初始化采集
      let ret: number = TigerTallyAPI.init(getContext(this), appkey, options, null);
  3. 数据签名。

    对输入数据input进行签名处理,并且返回wToken字符串用于请求认证。

    /**
     * 数据签名
     *
     * @param type  数据类型
     * @param input 签名数据
     * @return wToken
     */public static vmpSign(type: number, input: string): string
    • 参数说明

      • type:number类型,设置数据签名类型,固定取值0

      • input:string类型,表示待签名的数据,一般是整个请求体request body

    • 返回值:string类型,返回wToken字符串。

    • 示例代码

      let body: string = "i am the request body, encrypted or not!";
      let wtoken: string = TigerTallyAPI.vmpSign(0, body);
      说明
      1. wToken为以下字符串时表示初始化流程存在异常:

        1. you must call init first:表示未调用init函数。

        2. you must input correct data:表示传入数据错误。

        3. you must input correct type:表示传入类型错误。

        4. inner error:表示系统内部错误。

3. 二次校验

  1. 判断结果。

    根据responsecookiebody字段判断是否要进行二次校验。header中可能存在多个Set-Cookie

    /**
     * 判断是否进行二次校验
     *
     * @param cookie  cookie
     * @param body    body
     * @return 0:通过 1:二次校验
     */
    public static cptCheck(cookie: string, body: string): number
    • 参数说明

      • cookie:string类型,设置请求response中全部cookie

      • body:string类型,设置请求response中全部body

    • 返回值:number类型,返回决策结果,0表示通过,1表示需要二次校验。

    • 示例代码

      let cookie: string = "key1=value1;kye2=value2;";
      let body: string = "....";
      let recheck: number = TigerTallyAPI.cptCheck(cookie, body);
  2. 创建滑块。

    根据cptCheck返回结果决定是否要创建一个滑块对象,TTCaptcha对象提供showdismiss方法,对应显示滑块和隐藏滑块窗口。TTCaptchaOption封装了滑块可配置的参数,TTCaptchaListener包含了滑块的2种回调状态。

    /**
     * 创建滑块对象
     *
     * @param ctx
     * @param option    参数
     * @param listener  回调
     * @return 滑块验证对象
     */
    public static cptCreate(ctx: UIContext, option: TTCaptchaOption, listener: TTCaptchaListener): TTCaptcha
    
      
    /**
     * 滑块对象
     */
    export class TTCaptcha {
        /**
         * 显示滑块
         */
        public show(): void
    
        /**
         * 隐藏滑块
         */
        public dismiss(): void
    }
    
    /**
     * 滑块参数
     */
    export class TTCaptchaOption {
      // 是否支持点击空白处隐藏滑块
      cancelable: boolean;
    
    }
    
    /**
     * 滑块回调
     */
    export interface TTCaptchaListener {
      /**
       * 验证成功
       *
       * @param captcha 滑块对象
       * @param code, 默认为 traceId
       */
      success: (captcha: TTCaptcha, code: string) => void;
    
      /**
       * 验证失败或异常
       *
       * @param captcha 滑块对象
       * @param code    错误码
       */
      fail: (captcha: TTCaptcha, code: string) => void;
    }
    
    • 参数说明

      • ctx:UIContext类型,设置当前页面UIContext。

      • option:TTCaptchaOption类型,设置滑块配置参数。

      • listener:TTCaptchaListener类型,设置滑块状态回调。

    • 返回值:TTCaptcha类型,返回滑块对象。

    • 示例代码

      let option: TTCaptchaOption = new TTCaptchaOption();
      // option.cancelable = false;
      
      let captcha: TTCaptcha = TigerTallyAPI.cptCreate(this.getUIContext(), option, {
        success: (captcha: TTCaptcha, code: string) => {
          console.log("captcha success:", code);
          captcha.dismiss();
        },
        fail: (captcha: TTCaptcha, code: string) => {
          console.log("captcha failed:", code);
          captcha.dismiss();
        }
      });
      captcha.show();
      说明
      • 创建滑块cptCreate接口涉及UI操作,需要在主线程中调用。

      • 验证异常,表示在加载滑块过程中检测到异常情况;验证失败,表示用户滑动结束后检测到异常情况。具体错误码如下所示:

        • 1001:验证失败判定不通过。

        • 1002:系统异常。

        • 1003:参数错误。

        • 1005:验证取消。

        • 8001:滑块唤起错误。

        • 8002:滑块验证异常。

        • 8004:网络错误。

最佳实践示例

import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';

import { TigerTallyAPI, TTCode } from 'aliyuntigertally';
import { TTCaptcha, TTCaptchaOption, TTCaptchaListener} from 'aliyuntigertally';

@Entry
@Component
struct Index {
  
  build() {
    ...
  }
  
  aboutToAppear() {
    this.onTest();
  }

  async onTest() {
    
    const APP_KEY: string = "xxxxxx";
    const APP_URL: string = "xxxxxx";
    const APP_HOST: string = "xxxxxx";
    
    // 初始化
    let options: Map<string, string> = new Map<string, string>();
    options.set("IPv6", "0");// 配置为IPv6
    options.set("Intl", "0");// 配置为中国内地上报
    let retCode: number = TigerTallyAPI.init(getContext(this), APP_KEY, options, null);
    console.log("TigerTally init:", retCode);

    // 不能立即同步调用
    const sleep = (duration: number) => {
      return new Promise<void>(resolve => setTimeout(resolve, duration));
    };
    await sleep(2000);

    // 数据签名
    let data: string = "i am the request body, encrypted or not!";
    let wtoken: string = TigerTallyAPI.vmpSign(0, data);
    console.log("TigerTally vmpSign:", wtoken);

    // 请求接口
    this.doPost(APP_URL, APP_HOST, wtoken, data, (code, cookie, body) => {
      // 判断是否需要显示滑块
      let recheck: number = TigerTallyAPI.cptCheck(cookie, body);
      console.log("TigerTally captcha check:", recheck);

      if (recheck === 0) return;
      this.doShow();
    });
  }

  // 显示滑块
  async doShow() {
    console.log("滑块显示");

    let option: TTCaptchaOption = new TTCaptchaOption();
    // option.cancelable = false;

    let captcha: TTCaptcha = TigerTallyAPI.cptCreate(this.getUIContext(), option, {
      success: (captcha: TTCaptcha, code: string) => {
        console.log("captcha success:", code);
        captcha.dismiss();
      },
      fail: (captcha: TTCaptcha, code: string) => {
        console.log("captcha failed:", code);
        captcha.dismiss();
      }
    });
    captcha.show();
  }

  // 发送请求
  async doPost(url: string, host: string, wtoken: string, body: string,
    callback: (code: number, cookie: string, body: string) => void): Promise<void> {
    let response_code: number = 0;
    let response_body: string = "";
    let response_cookie: string = "";

    try {
      let httpRequest = http.createHttp();
      let response: http.HttpResponse = await new Promise<http.HttpResponse>((resolve, reject) => {
        httpRequest.request(
          url,
          {
            method: http.RequestMethod.POST,
            header: {
              "Content-Type": "text/x-markdown",
              "User-Agent": "",
              "Host": host,
              "wToken": wtoken,
            },
            extraData: body.length > 0 ? body : undefined,
            connectTimeout: 12000,
          },
          (err: BusinessError, data: http.HttpResponse) => {
            if (!err) {
              resolve(data);
            } else {
              reject(err);
            }
            httpRequest.destroy();
          }
        );
      });

      if (response != null) {
        response_code = response.responseCode;
        let success: boolean = (response_code === 200);

        if (success) {
          response_body = response.result ? response.result.toString() : "";
          response_cookie = response.header["set-cookie"] ? response.header["set-cookie"].join(";") : "";
        } else {
          response_body = response.result ? response.result.toString() : "";
        }
      } else {
        response_code = -1;
      }

      console.log("response code:", response_code);
      console.log("response body:", response_body);
      console.log("response cookie:", response_cookie);

    } catch (error) {
      console.log("response error:", error.code, error.message);
      response_code = -1;
      response_body = error.message;
    } finally {
      if (callback != null) {
        callback(response_code, response_cookie, response_body);
      }
    }
  }
}