Android应用集成SDK

您需要在应用中集成SDK,才能在控制台BOT管理中配置App防爬场景化规则。本文介绍了如何为Android应用集成WAF App防护SDK(以下简称SDK)。

背景信息

App防护SDK主要用于对通过App客户端发起的请求进行签名。WAF服务端通过校验App请求签名,识别App业务中的风险、拦截恶意请求,实现App防护的目的。

关于App防护提供的SDK所涉及的隐私政策,请参见Web应用防火墙App防护SDK隐私政策

使用限制

  • Android应用支持如下2个软件版本的SO:arm64-v8aarmeabi-v7a

  • Android应用的API版本必须是16及以上。

  • init初始化接口存在耗时操作,为确保安全能力完整性,建议在调用init接口后,确保至少间隔2秒再调用后续的vmpSign签名接口。此间隔为推荐值(非强制要求),旨在提升SDK的防护效果。实际调用中可根据业务需求灵活调整,但缩短间隔可能影响安全能力的完整生效。

  • 当使用proguard进行代码混淆时,请使用-keep选项对SDK的接口函数进行设置,例如:

    -keep class com.aliyun.TigerTally.** {*;}
    -keep class com.aliyun.captcha.* {*;}
    -keepclassmembers,allowobfuscation class * {
         @com.alibaba.fastjson.annotation.JSONField <fields>;
    }
    -keep class com.alibaba.fastjson.** {*;}

前提条件

  • 已获取Android应用对应的SDK。

    获取方法:请提交工单,联系产品技术专家获取SDK。

    说明

    Android应用对应的SDK包含2AAR文件,文件名为AliTigerTally_X.Y.Z.aar、AliCaptcha_X.Y.Z.aar,其中X.Y.Z表示版本号。

  • 已获取SDK认证密钥(即appkey)。

    开启BOT管理后,即可在BOT管理 > App防护列表中,单击获取并复制appkey,获取SDK认证密钥。该密钥用于发起SDK初始化请求,需要在集成代码中使用。

    image

    说明

    每个阿里云账号拥有唯一的appkey(适用于所有接入WAF防护的域名),且Android、iOSHarmony应用集成SDK时都使用该appkey。

    认证密钥示例:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。

步骤一:新建工程

Android Studio工具为例,新建一个Android工程,并按照配置向导完成创建。创建好的工程目录如下图所示。

image.png

步骤二:集成AAR

  1. 将获取到的SDK文件tigertally-X.Y.Z-xxxxxx-android.tgz包解压,将文件夹中的所有aar文件拷贝到主工程模块下的libs目录中(具体以工程实际配置为准)。image.png

  2. 打开Appbuild.gradle文件,将libs目录添加为查找依赖的源,并添加编译依赖为AliTigerTally_X.Y.Z.aar、AliCaptcha_X.Y.Z.aar

    重要

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

    具体配置信息如下所示:

    dependencies {
        // ...
        implementation files('libs/AliTigerTally_X.Y.Z.aar')
        implementation files('libs/AliCaptcha_X.Y.Z.aar')
      
        // 三方库依赖
        implementation 'com.alibaba:fastjson:1.2.83_noneautotype'
        implementation 'com.squareup.okhttp3:okhttp:3.11.0'
        implementation 'com.squareup.okio:okio:1.14.0
    }

步骤三:过滤SO CPU架构

如果项目在此之前未使用过SO,需在build.gradle中添加以下配置。

android {
    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }
}

步骤四:为应用申请权限

  • 必备权限

    <uses-permission android:name="android.permission.INTERNET"/>
  • 可选权限

    <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_STORAGEandroid.permission.WRITE_EXTERNAL_STORAGE权限在Android 6.0及以上版本需要动态申请。

步骤五:添加集成代码

1. 添加头文件

import com.alibaba.fastjson.*;
import com.aliyun.tigertally.*;

2. 设置数据签名

  1. 设置业务自定义的终端用户标识,方便您更灵活地配置WAF防护策略。

    /**
    * 设置用户账户
    *
    * @param account 账户
    * @return 错误码
    */
    public static int setAccount(String account)
    • 参数说明

      • account,String类型,表示标识一个用户的字符串,建议您使用脱敏后的格式。

    • 返回值:int类型,返回是否设置成功,0表示成功,-1表示失败。

    • 示例代码

      // 游客身份可以暂时先不setAccount,直接初始化;登录以后调用setAccount和重新初始化
      String account = "user001";
      TigerTallyAPI.setAccount(account);
  2. 初始化SDK,执行一次初始化采集。

    一次初始化采集表示采集一次终端设备信息,您可以根据业务的不同,重新调用init函数进行初始化采集。初始化采集分为三种模式:全量采集、自定义隐私采集、非隐私采集(不采集涉及终端设备用户隐私的字段,包括:imei、imsi、simSerial、wifiMac、wifiList、bluetoothMac、androidId等)。

    说明

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

    // 初始化回调
    public interface TTInitListener {
        // code表示接口调用状态码
        void onInitFinish(int code);
    }
    
    /**
     * SDK 初始化,带 callback
     *
     * @param appkey 密钥
     * @param type 采集数据的类型
     * @param otherOptions 各类参数选项
     * @return 错误码
     */
    public static int init(Context context, String appkey, int collectType,
                           Map<String, String> otherOptions, TTInitListener listener);
    • 参数说明

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

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

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

        字段名

        说明

        示例

        TT_DEFAULT

        表示采集全量数据。

        TigerTallyAPI.TT_DEFAULT

        TT_NO_BASIC_DATA

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

        包括:设备名(Build.DEVICE)、Android版本号(Build.VERSION#RELEASE)、屏幕分辨率。

        TigerTallyAPI.X | TigerTallyAPI.Y

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

        TT_NO_IDENTIFY_DATA

        表示不采集设备标识数据。

        包括:IMEI、IMSI、SimSerial、BuildSerial(SN)、MAC地址。

        TT_NO_UNIQUE_DATA

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

        包括:OAID、Google广告ID、Android ID。

        TT_NO_EXTRA_DATA

        表示不采集扩展设备数据。

        包括:黑灰产App列表、局域网IP、DNS IP、连接的WIFI信息(SSID、BSSID)、附近WIFI列表、定位信息、传感器信息。

        TT_NOT_GRANTED

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

        TigerTallyAPI.TT_NOT_GRANTED

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

        字段名

        说明

        示例

        IPv6

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

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

        • 1:使用IPv6域名。

        1

        Intl

        是否使用非中国内地域名上报设备信息。

        • 0(默认):中国内地上报。

        • 1:非中国内地上报。

        1

        CustomUrl

        设置数据上报服务器域名

        https://cloudauth-device.us-west-1.aliyuncs.com

        CustomHost

        设置数据上报服务器host

        cloudauth-device.us-west-1.aliyuncs.com

        说明

        常见国际站点设置Intl参数即可,只有指定站点上报需要设置CustomUrlCustomHost,例如美西站点:https://cloudauth-device.us-west-1.aliyuncs.com。

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

        TTCode

        Code

        备注

        TT_SUCCESS

        0

        SDK初始化成功

        TT_NOT_INIT

        -1

        SDK未调用初始化

        TT_NOT_PERMISSION

        -2

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

        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版本不匹配

    • 返回值:int类型,返回初始化结果,0表示成功,-1表示失败。

    • 示例代码

      // appkey代表阿里云客户平台分配的认证密钥
      final String appkey="******";
      // 可选参数, 可配置IPv6与国际上报
      Map<String, String> options = new HashMap<>();
      options.put("IPv6", "0");   // 配置为IPv4
      options.put("Intl", "0");   // 配置为中国内地上报
      //options.put("Intl", "1"); // 配置为非中国内地上报
      
      // 美西站点上报
      //options.put("CustomUrl", "https://cloudauth-device.us-west-1.aliyuncs.com"); 
      //options.put("CustomHost", "cloudauth-device.us-west-1.aliyuncs.com"); 
      
      // 一次初始化采集,代表一次设备信息采集,可以根据业务的不同,重新调用函数init初始化采集
      // 全量采集
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_DEFAULT, options, null);
      
      // 指定隐私数据采集,不同的隐私数据可以通过"|"进行拼接
      int privacyFlag = TigerTallyAPI.TT_NO_BASIC_DATA | TigerTallyAPI.TT_NO_UNIQUE_DATA;
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, privacyFlag, options, null);
      
      // 不采集隐私字段
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_NOT_GRANTED, options, null);
      Log.d("AliSDK", "ret:" + ret);
  3. 数据哈希。

    自定义加签接口,对输入数据input进行哈希计算,返回生成的whash字符串作为自定义签名数据。Post、Put、Patch请求需要传入request body,Get、Delete请求传入完整的URL地址。同时,whash字符串需要添加到HTTP请求headerali_sign_whash中。

    // 请求类型:
    public enum RequestType { GET, POST, PUT, PATCH, DELETE }
    
    /**
     *  自定义Hash签名数据 
     *
     * @param type  数据类型
     * @param input 哈希数据
     * @return whash
     */
    public static String vmpHash(RequestType type, byte[] input);
    • 参数说明

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

      • GET:表示Get请求数据。

      • POST:表示Post请求数据。

      • PUT:表示Put请求数据。

      • PATCH:表示Patch请求数据。

      • DELETE:表示Delete请求数据。

    • input:byte[]类型,表示待加签的数据。

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

    • 示例代码

      // get 请求
      String url = "https://tigertally.aliyun.com/apptest";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      
      // post 请求
      String body = "hello world";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      说明

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

  4. 数据签名。

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

    /**
     * 数据签名
     *
     * @param type 签名类型
     * @param input 签名数据
     * @return wtoken
     */
    public static String vmpSign(int type, byte[] input);
    • 参数说明

      • type:int类型,设置数据签名类型,固定取值1

      • input:byte[]类型,表示待签名的数据,一般是整个请求体request body,或者是自定义加签的whash

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

    • 示例代码

      // 控制台配置默认签名,即不勾选自定义加签
      String body = "i am the request body, encrypted or not!";
      String wtoken = TigerTallyAPI.vmpSign(1, body.getBytes("UTF-8"));
      Log.d("AliSDK", "wToken:" + wtoken);
      
      // 控制台配置自定义加签
      // 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);
      说明
      • 调用vmpHash进行自定义加签时,签名接口vmpSign的参数input为生成的whash字符串,且在配置App防爬场景化策略时,自定义加签字段的值需设置为ali_sign_whash。

      • 调用vmpHash生成Get请求的whash时,必须保证输入的URL地址和最终网络请求的URL一致,特别需要注意UrlEncode情况,部分框架会自动对中文或者参数进行UrlEncode编码。

      • 接口vmpHash的参数input不支持字节或者空字符串,输入为URL时必须存在Path或者Param。

      • 调用vmpSign时,如果请求体为空(例如,Post请求或Get请求的body为空),则填写空对象null或空字符串的Bytes值(例如"".getBytes("UTF-8"))。

      • whashwtoken为以下字符串时表示初始化流程存在异常:

        • you must call init first:表示未调用init函数。

        • you must input correct data:表示传入数据错误。

        • you must input correct type:表示传入类型错误。

3. 二次校验

  1. 判断结果。

    根据responsecookiebody字段判断是否要进行二次校验。header中可能存在多个Set-Cookie,需要按照cookie格式合并后调用该接口。

    /**
     * 判断是否进行二次校验
     *
     * @param cookie cookie
     * @param body body
     * @return 0:通过 1:二次校验
     */
    public static int cptCheck(String cookie, String body)
    • 参数说明

      • cookie:String类型,设置请求response中全部cookie

      • body:String类型,设置请求response中全部body

    • 返回值:int类型,返回决策结果,0表示通过,1表示需要二次校验。

    • 示例代码

      String cookie = "key1=value1;kye2=value2;";
      String body = "....";
      int recheck = TigerTallyAPI.cptCheck(cookie, body);
      Log.d("AliSDK", "recheck:" + recheck);
  2. 创建滑块。

    根据cptCheck返回结果决定是否要创建一个滑块对象,TTCaptcha对象提供showdismiss方法,对应显示滑块和隐藏滑块窗口。TTOption封装了滑块可配置的参数,TTListener包含了滑块的2种回调状态。如果需要自定义滑块窗口页面需要传入自定义页面地址,支持本地 html文件,或者远程页面。

    /**
     * 创建滑块对象
     *
     * @param activity 显示页面
     * @param option 参数
     * @param listener 回调
     * @return 滑块验证对象
     */
    public static TTCaptcha cptCreate(Activity activity, TTOption option, TTListener listener);
    
    
    /**
     * 滑块对象
     */
    public class TTCaptcha {
        /**
         * 显示滑块
         */
        public void show();
    
        /**
         * 隐藏滑块
         */
        public void dismiss();
    
        /**
         * 获取滑块traceId,用于数据统计
         */
        public String getTraceId();
    }
    
    /**
     * 滑块参数
     */
    public static class TTOption {
        // 是否支持点击空白处隐藏滑块
        public boolean cancelable;
    
        // 自定义页面,支持本地html文件和远程url
        public String customUri;
    
        // 设置语言
        public String language;
    }
    
    /**
     * 滑块回调
     */
    public interface TTListener {
        /**
         * 验证成功
         *
         * @param captcha 滑块对象
         * @param data token, 默认为certifyId
         */
        void success(TTCaptcha captcha, String data);
    
        /**
         * 验证失败
         *
         * @param captcha 滑块对象
         * @param code 错误码
         */
        void failed(TTCaptcha captcha, String code);
    }
    • 参数说明

      • activity:Activity类型,设置当前页面activity。

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

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

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

    • 示例代码

    TTCaptcha.TTOption option = new TTCaptcha.TTOption();
    // option.customUri  = "file:///android_asset/ali-tt-captcha-demo.html";
    option.language   = "cn";
    option.cancelable = false;
    
    TTCaptcha captcha = TigerTallyAPI.cptCreate(this, option, new TTCaptcha.TTListener() {
        @Override
        public void success(TTCaptcha captcha, String data) {
            Log.d(TAG, "captcha check success:" + data);
        }
        @Override
        public void failed(TTCaptcha captcha, String code) {
            Log.d(TAG, "captcha check failed:" + code);
        }
    });
    captcha.show();
    说明

    证失败,表示用户滑动结束后检测异常情况。

    具体错误码如下所示:

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

    • 1002:系统异常。

    • 1003:参数错误

    • 1005:验证取消

    • 8001:滑块唤起错误。

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

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

    • 8004:网络错误。

最佳实践示例

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(() -> {
            // 初始化
            Map<String, String> options = new HashMap<>();
            //options.put("Intl", "1"); // 配置为国际上报
            // 全量采集
            int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_DEFAULT, options, null);
            // 不采集隐私字段
            // int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_NOT_GRANTED, null, null);
            Log.d(TAG, "tiger tally init: " + ret);

            // 不能立即同步调用
            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.language   = "cn";
        option.cancelable = false;

        TTCaptcha captcha = TigerTallyAPI.cptCreate(this, option, new TTCaptcha.TTListener() {
            @Override
            public void success(TTCaptcha captcha, String data) {
                Log.d(TAG, "captcha check success:" + data);
            }

            @Override
            public void failed(TTCaptcha captcha, String code) {
                Log.d(TAG, "captcha check failed:" + code);
            }
        });

        captcha.show();
    }

    // 发送请求
    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);
    }
}