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

背景信息

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

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

使用限制

  • Android应用支持如下三个软件版本的SO:arm64-v8aarmeabi-v7ax86
  • Android应用的API版本必须是16及以上。
  • init初始化接口存在耗时操作,调用后不能立即同步调用vmpSign接口。
  • 当使用proguard进行代码混淆时,请使用-keep选项对SDK的接口函数进行设置,例如:
    -keep class com.aliyun.TigerTally.** {*;}

前提条件

  • 已获取Android应用对应的SDK。
    获取方法: 请加入钉群(钉群号:34657699),联系产品技术专家进行咨询。
    说明 Android应用对应的SDK包含1个 AAR文件,文件名为 AliTigerTally_X.Y.Z.aar,其中 X.Y.Z表示版本号。
  • 已获取SDK认证密钥(即appkey)。
    开启 BOT管理后,即可在新建或编辑防护模板的 防护场景定义配置导向的 APP SDK集成中单击 获取并复制appkey,获取SDK认证密钥。该密钥用于发起SDK初始化请求,需要在集成代码中使用。
    说明 每个阿里云账号拥有唯一的 appkey(适用于所有接入WAF防护的域名),且Android和iOS应用集成SDK时都使用该 appkey
    认证密钥示例:
    ****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****

操作步骤

  1. 新建工程。
    以Android Studio工具为例,新建一个Android工程,并按照配置向导完成创建。创建好的工程目录如下图所示。 tigertally-demo-spk
  2. 集成AAR包。
    1. 将获取到的SDK文件AliTigerTally_X.Y.Z.aar拖放到/project/app/libs目录。
      拷贝AAR文件
    2. 打开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')
      }
  3. 如果项目在此之前未使用过SO,请在build.gradle中添加以下配置,过滤SO CPU架构。
    android {
        defaultConfig {
            ndk {
                abiFilters 'arm64-v8a', 'x86', "armeabi-v7a"
                //abiFilters "armeabi-v7a"
            }
        }
    }
  4. 为应用申请权限。
    • 必备权限
      <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及以上版本需要动态申请。
  5. 添加集成代码。
    • 设置数据签名
      接口 描述 参数 返回值 配置示例
      用户标识 设置您业务中自定义的终端用户标识,方便您更灵活地配置WAF防护策略。
      接口定义
      /**
      * 设置用户账户
      *
      * @param account 账户
      * @return 错误码
      */
      public static int setAccount(String account);
      account,String类型,表示标识一个用户的字符串,建议您使用脱敏后的格式。 int类型,返回是否设置成功,0表示成功,-1表示失败。
      代码示例
      // 游客身份可以暂时先不setAccount,直接初始化;登录以后调用setAccount和重新初始化
      String account = "user001";
      TigerTallyAPI.setAccount(account);
      SDK初始化 初始化SDK,执行一次初始化采集。一次初始化采集表示采集一次终端设备信息,您可以根据业务的不同,重新调用init函数进行初始化采集。
      初始化采集分为两种模式:采集全量数据、采集除需授权字段外的数据(不采集涉及终端设备用户隐私的字段,包括: imeiimsisimSerialwifiMacwifiListbluetoothMac)。
      说明 建议您在终端用户同意App的隐私政策前,采集除需授权字段外的数据;在终端用户同意App的隐私政策后,再采集全量数据。采集全量数据有利于更好地识别风险。
      接口定义
      // 采集类型: 全量采集, 不采集隐私数据
      public enum CollectType { DEFAULT, NOT_GRANTED }
      
      /**
       * SDK 初始化
       *
       * @param appkey 密钥
       * @param type   采集类型
       * @return 错误码
       */
      public static int init(Context context, String appkey, CollectType type);
      • context:Context类型,传入您应用的上下文。
      • appkey:String类型,设置为您的SDK认证密钥。
      • type:CollectType类型,设置采集模式。取值:
        • DEFAULT:表示采集全量数据。
        • NO_GRANTED:表示采集除需授权字段外的数据。
      int类型,返回初始化结果,0表示成功,-1表示失败。
      代码示例
      // appkey代表阿里云客户平台分配的认证密钥
      final String appkey="******";
      
      // 一次初始化采集,代表一次设备信息采集,可以根据业务的不同,重新调用函数init初始化采集
      // 全量采集
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.CollectType.DEFAULT);
      
      // 不采集隐私字段
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.CollectType.NOT_GRANTED);
      Log.d("AliSDK", "ret:" + ret);
      数据哈希 自定义加签使用接口,将对传入的数据计算生成一个whash字符串,Post请求需要传入request body,Get请求传入完整的URL地址。同时,whash字符串需要添加到http请求header的ali_sign_whash中。
      接口定义
      // 请求类型:
      public enum RequestType { GET, POST }
      
      /**
       *  自定义签名数据 Hash
       *
       * @param type  数据类型
       * @param input 哈希数据
       * @return whash
       */
      public static String vmpHash(RequestType type, byte[] input);
      • type:RequestType类型,设置数据类型。取值:
        • GET:表示Get请求数据。
        • POST:表示Post请求数据。
      • input:byte[]类型,表示待加签的数据。
      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);
      数据签名 使用vmp技术对input的数据进行签名处理,并且返回wtoken字符串。
      接口定义
      /**
       * 数据签名
       *
       * @param type  签名类型
       * @param input 签名数据
       * @return wtoken
       */
      public static String vmpSign(int type, byte[] input);
      • type:CollectType类型,设置数据签名类型,固定取值1
      • input:byte[]类型,表示待签名的数据,一般是整个请求体request body
      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);
      说明
      • 调用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"))。
      • 当whash或wtoken为以下字符串时表示初始化流程存在异常:
        • you must call init first:表示未调用init函数。
        • you must input correct data:表示传入数据错误。
        • you must input correct type:表示传入类型错误。
    • 二次校验
      接口 描述 参数 返回值 配置示例
      判断结果 根据responsecookiebody字段判断是否要进行二次校验。header中可能存在多个Set-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);
      创建滑块 根据cptCheck返回结果决定是否要创建一个滑块对象,TTCptcha对象提供show和dismiss方法,对应显示滑块和隐藏滑块窗口。TTOption封装了滑块可配置的参数,TTListener包含了滑块的三种回调状态。如果需要自定义滑块窗口页面需要传入自定义页面地址,支持本地 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();
      }
      
      /**
       * 滑块参数
       */
      public class TTOption {
          // 点击取消
          public boolean cancelable;
      
          // 隐藏滑块错误码
          public boolean hideError;
      
          // 自定义页面
          public String customUri;
      
          // 设置语言
          public String language;
      
          // 二次校验请求 trace
          public String traceId;
      
          // 滑块标题文案
          public String titleText;
      
          // 滑块描述文案
          public String descText;
      
          // 滑块颜色
          public String slideColor;
      }
      
      /**
       * 滑块回调
       */
      public interface TTListener {
          /**
           * 验证成功
           *
           * @param captcha 滑块对象
           * @param data    token, 默认为 traceId
           */
          void success(TTCaptcha captcha, String data);
      
          /**
           * 验证失败
           *
           * @param captcha 滑块对象
           * @param code    错误码
           */
          void failed(TTCaptcha captcha, String code);
      
          /**
           * 验证异常
           *
           * @param captcha 滑块对象
           * @param code    错误码
           * @param message 错误信息
           */
          void error(TTCaptcha captcha, int code, String message);
      }
                                                              
      • acitivity:Activity类型,设置当前页面activity。
      • option:TTOption类型,设置滑块配置参数。
      • listener:TTlistener类型,设置滑块状态回调。
      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";
      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();
      说明 验证异常,表示在加载滑块过程中检测到异常情况。验证失败,表示用户滑动结束后检测异常情况。
      具体错误码如下所示:
      • 1001:输入参数错误。
      • 1002:网络检测异常。
      • 1003:js回调数据异常。
      • 1004:WebView加载异常。
      • 1005:js滑块返回异常。
      • 1100:主动关闭滑块。

最佳实践示例

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);
            // 不采集隐私字段
            // int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.CollectType.NOT_GRANTED);
            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.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();
    }

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