您需要在應用中整合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管理後,即可在列表中,單擊获取并复制appkey,擷取SDK認證密鑰。該密鑰用於發起SDK初始化請求,需要在整合代碼中使用。
說明每個阿里雲帳號擁有唯一的appkey(適用於所有接入WAF防護的網域名稱),且Android、iOS和Harmony應用整合SDK時都使用該appkey。
認證密鑰樣本:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。
步驟一:建立工程
以DevEco Studio工具為例,建立一個Harmony工程,並按照設定精靈完成建立。建立好的工程目錄如下圖所示。
步驟二:整合HAR包
將擷取到的SDK檔案
tigertally-X.Y.Z-xxxxxx-harmony.tgz包解壓,將擷取到的HAR檔案拷貝到工程中存放HAR包的目錄。
開啟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. 設定資料簽名
設定業務自訂的終端使用者標識,方便您更靈活地配置WAF防護策略。
/** * 設定使用者賬戶 * * @param account 賬戶 * @return 錯誤碼 */ public static setAccount(account: string): number參數說明:
account,string類型,表示標識一個使用者的字串,建議使用脫敏後的格式。傳回值:number類型,返回是否設定成功,0表示成功,-1表示失敗。
範例程式碼:
// 遊客身份可以暫時先不setAccount,直接初始化;登入以後調用setAccount和重新初始化 let account: string = "user001"; TigerTallyAPI.setAccount(account);
初始化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);
資料雜湊。
自訂加簽介面對輸入資料
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);說明控制台勾選預設簽名不需要調用該介面,勾選自訂加簽時需要在資料簽名前調用該介面進行雜湊校正。
資料簽名。
使用 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. 二次校正
判斷結果。
根據響應(
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);
建立滑塊。
根據
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);
}
}
}
}