全部產品
Search
文件中心

Web Application Firewall:Harmony應用整合SDK

更新時間:Jan 21, 2026

您需要在應用中整合SDK,才能在控制台BOT管理中配置App防爬情境化規則。本文介紹了如何為Harmony應用整合WAF App防護SDK(以下簡稱SDK)。

背景資訊

App防護SDK主要用於對通過App用戶端發起的請求進行簽名。WAF服務端通過校正App請求籤名,識別App業務中的風險、攔截惡意請求,實現App防護的目的。

適用範圍

  • 需要在Harmony Next 4.1 及以上版本的系統運行,API版本最低支援12。

  • init初始化介面存在耗時操作,調用後不能立即同步調用vmpSign介面,請確保SDK的初始化介面和簽名介面調用時間間隔2秒以上。

  • 滑塊建立cptCreate涉及UI操作,以及初始化使用回調模式時,需要在主線程中進行調用。

  • 不支援模擬器模式調試。

  • 僅支援開啟位元組碼打包方案。

前提條件

  • 已擷取Harmony應用對應的SDK。

    擷取方法:請提交工單,聯絡產品技術專家擷取SDK。

    說明

    Harmony應用對應的SDK包含2個HAR檔案,檔案名稱為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、iOS和Harmony應用整合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. 開啟App的oh-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 參數不支援Null 字元串。當輸入為 URL 時,必須包含路徑(Path)或查詢參數(Param)。

      • 調用 vmpSign 時,若請求體為空白(例如 POST 或 GET 請求無 body),input 應傳入Null 字元串。

      • 當返回的 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);
      }
    }
  }
}