Harmony应用集成SDK

更新时间:
复制为 MD 格式

您需要在应用中集成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包含2HAR文件,文件名为AliTigerTally_X.Y.Z.har、AliCaptcha_X.Y.Z.har,其中X.Y.Z表示版本号。

  • 已获取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. 将获取到的SDK文件tigertally-X.Y.Z-xxxxxx-harmony.tgz包解压,将获取到的HAR文件拷贝到工程中存放HAR包的目录。image

  2. 打开Appoh-package.json5文件,在dependencies中添加@aliyun/tigertally、@aliyun/captcha编译依赖,示例如下:
    建议参考鸿蒙官方文档放至libs目录下。

    重要

    您需要将AliTigerTally_X.Y.Z.har、AliCaptcha_X.Y.Z.har文件的版本号X.Y.Z替换成您获取的HAR文件的版本号。

    {
      ...
      "dependencies": {
        "@aliyun/tigertally": "file:../libs/AliTigerTally_X.Y.Z.har",
        "@aliyun/captcha": "file:../libs/AliCaptcha_X.Y.Z.har",
        ...
      }
    }

步骤三:为应用申请权限

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

权限

是否必须

说明

ohos.permission.INTERNET

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

ohos.permission.GET_NETWORK_INFO

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

ohos.permission.STORE_PERSISTENT_DATA

否(推荐赋予)

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

步骤四:添加集成代码

1. 添加头文件

import { TTCode, TTInitListener, TigerTallyAPI } from '@aliyun/tigertally';
import { TTCaptcha, TTCaptchaListener, TTOption } from '@aliyun/tigertally';

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函数进行初始化采集。

    初始化采集分为三种模式:全量采集、自定义隐私采集、非隐私采集(不采集涉及终端设备用户隐私的字段,包括:odid)。

    说明

    建议在符合内部合规要求的前提下,选择适配的采集模式,确保数据采集的完整性。完整数据有助于更有效地识别潜在风险。

    /**
     * 初始化回调
     */
    export interface TTInitListener {
      /**
       * SDK状态码回调
       * @param code 接口调用状态码
       */
      onInitFinish: (code: number) => void;
    }
    
    /**
    * SDK 初始化,带 callback
    * @param ctx
    * @param appkey 密钥
    * @param collectType 采集数据的类型
    * @param options 各类参数选项
    * @param listener
    * @return 错误码
    */
    public static init(context: Context, appKey: string, collectType: number, 
                       options: Map<string, string> | null, 
                       listener: TTInitListener | null): number
    • 参数说明

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

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

      • collectType:number类型,设置采集模式。取值:

        字段名

        说明

        示例

        TT_DEFAULT

        表示采集全量数据。

        TigerTallyAPI.TT_DEFAULT

        TT_NO_BASIC_DATA

        表示不采集基础设备数据。

        包括:设备名(Build.DEVICE)、 Harmony系统版本号(Build.VERSION#RELEASE)。

        TigerTallyAPI.X | TigerTallyAPI.Y

        (表示既不采集X又不采集Y, X、Y表示具体某项的字段名)

        TT_NO_UNIQUE_DATA

        表示不采集唯一标识数据。

        包括:ODID。

        TT_NOT_GRANTED

        表示不采集以上所有隐私数据。

        TigerTallyAPI.TT_NOT_GRANTED

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

        字段名

        说明

        示例

        IPv6

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

        • 0(默认):使用IPv4域名。

        • 1:使用IPv6域名。

        "1"

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

        TTCode

        Code

        备注

        TT_SUCCESS

        0

        SDK初始化成功

        TT_NOT_INIT

        -1

        SDK未调用初始化

        TT_NOT_PERMISSION

        -2

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

        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");// 配置为IPv4
      
      // 一次初始化采集,代表一次设备信息采集,可以根据业务的不同,重新调用函数init初始化采集
      // 全量采集
      let ret: number = TigerTallyAPI.init(getContext(this), appkey, TigerTallyAPI.TT_DEFAULT, options, null);
      
      // 指定隐私数据采集,不同的隐私数据可以通过"|"进行拼接
      let privacyFlag: number = TigerTallyAPI.TT_NO_BASIC_DATA | TigerTallyAPI.TT_NO_UNIQUE_DATA;
      let ret: number = TigerTallyAPI.init(getContext(this), appkey, privacyFlag, options, null);
      
      // 不采集隐私字段
      let ret: number = TigerTallyAPI.init(getContext(this), appkey, TigerTallyAPI.TT_NOT_GRANTED, options, null);
      console.log("ret:" + ret);
  3. 数据哈希。

    自定义加签接口对输入数据 input 执行哈希计算,生成并返回 whash 字符串作为自定义签名。 

    • 对于 POST、PUT 和 PATCH 请求,input 为请求体(request body)内容。 

    • 对于 GET 和 DELETE 请求,input 为完整的 URL 地址。

    生成的 whash 字符串须添加至 HTTP 请求头字段 ali_sign_whash 中。

    /**
    * 请求类型
    */
    public static GET: number = 0;
    public static POST: number = 1;
    public static PUT: number = 2;
    public static PATCH: number = 3;
    public static DELETE: number = 4;
    
    /**
    * 自定义Hash签名数据
    *
    * @param type  数据类型
    * @param input 哈希数据
    * @return whash
    */
    public static vmpHash(type: number, input: string): string
    • 参数说明:

      • type:RequestType类型,设置数据类型。取值:

        • GET:表示Get请求数据。

          • POST:表示Post请求数据。

          • PUT:表示Put请求数据。

          • PATCH:表示Patch请求数据。

          • DELETE:表示Delete请求数据。

        • input:string类型,表示待加签的数据。

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

    • 示例代码

      // get 请求
      let url: string = "https://tigertally.aliyun.com/apptest";
      let whash: string = TigerTallyAPI.vmpHash(TigerTallyAPI.GET, url);
      console.log("whash:" + whash);
      
      // post 请求
      let body: string = "hello world";
      let whash: string = TigerTallyAPI.vmpHash(TigerTallyAPI.POST, body);
      console.log("whash:" + whash);
      说明

      控制台勾选默认签名不需要调用该接口,勾选自定义加签时需要在数据签名前调用该接口进行哈希校验。

  4. 数据签名。

    使用 VMP 技术对输入数据 input 进行签名处理,生成并返回 wtoken 字符串,用于请求认证。

    /**
     * 数据签名
     *
     * @param input 签名数据
     * @return wtoken
     */
    public static vmpSign(input: string): string
    • 参数说明:

      • input:string类型,表示待签名的数据,通常为完整的请求体(request body)或自定义加签生成的 whash

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

    • 示例代码

      // 控制台配置默认签名,即不勾选自定义加签
      let body: string = "i am the request body, encrypted or not!";
      let wtoken: string = TigerTallyAPI.vmpSign(body);
      console.log("wToken:" + wtoken);
      
      // 控制台配置自定义加签
      // get 请求
      let url: string = "https://tigertally.aliyun.com/apptest";
      let whash: string = TigerTallyAPI.vmpHash(TigerTallyAPI.GET, url);
      let wtoken: string = TigerTallyAPI.vmpSign(whash);
      console.log("whash:" + whash + ", wtoken:" + wtoken);
      
      // post 请求
      let body: string = "hello world";
      let whash: string = TigerTallyAPI.vmpHash(TigerTallyAPI.POST, body);
      let wtoken: string = TigerTallyAPI.vmpSign(whash);
      console.log("whash:" + whash + ", wtoken:" + wtoken);
      说明
      • 调用 vmpHash 进行自定义加签时,vmpSign 接口的 input 参数应为生成的 whash 字符串。在配置 App 防爬场景化策略时,自定义加签字段的值须设置为 ali_sign_whash

      • 调用 vmpHash 生成 GET 请求的 whash 时,输入的 URL 必须与最终发起网络请求的 URL 完全一致,特别需注意 URL 编码(URL encoding)问题:部分框架会自动对中文字符或参数值进行 URL 编码,应确保编码前后的一致性。

      • vmpHash 的 input 参数不支持空字符串。当输入为 URL 时,必须包含路径(Path)或查询参数(Param)。

      • 调用 vmpSign 时,若请求体为空(例如 POST 或 GET 请求无 body),input 应传入空字符串。

      • 当返回的 whash 或 wtoken 为以下字符串时,表示初始化流程存在异常:

        • "you must call init first":未调用 init 函数;

        • "you must input correct data":传入的数据内容无效;

        • "you must input correct type":传入的数据类型错误。

3. 二次校验

  1. 判断结果。

    根据响应(response)中的 cookie 和 body 字段判断是否需执行二次校验。 

    若响应头(header)中包含多个 Set-Cookie 字段,须按标准 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);
      console.log("recheck:" + recheck);
  2. 创建滑块。

    根据 cptCheck 的返回结果决定是否创建滑块验证对象。 

    TTCaptcha 对象提供以下方法: 

    • show():显示滑块验证窗口; 

    • dismiss():隐藏滑块验证窗口。

    滑块行为通过 TTOption 配置,该类封装了滑块的可配置参数。 

    滑块的状态回调由 TTCaptchaListener 定义,包含两种回调状态。 

    若需自定义滑块窗口页面,可在配置中传入自定义页面地址,支持本地 HTML 文件或远程页面 URL。

    /**
     * 创建滑块对象
     *
     * @param ctx 显示页面
     * @param option 参数
     * @param listener 回调
     * @return 滑块验证对象
     */
    public static cptCreate(ctx: UIContext, option: TTOption, listener: TTCaptchaListener): TTCaptcha | null
    
    
    /**
     * 滑块对象
     */
    export class TTCaptcha {
      /**
      * 显示滑块
      */
      public show(): void
    
      /**
      * 隐藏滑块
      */
      public dismiss(): void
    
    
      /**
      * 获取滑块traceId,用于数据统计
      */
      public getTraceId(): string
    }
    
    /**
     * 滑块参数
     */
    export class TTOption {
      // 是否支持点击空白处隐藏滑块
      public cancelable: boolean;
    
      // 支持本地 htm文件和远程 url
      public customUri: string;
    
      // 设置语言
      public language: string;
    
      // 拦截请求 traceId, 可在 response 中 cookie(acw_tc) 获取
      public traceId: string;
    }
    
    /**
     * 滑块回调
     */
    export interface TTCaptchaListener {
      /**
         * 验证成功
         *
         * @param captcha 滑块对象
         * @param data token, 默认为certifyId
         */
      success: (captcha: TTCaptcha, data: string) => void;
      /**
         * 验证失败或异常
         *
         * @param captcha 滑块对象
         * @param code    错误码
         */
      failed: (captcha: TTCaptcha, code: string) => void;
    }
    • 参数说明:

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

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

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

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

    • 示例代码

      let option: TTOption = new TTOption();
      // customUri传入本地html文件时,需将该html文件置于工程的src/main/resources/rawfile/路径下
      // option.customUri = "captchaindex.html"
      option.language = "cn";
      option.cancelable = false;
      
      let captcha: TTCaptcha | null = TigerTallyAPI.cptCreate(this.getUIContext(), option, {
        success: (captcha: TTCaptcha, data: string) => {
          console.log("captcha success:", data);
        },
        failed: (captcha: TTCaptcha, code: string) => {
          console.log("captcha failed:", code);
        }
      });
      captcha?.show();
      说明
      • 创建滑块cptCreate接口涉及UI操作,需要在主线程中调用。

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

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

        • 1002:系统异常。

        • 1003:参数错误

        • 1005:验证取消

        • 8001:滑块唤起错误。

        • 8002:滑块验证数据异常。

        • 8003:滑块验证内部异常。

        • 8004:网络错误。

最佳实践示例

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

import { TigerTallyAPI, TTCode, TTCaptcha, 
        TTOption, TTCaptchaListener} from '@aliyun/tigertally';

@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", "1");// 配置为Ipv6上报
    // 全量采集
    let retCode: number = TigerTallyAPI.init(getContext(this), APP_KEY, TigerTallyAPI.TT_DEFAULT, options, null);
    // 不采集隐私字段
    // let retCode: number = TigerTallyAPI.init(getContext(this), APP_KEY, TigerTallyAPI.TT_NOT_GRANTED, 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(data);
    // console.log("TigerTally vmpSign:", wtoken);
    // 自定义加签
    let whash: string = TigerTallyAPI.vmpHash(TigerTallyAPI.POST, data);
    let wtoken: string = TigerTallyAPI.vmpSign(whash);
    console.log("TigerTally vmpHash:", whash, ", vmpSign:", wtoken);

    // 请求接口
    this.doPost(APP_URL, APP_HOST, whash, 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: TTOption = new TTOption();
    // option.cancelable = false;

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

  // 发送请求
  async doPost(url: string, host: string, whash: 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 headers: Map<string, string> = new Map<string, string>();
      headers.set("Content-Type", "text/x-markdown");
      headers.set("User-Agent", "");
      headers.set("Host", host);
      headers.set("wToken", wtoken);
  
      if (whash.length > 0) {
        headers.set("ali_sign_whash", whash);
      }
  
      let formHeader: Record<string, string> = {};
      headers.forEach((value, key) => {
        formHeader[key] = value.toString();
      });
  
      let httpRequest = http.createHttp();
      let response: http.HttpResponse = await new Promise<http.HttpResponse>((resolve, reject) => {
        httpRequest.request(
          url,
          {
            method: http.RequestMethod.POST,
            header: formHeader,
            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);
      }
    }
  }
}