您需要在应用中集成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包含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参数不支持空字符串。当输入为 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. 二次校验
判断结果。
根据响应(
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);
}
}
}
}