Todos los productos
Search
Centro de documentos

Web Application Firewall:Integrate the SDK for Android

Última actualización: Sep 23, 2026

Para configurar reglas basadas en escenarios contra rastreadores (anti-crawler) en la gestión de bots desde la consola, debe integrar el SDK de App Protection de WAF. Este tema explica cómo integrar el SDK con aplicaciones Android.

Contexto

El SDK de App Protection firma las solicitudes provenientes de los clientes de la aplicación. El servidor de Web Application Firewall (WAF) verifica estas firmas para identificar riesgos, bloquear solicitudes maliciosas y proteger la aplicación.

Limitaciones

  • En Android, el SDK es compatible con las arquitecturas arm64-v8a y armeabi-v7a.

  • El nivel de la API de Android debe ser 16 o superior.

  • La ejecución del método init puede tomar tiempo. Para garantizar una protección completa, espere como mínimo 2 segundos entre la llamada al método init y al método vmpSign. Este retraso se recomienda para obtener una protección óptima, pero no es obligatorio. Puede ajustarlo según sus necesidades de negocio; sin embargo, un retraso más corto podría impedir que las funciones de seguridad funcionen por completo.

  • Si utiliza ProGuard para ofuscar el código, use la opción -keep para conservar los métodos del SDK. Por ejemplo:

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

Requisitos previos

  • Obtenga el SDK para su aplicación Android.

    Para obtener el SDK, envíe un ticket a nuestros expertos técnicos del producto.

    Nota

    El SDK de Android incluye dos archivos AAR: AliTigerTally_X.Y.Z.aar y AliCaptcha_X.Y.Z.aar, donde X.Y.Z es el número de versión.

  • Ya obtuvo la clave de autenticación del SDK (también conocida como appkey).

    Después de habilitar Bot Management, inicie sesión en la consola de Web Application Firewall 3.0. En la barra de menú superior, seleccione el resource group y la región (Chinese Mainland o Outside Chinese Mainland) de la instancia de WAF. En el panel de navegación izquierdo, elija Protection Config > Bot Management > Bot Management Template. Vaya a la pestaña App Protection y haga clic en Obtain and Copy AppKey en la esquina superior derecha para recuperar la clave de autenticación del SDK. Esta clave es obligatoria para inicializar el SDK y debe incluirse en su código de integración.

    Nota

    Cada cuenta de Alibaba Cloud tiene un appkey único para todos los nombres de dominio protegidos por WAF. Este appkey se usa para integrar el SDK en aplicaciones Android, iOS y HarmonyOS.

    Ejemplo de clave de autenticación: ****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****.

Paso 1: Crear un proyecto

En Android Studio, siga el asistente de configuración para crear un nuevo proyecto de Android. La estructura de directorios del proyecto es la siguiente:


tigertally-demo-apk [TigerTallyDemo]
  .gradle
  .idea
  app
    build
    libs
      AliCaptcha_xxx.aar
      AliTigerTally_xxx.aar
    src
      androidTest
      main
      test [unitTest]
    .gitignore
    build.gradle
    proguard-rules.pro
  gradle
    wrapper
      gradle-wrapper.jar
      gradle-wrapper.properties
  .gitignore
  .java-version
  build.gradle
  gradle.properties
  gradlew
  gradlew.bat
  local.properties
  settings.gradle
  tigertally.jks
  External Libraries
  Scratches and Consoles

Paso 2: Integrar AAR

  1. Extraiga el archivo del SDK tigertally-X.Y.Z-xxxxxx-android.tgz y copie todos los archivos AAR de la carpeta extraída al directorio libs de su módulo principal (la ruta exacta depende de la configuración de su proyecto).

  2. Abra el archivo build.gradle de su aplicación y agregue las dependencias de AliTigerTally_X.Y.Z.aar y AliCaptcha_X.Y.Z.aar desde el directorio libs.

    Importante

    Reemplace X.Y.Z en los nombres de archivo AliTigerTally_X.Y.Z.aar y AliCaptcha_X.Y.Z.aar por el número de versión de sus archivos AAR.

    La configuración es la siguiente:

    dependencies {
        // ...
        implementation files('libs/AliTigerTally_X.Y.Z.aar')
        implementation files('libs/AliCaptcha_X.Y.Z.aar')
      
        // third-party library dependencies
        implementation 'com.alibaba:fastjson:1.2.83_noneautotype'
        implementation 'com.squareup.okhttp3:okhttp:3.11.0'
        implementation 'com.squareup.okio:okio:1.14.0'
    }

Paso 3: Filtrar las arquitecturas de CPU SO

Si su proyecto no ha utilizado archivos SO, agregue la siguiente configuración a su archivo build.gradle.

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

Paso 4: Solicitar permisos

  • Permiso obligatorio

    <uses-permission android:name="android.permission.INTERNET"/>
  • Permisos opcionales

    <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"/>
Nota

En Android 6.0 y versiones posteriores, debe solicitar dinámicamente android.permission.READ_EXTERNAL_STORAGE y android.permission.WRITE_EXTERNAL_STORAGE.

Paso 5: Agregar el código de integración

1. Agregar los archivos de encabezado

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

2. Configurar la firma de datos

  1. Establezca un ID personalizado para cada usuario final. Esto le permite configurar las políticas de mitigación de WAF con mayor flexibilidad.

    /**
    * Sets the user account.
    *
    * @param account The user account.
    * @return An error code.
    */
    public static int setAccount(String account)
    • Parámetros:

      • account: String. Una cadena que identifica a un usuario. Se recomienda usar un formato anonimizado.

    • Valor devuelto: int. Devuelve 0 si la operación se completó correctamente o -1 si falló.

    • Código de ejemplo:

      // For a guest, you can skip setAccount and directly initialize the SDK. After the user logs in, call setAccount and reinitialize.
      String account = "user001";
      TigerTallyAPI.setAccount(account);
  2. Inicialice el SDK y ejecute una recopilación inicial de datos.

    Una recopilación inicial de datos obtiene la información del dispositivo una sola vez. Según sus necesidades de negocio, puede volver a llamar a la función init para ejecutar una nueva recopilación. Existen tres modos de recopilación: completa, personalizada de privacidad y sin privacidad. El modo sin privacidad no recopila datos de campos relacionados con la privacidad del usuario final, entre ellos: imei, imsi, simSerial, wifiMac, wifiList, bluetoothMac y androidId.

    Nota

    Seleccione un modo de recopilación que cumpla con sus requisitos normativos y garantice la integridad de los datos. Los datos completos mejoran la identificación de riesgos.

    // Initialization callback
    public interface TTInitListener {
        // The code parameter indicates the status code of the interface call.
        void onInitFinish(int code);
    }
    
    /**
     * Initializes the SDK with a callback.
     *
     * @param context The application context.
     * @param appkey The SDK appkey.
     * @param collectType The data collection mode.
     * @param otherOptions Optional parameters.
     * @param listener The initialization callback.
     * @return An error code.
     */
    public static int init(Context context, String appkey, int collectType,
                           Map<String, String> otherOptions, TTInitListener listener);
    • Parámetros:

      • context: Context. El contexto de su aplicación.

      • appkey: String. El appkey de su SDK.

      • collectType: int. El modo de recopilación. Valores válidos:

        Parámetro

        Descripción

        Ejemplo

        TT_DEFAULT

        Recopila todos los datos.

        TigerTallyAPI.TT_DEFAULT

        TT_NO_BASIC_DATA

        No recopila datos básicos del dispositivo.

        Incluye: nombre del dispositivo (Build.DEVICE), versión de Android (Build.VERSION#RELEASE) y resolución de pantalla.

        TigerTallyAPI.X | TigerTallyAPI.Y

        (Indica que no se recopilan ni X ni Y. X e Y representan los nombres de campo de elementos específicos).

        TT_NO_IDENTIFY_DATA

        No recopila datos de identificación del dispositivo.

        Incluye: IMEI, IMSI, SimSerial, BuildSerial (SN) y dirección MAC.

        TT_NO_UNIQUE_DATA

        No recopila datos de identificadores únicos.

        Incluye: OAID, Google Advertising ID y Android ID.

        TT_NO_EXTRA_DATA

        No recopila datos extendidos del dispositivo.

        Incluye: lista de aplicaciones maliciosas o en zona gris, IP de LAN, IP de DNS, información de Wi-Fi conectada (SSID, BSSID), lista de redes Wi-Fi cercanas, información de ubicación y datos de sensores.

        TT_NOT_GRANTED

        No recopila ninguno de los datos de privacidad anteriores.

        TigerTallyAPI.TT_NOT_GRANTED

      • otherOptions: Map<String, String>. Parámetros opcionales de recopilación de datos. Este valor puede ser null. Parámetros disponibles:

        Parámetro

        Descripción

        Ejemplo

        IPv6

        Especifica si se usa un nombre de dominio IPv6 para reportar la información del dispositivo.

        • 0 (predeterminado): usar un nombre de dominio IPv4.

        • 1: usar un nombre de dominio IPv6.

        1

        Intl

        Especifica si la información del dispositivo se reporta a una región fuera de China continental.

        • 0 (predeterminado): reportar a China continental.

        • 1: reportar a una región fuera de China continental.

        1

        CustomUrl

        El nombre de dominio del servidor de reporte de datos.

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

        CustomHost

        El host del servidor de reporte de datos.

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

        Nota

        Para la mayoría de los sitios internacionales, configure únicamente el parámetro Intl. Si necesita reportar datos a un sitio específico, establezca los parámetros CustomUrl y CustomHost. Los sitios disponibles incluyen:

        • Si Intl se establece en 0, los datos se reportan al sitio predeterminado en China (Shanghai): https://cloudauth-device.cn-shanghai.aliyuncs.com

        • Si Intl se establece en 1:

          • El sitio predeterminado es Singapore: https://cloudauth-device.ap-southeast-1.aliyuncs.com

          • Indonesia (Jakarta): https://cloudauth-device.ap-southeast-5.aliyuncs.com

          • US (Silicon Valley): https://cloudauth-device.us-west-1.aliyuncs.com

          • Germany (Frankfurt): https://cloudauth-device.eu-central-1.aliyuncs.com

          • China (Hong Kong): https://cloudauth-device.cn-hongkong.aliyuncs.com

      • listener: TTInitListener. La devolución de llamada de inicialización del SDK. Úsela para comprobar el estado de la inicialización. Este valor puede ser null.

        TTCode

        Code

        Descripción

        TT_SUCCESS

        0

        SDK inicializado correctamente.

        TT_NOT_INIT

        -1

        SDK no inicializado.

        TT_NOT_PERMISSION

        -2

        No se han concedido al SDK los permisos de Android obligatorios.

        TT_UNKNOWN_ERROR

        -3

        Error desconocido del sistema.

        TT_NETWORK_ERROR

        -4

        Error de red.

        TT_NETWORK_ERROR_EMPTY

        -5

        Error de red: respuesta vacía.

        TT_NETWORK_ERROR_INVALID

        -6

        Formato de respuesta de red no válido.

        TT_PARSE_SRV_CFG_ERROR

        -7

        Error al analizar sintácticamente la configuración del servidor.

        TT_NETWORK_RET_CODE_ERROR

        -8

        La puerta de enlace devolvió una respuesta con errores.

        TT_APPKEY_EMPTY

        -9

        Appkey vacío.

        TT_PARAMS_ERROR

        -10

        Parámetro no válido.

        TT_FGKEY_ERROR

        -11

        Error de cálculo de clave.

        TT_APPKEY_ERROR

        -12

        La versión del SDK y el appkey no coinciden.

    • Valor devuelto: int. Devuelve 0 si la operación se completó correctamente o -1 si falló.

    • Código de ejemplo:

      // The appkey is the authentication key assigned on the Alibaba Cloud platform.
      final String appkey="******";
      // Optional parameters to configure IPv6 and international reporting.
      Map<String, String> options = new HashMap<>();
      options.put("IPv6", "0");   // Use IPv4.
      options.put("Intl", "0");   // Report to the Chinese mainland.
      //options.put("Intl", "1"); // Report to a region outside the Chinese mainland.
      
      // Report to US (Silicon Valley).
      //options.put("CustomUrl", "https://cloudauth-device.us-west-1.aliyuncs.com"); 
      //options.put("CustomHost", "cloudauth-device.us-west-1.aliyuncs.com"); 
      
      // An initial data collection gathers device information one time. You can call the init function again to perform a new collection based on your business requirements.
      // Full collection.
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_DEFAULT, options, null);
      
      // Specify privacy data to collect. You can combine different flags by using the "|" operator.
      int privacyFlag = TigerTallyAPI.TT_NO_BASIC_DATA | TigerTallyAPI.TT_NO_UNIQUE_DATA;
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, privacyFlag, options, null);
      
      // Do not collect privacy fields.
      int ret = TigerTallyAPI.init(this.getApplicationContext(), appkey, TigerTallyAPI.TT_NOT_GRANTED, options, null);
      Log.d("AliSDK", "ret:" + ret);
  3. Calcule el hash de los datos.

    Este método de firma personalizada calcula un hash de los datos de entrada y devuelve la cadena whash generada como datos de firma personalizada. Para las solicitudes POST, PUT y PATCH, pase el cuerpo de la solicitud. Para las solicitudes GET y DELETE, pase la URL completa. La cadena whash debe agregarse al campo ali_sign_whash en el encabezado de la solicitud HTTP.

    // Request type:
    public enum RequestType { GET, POST, PUT, PATCH, DELETE }
    
    /**
     *  Hashes data for a custom signature.
     *
     * @param type  The request type.
     * @param input The data to hash.
     * @return The whash string.
     */
    public static String vmpHash(RequestType type, byte[] input);
    • Parámetros:

    • type: RequestType. El tipo de solicitud. Valores válidos:

      • GET: una solicitud GET.

      • POST: una solicitud POST.

      • PUT: una solicitud PUT.

      • PATCH: una solicitud PATCH.

      • DELETE: una solicitud DELETE.

    • input: byte[]. Los datos a los que se calculará el hash.

    • Valor devuelto: String. Devuelve la cadena whash.

    • Código de ejemplo:

      // GET request
      String url = "https://tigertally.aliyun.com/apptest";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      
      // POST request
      String body = "hello world";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      Nota

      Este método no es necesario para la firma predeterminada. En la firma personalizada, llame a este método para calcular el hash de los datos antes de firmarlos.

  4. Firme los datos.

    Este método utiliza tecnología VMP para firmar los datos de entrada y devuelve una cadena wtoken para la autenticación de la solicitud.

    /**
     * Signs the data.
     *
     * @param type The signature type.
     * @param input The data to sign.
     * @return The wtoken string.
     */
    public static String vmpSign(int type, byte[] input);
    • Parámetros:

      • type: int. El tipo de firma de datos. El valor debe ser 1.

      • input: byte[]. Los datos que se van a firmar. Normalmente corresponde a todo el cuerpo de la solicitud o a la cadena whash obtenida con la firma personalizada.

    • Valor devuelto: String. Devuelve la cadena wtoken.

    • Código de ejemplo:

      // Use this code if default signing is configured in the console (custom signing is not selected).
      String body = "i am the request body, encrypted or not!";
      String wtoken = TigerTallyAPI.vmpSign(1, body.getBytes("UTF-8"));
      Log.d("AliSDK", "wToken:" + wtoken);
      
      // Use this code if custom signing is configured in the console.
      // GET request
      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 request
      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);
      Nota
      • Cuando llama a vmpHash para la firma personalizada, el parámetro input del método vmpSign es la cadena whash generada. Al configurar las políticas anti-bots de la aplicación, el valor de Custom Signature Field debe establecerse en ali_sign_whash.

      • Cuando llama a vmpHash para generar una cadena whash en una solicitud GET, asegúrese de que la URL de entrada sea idéntica a la URL final utilizada en la solicitud de red. Preste especial atención a la codificación de la URL, ya que algunos frameworks codifican automáticamente los caracteres chinos o los parámetros.

      • El parámetro input del método vmpHash no admite una matriz de bytes vacía ni una cadena vacía. Si la entrada es una URL, debe contener una ruta o parámetros.

      • Al llamar a vmpSign, si el cuerpo de la solicitud está vacío (por ejemplo, en una solicitud GET o en una solicitud POST con cuerpo vacío), pase un objeto null o el valor en bytes de una cadena vacía, como "".getBytes("UTF-8").

      • Si el valor de whash o de wtoken es una de las siguientes cadenas, significa que el SDK devolvió un error:

        • you must call init first: el SDK no está inicializado. Llame primero a init().

        • you must input correct data: los datos de entrada no son válidos.

        • you must input correct type: el tipo de entrada no es válido.

3. Ejecutar la verificación secundaria

  1. Evalúe el resultado.

    Compruebe los campos cookie y body de la respuesta para determinar si se requiere una verificación secundaria. Antes de llamar a este método, combine varias entradas Set-Cookie en una sola cadena de cookies.

    /**
     * Determines whether to perform secondary verification.
     *
     * @param cookie The cookie string.
     * @param body The body string.
     * @return 0 for pass, or 1 for secondary verification required.
     */
    public static int cptCheck(String cookie, String body)
    • Parámetros:

      • cookie: String. Todas las cookies de la respuesta HTTP.

      • body: String. Todo el cuerpo de la respuesta HTTP.

    • Valor devuelto: int. Devuelve 0 si la solicitud pasó la validación o 1 si se requiere una verificación secundaria.

    • Código de ejemplo:

      String cookie = "key1=value1;kye2=value2;";
      String body = "....";
      int recheck = TigerTallyAPI.cptCheck(cookie, body);
      Log.d("AliSDK", "recheck:" + recheck);
  2. Cree un control deslizante.

    Si cptCheck devuelve 1, cree un objeto de control deslizante. El objeto TTCaptcha proporciona los métodos show() y dismiss() para mostrar y ocultar la ventana del control deslizante, respectivamente. TTOption encapsula los parámetros configurables del control deslizante, mientras que TTListener ofrece devoluciones de llamada para los estados de éxito y error. Para usar una ventana de control deslizante personalizada, pase la URL de la página correspondiente. Se admiten tanto archivos HTML locales como páginas remotas.

    /**
     * Creates a slider object.
     *
     * @param activity The activity where the slider is displayed.
     * @param option The slider parameters.
     * @param listener The callback for the slider.
     * @return The slider verification object.
     */
    public static TTCaptcha cptCreate(Activity activity, TTOption option, TTListener listener);
    
    /**
     * The slider object.
     */
    public class TTCaptcha {
        /**
         * Displays the slider.
         */
        public void show();
    
        /**
         * Hides the slider.
         */
        public void dismiss();
    
        /**
         * Gets the slider traceId for data statistics.
         */
        public String getTraceId();
    }
    
    /**
     * The slider parameters.
     */
    public static class TTOption {
        // Specifies whether the slider can be dismissed by clicking a blank area.
        public boolean cancelable;
    
        // The custom page. Local HTML files and remote URLs are supported.
        public String customUri;
    
        // The language.
        public String language;
    }
    
    /**
     * The callback for slider events.
     */
    public interface TTListener {
        /**
         * Called on successful verification.
         *
         * @param captcha The slider object.
         * @param data The token. Defaults to certifyId.
         */
        void success(TTCaptcha captcha, String data);
    
        /**
         * Called on verification failure.
         *
         * @param captcha The slider object.
         * @param code The error code.
         */
        void failed(TTCaptcha captcha, String code);
    }
    • Parámetros:

      • activity: Activity. La actividad de la página actual.

      • option: TTOption. Los parámetros de configuración del control deslizante.

      • listener: TTListener. La devolución de llamada del estado del control deslizante.

    • Valor devuelto: TTCaptcha. El objeto del control deslizante.

    • Código de ejemplo:

    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();
    Nota

    Un error de verificación indica que se detectó una excepción después de que el usuario completó el desafío del control deslizante.

    A continuación se describen los códigos de error:

    • 1001: la verificación falló.

    • 1002: excepción del sistema.

    • 1003: parámetro no válido.

    • 1005: verificación cancelada.

    • 8001: error al mostrar el control deslizante.

    • 8002: datos anómalos en la verificación del control deslizante.

    • 8003: excepción interna en la verificación del control deslizante.

    • 8004: error de red.

Ejemplo de mejores prácticas

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(() -> {
            // Initialize the SDK.
            Map<String, String> options = new HashMap<>();
            //options.put("Intl", "1"); // To enable international reporting.
            // Use full data collection mode.
            int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_DEFAULT, options, null);
            // Alternatively, do not collect privacy fields.
            // int ret = TigerTallyAPI.init(this, APP_KEY, TigerTallyAPI.TT_NOT_GRANTED, null, null);
            Log.d(TAG, "tiger tally init: " + ret);

            // Wait for initialization to complete.
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // Sign the data.
            String data = "hello world";
            String whash = null, wtoken = null;
            // Use custom signing.
            whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, data.getBytes());
            wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
            Log.d(TAG, "tiger tally vmp: " + whash + ", " + wtoken);

            // Use standard signing.
            // wtoken = TigerTallyAPI.vmpSign(1, data.getBytes());
            // Log.d(TAG, "tiger tally vmp: " + wtoken);

            // Send the request.
            doPost(APP_URL, APP_HOST, whash, wtoken, data, (code, cookie, body) -> {
                // Check if the slider is required.
                int recheck = TigerTallyAPI.cptCheck(cookie, body);
                Log.d(TAG, "captcha check result: " + recheck);

                if (recheck == 0) return;
                this.runOnUiThread(this::doShow);
            });
        }).start();
    }

    // Display the slider.
    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();
    }

    // Send the request.
    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);
    }
}