Android應(yīng)用集成SDK
您需要在應(yīng)用中集成SDK,才能在控制臺BOT管理中配置App防爬場景化規(guī)則。本文介紹了如何為Android應(yīng)用集成WAF App防護(hù)SDK(以下簡稱SDK)。
背景信息
App防護(hù)SDK主要用于對通過App客戶端發(fā)起的請求進(jìn)行簽名。WAF服務(wù)端通過校驗(yàn)App請求簽名,識別App業(yè)務(wù)中的風(fēng)險(xiǎn)、攔截惡意請求,實(shí)現(xiàn)App防護(hù)的目的。
關(guān)于App防護(hù)提供的SDK所涉及的隱私政策,請參見Web應(yīng)用防火墻App防護(hù)SDK隱私政策。
使用限制
Android應(yīng)用支持如下三個(gè)軟件版本的SO:arm64-v8a、armeabi-v7a。
Android應(yīng)用的API版本必須是16及以上。
init初始化接口存在耗時(shí)操作,調(diào)用后不能立即同步調(diào)用vmpSign接口,請確保SDK的初始化接口和簽名接口調(diào)用時(shí)間間隔2秒以上。
當(dāng)使用proguard進(jìn)行代碼混淆時(shí),請使用-keep選項(xiàng)對SDK的接口函數(shù)進(jìn)行設(shè)置,例如:
-keep class com.aliyun.TigerTally.** {*;}
前提條件
已獲取Android應(yīng)用對應(yīng)的SDK。
獲取方法:請?zhí)峤?span data-tag="ph" id="c379129ecewh5" docid="3307547" class="ph">工單,聯(lián)系產(chǎn)品技術(shù)專家獲取SDK。
說明Android應(yīng)用對應(yīng)的SDK包含1個(gè)AAR文件,文件名為AliTigerTally_X.Y.Z.aar,其中X.Y.Z表示版本號。
已獲取SDK認(rèn)證密鑰(即appkey)。
開啟BOT管理后,即可在新建或編輯防護(hù)模板的防護(hù)場景定義配置導(dǎo)向中的APP SDK集成單擊獲取并復(fù)制appkey,獲取SDK認(rèn)證密鑰。該密鑰用于發(fā)起SDK初始化請求,需要在集成代碼中使用。
說明每個(gè)阿里云賬號擁有唯一的appkey(適用于所有接入WAF防護(hù)的域名),且Android和iOS應(yīng)用集成SDK時(shí)都使用該appkey。
認(rèn)證密鑰示例:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。
步驟一:新建工程
以Android Studio工具為例,新建一個(gè)Android工程,并按照配置向?qū)瓿蓜?chuàng)建。創(chuàng)建好的工程目錄如下圖所示。
步驟二:集成AAR包
將獲取到的SDK文件AliTigerTally_X.Y.Z.aar拖放到/project/app/libs目錄中。
打開App的build.gradle文件,將libs目錄添加為查找依賴的源,并添加編譯依賴為AliTigerTally_X.Y.Z.aar。
具體配置信息如下所示:
您需要將AliTigerTally_X.Y.Z.aar文件的版本號X.Y.Z替換成您獲取的AAR文件的版本號。
//...
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
// ...
compile(name: 'AliTigerTally_X.Y.Z', ext: 'aar')
}
步驟三:過濾SO CPU架構(gòu)
如果項(xiàng)目在此之前未使用過SO,需在build.gradle中添加以下配置。
android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
}
步驟四:為應(yīng)用申請權(quán)限
必備權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
可選權(quán)限
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
說明android.permission.READ_EXTERNAL_STORAGE和android.permission.WRITE_EXTERNAL_STORAGE權(quán)限在Android 6.0及以上版本需要動態(tài)申請。
步驟五:添加集成代碼
數(shù)據(jù)簽名。
設(shè)置業(yè)務(wù)自定義的終端用戶標(biāo)識,方便您更靈活地配置WAF防護(hù)策略。
/** * 設(shè)置用戶賬戶 * * @param account 賬戶 * @return 錯(cuò)誤碼 */ public static int setAccount(String account)
參數(shù)說明:
account:String類型,表示標(biāo)識一個(gè)用戶的字符串,建議您使用脫敏后的格式.
返回值:int類型,返回是否設(shè)置成功,0表示成功,-1表示失敗。
示例代碼:
// 游客身份可以暫時(shí)先不setAccount,直接初始化;登錄以后調(diào)用setAccount和重新初始化 String account = "user001"; TigerTallyAPI.setAccount(account);
初始化SDK,執(zhí)行一次初始化采集。
一次初始化采集表示采集一次終端設(shè)備信息,您可以根據(jù)業(yè)務(wù)的不同,重新調(diào)用init函數(shù)進(jìn)行初始化采集。
初始化采集分為兩種模式:采集全量數(shù)據(jù)、采集除需授權(quán)字段外的數(shù)據(jù)(不采集涉及終端設(shè)備用戶隱私的字段,包括:imei、imsi、simSerial、wifiMac、wifiList、bluetoothMac、androidId)。
說明建議您在終端用戶同意App的隱私政策前,采集除需授權(quán)字段外的數(shù)據(jù);在終端用戶同意App的隱私政策后,再采集全量數(shù)據(jù)。采集全量數(shù)據(jù)有利于更好地識別風(fēng)險(xiǎn)。
// 采集類型: 全量采集, 不采集隱私數(shù)據(jù) public enum CollectType { DEFAULT, NOT_GRANTED } // 初始化回調(diào) public interface TTInitListener { // code表示接口調(diào)用狀態(tài)碼 void onInitFinish(int code); } /** * SDK 初始化,帶 callback * * @param appkey 密鑰 * @param type 采集數(shù)據(jù)的類型 * @param otherOptions 各類參數(shù)選項(xiàng) * @return 錯(cuò)誤碼 */ public static int init(Context context, String appkey, CollectType type, Map<String, String> otherOptions, TTInitListener listener);
參數(shù)說明:
context:Context類型,傳入您應(yīng)用的上下文。
appkey:String類型,設(shè)置為您的SDK認(rèn)證密鑰。
type:CollectType類型,設(shè)置采集模式。取值:
DEFAULT:表示采集全量數(shù)據(jù)。
NO_GRANTED:表示采集除需授權(quán)字段外的數(shù)據(jù)。
otherOptions:Map<String, String>類型,信息采集可選項(xiàng),默認(rèn)可以為null??蛇x參數(shù)如下
字段名
說明
示例
IPv6
是否使用IPv6域名上報(bào)設(shè)備信息。
0:使用IPv4域名,默認(rèn)值
1:使用IPv6域名。
1
Intl
是否使用國際域名上報(bào)設(shè)備信息。
0:中國內(nèi)地上報(bào),默認(rèn)值
1:國際上報(bào)。
1
listener:TTInitListener類型,SDK初始化回調(diào)接口,可在回調(diào)中判斷初始化結(jié)果的具體狀態(tài),默認(rèn)可以傳null。
TTCode
Code
備注
TT_SUCCESS
0
SDK初始化成功
TT_NOT_INIT
-1
SDK未調(diào)用初始化
TT_NOT_PERMISSION
-2
SDK需要的Android基礎(chǔ)權(quán)限未完全授權(quán)
TT_UNKNOWN_ERROR
-3
系統(tǒng)未知錯(cuò)誤
TT_NETWORK_ERROR
-4
網(wǎng)絡(luò)錯(cuò)誤
TT_NETWORK_ERROR_EMPTY
-5
網(wǎng)絡(luò)錯(cuò)誤,返回內(nèi)容為空串
TT_NETWORK_ERROR_INVALID
-6
網(wǎng)絡(luò)返回的格式非法
TT_PARSE_SRV_CFG_ERROR
-7
服務(wù)端配置解析失敗
TT_NETWORK_RET_CODE_ERROR
-8
網(wǎng)關(guān)返回失敗
TT_APPKEY_EMPTY
-9
AppKey為空
TT_PARAMS_ERROR
-10
其他參數(shù)錯(cuò)誤
TT_FGKEY_ERROR
-11
密鑰計(jì)算錯(cuò)誤
TT_APPKEY_ERROR
-12
SDK版本和AppKey版本不匹配
返回值:int類型,返回初始化結(jié)果,0表示成功,-1表示失敗。
示例代碼:
// appkey代表阿里云客戶平臺分配的認(rèn)證密鑰 final String appkey="******"; // 可選參數(shù), 可配置IPv6與國際上報(bào) Map<String, String> options = new HashMap<>(); options.put("IPv6", "0");// 配置為IPv4 options.put("Intl", "0");// 中國內(nèi)地上報(bào) // 一次初始化采集,代表一次設(shè)備信息采集,可以根據(jù)業(yè)務(wù)的不同,重新調(diào)用函數(shù)init初始化采集 // 全量采集 int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.CollectType.DEFAULT, options, null); // 不采集隱私字段 int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.CollectType.NOT_GRANTED, options, null); Log.d("AliSDK", "ret:" + ret);
數(shù)據(jù)簽名。
使用vmp技術(shù)對輸入數(shù)據(jù)input進(jìn)行簽名處理,并且返回wtoken字符串用于請求認(rèn)證。
/** * 數(shù)據(jù)簽名 * * @param type 簽名類型 * @param input 簽名數(shù)據(jù) * @return wtoken */ public static String vmpSign(int type, byte[] input);
參數(shù)說明:
type:CollectType類型,設(shè)置數(shù)據(jù)簽名類型,固定取值1。
input:byte[]類型,表示待簽名的數(shù)據(jù),一般是整個(gè)請求體request body。
返回值:String類型,返回wtoken字符串。
示例代碼:
// 默認(rèn)簽名 String body = "i am the request body, encrypted or not!"; String wtoken = TigerTallyAPI.vmpSign(1, body.getBytes("UTF-8")); Log.d("AliSDK", "wToken:" + wtoken);
數(shù)據(jù)哈希。
自定義加簽使用接口,將對傳入的數(shù)據(jù)計(jì)算生成一個(gè)whash字符串,Post、Put、Patch請求需要傳入request body,Get、Delete請求傳入完整的URL地址。同時(shí),whash字符串需要添加到http請求header的ali_sign_whash中。
// 請求類型: public enum RequestType { GET, POST, PUT, PATCH, DELETE } /** * 自定義Hash簽名數(shù)據(jù) * * @param type 數(shù)據(jù)類型 * @param input 哈希數(shù)據(jù) * @return whash */ public static String vmpHash(RequestType type, byte[] input);
參數(shù)說明:
type:RequestType類型,設(shè)置數(shù)據(jù)類型。取值:
GET:表示Get請求數(shù)據(jù)。
POST:表示Post請求數(shù)據(jù)。
PUT:表示Put請求數(shù)據(jù)。
PATCH:表示Patch請求數(shù)據(jù)。
DELETE:表示Delete請求數(shù)據(jù)。
input:byte[]類型,表示待加簽的數(shù)據(jù)。
返回值:String類型,返回whash字符串。
示例代碼:
// get 請求 String url = "https://tigertally.aliyun.com/apptest"; String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes()); String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes()); Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken); // post 請求 String body = "hello world"; String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes()); String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes()); Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken);
說明調(diào)用vmpHash進(jìn)行自定義加簽時(shí),簽名接口vmpSign的參數(shù)input為生成的whash字符串,且在配置App防爬場景化策略時(shí),自定義加簽字段的值需設(shè)置為ali_sign_whash。
調(diào)用vmpHash生成Get請求的whash時(shí),必須保證輸入的URL地址和最終網(wǎng)絡(luò)請求的URL一致,特別需要注意UrlEncode情況,部分框架會自動對中文或者參數(shù)進(jìn)行UrlEncode編碼。
接口vmpHash的參數(shù)input不支持字節(jié)或者空字符串,輸入為URL時(shí)必須存在Path或者Param。
調(diào)用vmpSign時(shí),如果請求體為空,例如,Post請求或Get請求的body為空,則填寫空對象null或空字符串的Bytes值,例如
"".getBytes("UTF-8")
。當(dāng)whash或wtoken為以下字符串時(shí)表示初始化流程存在異常:
you must call init first:表示未調(diào)用init函數(shù)。
you must input correct data:表示傳入數(shù)據(jù)錯(cuò)誤。
you must input correct type:表示傳入類型錯(cuò)誤。
二次校驗(yàn)
判斷結(jié)果。
根據(jù)response中cookie和body字段判斷是否要進(jìn)行二次校驗(yàn)。header中可能存在多個(gè)Set-Cookie。
/** * 判斷是否進(jìn)行二次校驗(yàn) * * @param cookie cookie * @param body body * @return 0:通過 1:二次校驗(yàn) */ public static int cptCheck(String cookie, String body)
參數(shù)說明:
cookie:String類型,設(shè)置請求response中全部cookie。
body:String類型,設(shè)置請求response中全部body。
返回值:int類型,返回決策結(jié)果,0表示通過,1表示需要二次校驗(yàn)。
示例代碼:
String cookie = "key1=value1;kye2=value2;"; String body = "...."; int recheck = TigerTallyAPI.cptCheck(cookie, body); Log.d("AliSDK", "recheck:" + recheck);
創(chuàng)建滑塊。
根據(jù)cptCheck返回結(jié)果決定是否要創(chuàng)建一個(gè)滑塊對象,TTCaptcha對象提供show和dismiss方法,對應(yīng)顯示滑塊和隱藏滑塊窗口。TTOption封裝了滑塊可配置的參數(shù),TTListener包含了滑塊的三種回調(diào)狀態(tài)。如果需要自定義滑塊窗口頁面需要傳入自定義頁面地址,支持本地 html文件,或者遠(yuǎn)程頁面。
/** * 創(chuàng)建滑塊對象 * * @param activity 顯示頁面 * @param option 參數(shù) * @param listener 回調(diào) * @return 滑塊驗(yàn)證對象 */ public static TTCaptcha cptCreate(Activity activity, TTOption option, TTListener listener); /** * 滑塊對象 */ public class TTCaptcha { /** * 顯示滑塊 */ public void show(); /** * 隱藏滑塊 */ public void dismiss(); /** * 獲取滑塊traceId,用于數(shù)據(jù)統(tǒng)計(jì) */ public String getTraceId(); } /** * 滑塊參數(shù) */ public static class TTOption { // 是否支持點(diǎn)擊空白處隱藏滑塊 public boolean cancelable; // 是否支持隱藏滑塊錯(cuò)誤碼 public boolean hideError; // 自定義頁面,支持本地html文件和遠(yuǎn)程url public String customUri; // 設(shè)置語言 public String language; // 二次校驗(yàn)請求traceId public String traceId; // 滑塊標(biāo)題文案,最長20個(gè)字符 public String titleText; // 滑塊描述文案,最長60個(gè)字符 public String descText; // 滑塊顏色,格式例如"#007FFF" public String slideColor; // 是否隱藏traceId public boolean hideTraceId; } /** * 滑塊回調(diào) */ public interface TTListener { /** * 驗(yàn)證成功 * * @param captcha 滑塊對象 * @param data token, 默認(rèn)為traceId */ void success(TTCaptcha captcha, String data); /** * 驗(yàn)證失敗 * * @param captcha 滑塊對象 * @param code 錯(cuò)誤碼 */ void failed(TTCaptcha captcha, String code); /** * 驗(yàn)證異常 * * @param captcha 滑塊對象 * @param code 錯(cuò)誤碼 * @param message 錯(cuò)誤信息 */ void error(TTCaptcha captcha, int code, String message); }
參數(shù)說明:
activity:Activity類型,設(shè)置當(dāng)前頁面activity。
option:TTOption類型,設(shè)置滑塊配置參數(shù)。
listener:TTlistener類型,設(shè)置滑塊狀態(tài)回調(diào)。
返回值:TTCaptcha類型,返回滑塊對象。
示例代碼:
TTCaptcha.TTOption option = new TTCaptcha.TTOption(); // option.customUri = "file:///android_asset/ali-tt-captcha-demo.html"; // option.traceId = "4534534534adf433534534543"; option.titleText = "測試 Title"; option.descText = "測試 Description"; option.language = "cn"; option.cancelable = true; option.hideError = true; option.slideColor = "#007FFF"; option.hideTraceId= true; TTCaptcha captcha = TigerTallyAPI.cptBuild(this, option, new TTCaptcha.TTListener() { @Override public void success(TTCaptcha captcha, String data) { Log.d(TAG, "captcha check success:" + data); captcha.dismiss(); } @Override public void failed(TTCaptcha captcha, String code) { Log.d(TAG, "captcha check failed:" + code); } @Override public void error(TTCaptcha captcha, int code, String message) { Log.d(TAG, "captcha check error, code: " + code + ", message: " + message); } }); captcha.show();
說明驗(yàn)證異常,表示在加載滑塊過程中檢測到異常情況。驗(yàn)證失敗,表示用戶滑動結(jié)束后檢測異常情況。
具體錯(cuò)誤碼如下所示:
1001:輸入?yún)?shù)錯(cuò)誤。
1002:網(wǎng)絡(luò)檢測異常。
1003:js回調(diào)數(shù)據(jù)異常。
1004:WebView加載異常。
1005:js滑塊返回異常。
1100:主動關(guān)閉滑塊。
最佳實(shí)踐示例
package com.aliyun.tigertally.apk;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import com.aliyun.TigerTally.TigerTallyAPI;
import com.aliyun.TigerTally.captcha.api.TTCaptcha;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DemoActivity extends AppCompatActivity {
private final static String TAG = "TigerTally-Demo";
private final static String APP_HOST = "******";
private final static String APP_URL = "******";
private final static String APP_KEY = "******";
private final static OkHttpClient okHttpClient = new OkHttpClient();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
doTest();
}
private void doTest() {
Log.d(TAG, "captcha flow");
new Thread(() -> {
// 初始化
// 全量采集
int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.CollectType.DEFAULT, null, null);
// 不采集隱私字段
// int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.CollectType.NOT_GRANTED, null, null);
Log.d(TAG, "tiger tally init: " + ret);
// 不能立即同步調(diào)用
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 簽名
String data = "hello world";
String whash = null, wtoken = null;
// 自定義加簽
whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, data.getBytes());
wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
Log.d(TAG, "tiger tally vmp: " + whash + ", " + wtoken);
// 正常加簽
// wtoken = TigerTallyAPI.vmpSign(1, data.getBytes());
// Log.d(TAG, "tiger tally vmp: " + wtoken);
// 請求接口
doPost(APP_URL, APP_HOST, whash, wtoken, data, (code, cookie, body) -> {
// 判斷是否需要顯示滑塊
int recheck = TigerTallyAPI.cptCheck(cookie, body);
Log.d(TAG, "captcha check result: " + recheck);
if (recheck == 0) return;
this.runOnUiThread(this::doShow);
});
}).start();
}
// 顯示滑塊
public void doShow() {
Log.d(TAG, "captcha show");
TTCaptcha.TTOption option = new TTCaptcha.TTOption();
// option.customUri = "file:///android_asset/ali-tt-captcha-demo.html";
// option.traceId = "4534534534adf433534534543";
option.titleText = "測試 Title";
option.descText = "測試 Description";
option.language = "cn";
option.cancelable = true;
option.hideError = true;
option.slideColor = "#007FFF";
TTCaptcha captcha = TigerTallyAPI.cptCreate(this, option, new TTCaptcha.TTListener() {
@Override
public void success(TTCaptcha captcha, String data) {
Log.d(TAG, "captcha check success:" + data);
captcha.dismiss();
}
@Override
public void failed(TTCaptcha captcha, String code) {
Log.d(TAG, "captcha check failed:" + code);
}
@Override
public void error(TTCaptcha captcha, int code, String message) {
Log.d(TAG, "captcha check error, code: " + code + ", message: " + message);
}
});
captcha.show();
}
// 發(fā)送請求
public static void doPost(String url, String host, String whash, String wtoken, String body, Callback callback) {
Log.d(TAG, "start request post");
int responseCode = 0;
String responseBody = "";
StringBuilder responseCookie = new StringBuilder();
try {
Request.Builder builder = new Request.Builder()
.url(url)
.addHeader("wToken", wtoken)
.addHeader("Host", host)
.post(RequestBody.create(MediaType.parse("text/x-markdown"), body.getBytes()));
if (whash != null) {
builder.addHeader("ali_sign_whash", whash);
}
Response response = okHttpClient.newCall(builder.build()).execute();
responseCode = response.code();
responseBody = response.body() == null ? "" : response.body().string();
for (String item : response.headers("Set-Cookie")) {
responseCookie.append(item).append(";");
}
Log.d(TAG, "response code:" + responseCode);
Log.d(TAG, "response cookie:" + responseCookie);
Log.d(TAG, "response body:" + (responseBody.length() > 100 ? responseBody.substring(0, 100) : ""));
if (response.isSuccessful()) {
Log.d(TAG, "success: " + response.code() + ", " + response.message());
} else {
Log.e(TAG, "failed: " + response.code() + ", " + response.message());
}
response.close();
} catch (Exception e) {
e.printStackTrace();
responseCode = -1;
responseBody = e.toString();
} finally {
if (callback != null) {
callback.onResponse(responseCode, responseCookie.toString(), responseBody);
}
}
}
public interface Callback {
void onResponse(int code, String cookie, String body);
}
}