Todos os produtos
Search
Central de documentação

Web Application Firewall:Integrate the SDK for Android

Última atualização: Sep 23, 2026

Para configure regras baseadas em cenário anti-crawler para o gerenciamento de bots no console, você deve integrar o SDK de App Protection do WAF. Este tópico explica como integrar o SDK a aplicativos Android.

Contexto

O SDK de App Protection assina as requisições dos clientes do aplicativo. O servidor do Web Application Firewall (WAF) verifica essas assinaturas de requisição para identificar riscos, bloquear requisições maliciosas e proteger o aplicativo.

Limitações

  • Para Android, o SDK oferece suporte às arquiteturas arm64-v8a e armeabi-v7a.

  • O nível da API do Android deve ser 16 ou superior.

  • O método init pode ser demorado. Para garantir a proteção total, aguarde pelo menos 2 segundos entre a chamada do método init e o método vmpSign. Esse atraso é recomendado para uma proteção ideal, mas não é obrigatório. Você pode ajustar esse atraso com base nas necessidades do seu negócio, mas um atraso menor pode impedir que os recursos de segurança funcionem plenamente.

  • Ao usar o ProGuard para obfuscação de código, utilize a opção -keep para preservar os métodos do SDK. Por exemplo:

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

Pré-requisitos

  • Obtenha o SDK para o seu aplicativo Android.

    Para obter o SDK, envie um ticket para nossos especialistas técnicos do product.

    Nota

    O SDK para Android inclui dois arquivos AAR: AliTigerTally_X.Y.Z.aar e AliCaptcha_X.Y.Z.aar, em que X.Y.Z é o número da versão.

  • Você obteve a chave de autenticação do SDK (também conhecida como appkey).

    Após ative o Gerenciamento de Bots, você pode Faça login no console do Web Application Firewall 3.0. Na barra de menu superior, selecione o grupo de recursos e a região (Chinese Mainland ou Outside Chinese Mainland) da instância do WAF.. No painel de navegação à esquerda, escolha Protection Config > Bot Management > Bot Management Template. acesse a aba App Protection e clique em Obtain and Copy AppKey no canto superior direito para recuperar a chave de autenticação do SDK. Essa chave é necessária para inicializar o SDK e deve ser incluída no seu código de integração.

    Nota

    Cada conta Alibaba Cloud possui uma appkey exclusiva para todos os nomes de domínio protegidos pelo WAF. Essa appkey é usada para a integração do SDK em aplicativos Android, iOS e HarmonyOS.

    Exemplo de chave de autenticação: ****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****.

Etapa 1: crie um projeto

No Android Studio, siga o assistente de configuração para criar um novo projeto Android. A estrutura de diretórios do projeto é a seguinte:


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

Etapa 2: Integrar o AAR

  1. Extraia o arquivo do SDK tigertally-X.Y.Z-xxxxxx-android.tgz e copie todos os arquivos AAR da pasta extraída para o diretório libs do seu módulo principal (o caminho exato depende da configure do seu projeto).

  2. Abra o arquivo build.gradle do seu aplicativo e adicione dependências para AliTigerTally_X.Y.Z.aar e AliCaptcha_X.Y.Z.aar a partir do diretório libs.

    Importante

    Substitua X.Y.Z nos nomes dos arquivos AliTigerTally_X.Y.Z.aar e AliCaptcha_X.Y.Z.aar pelo número da versão dos seus arquivos AAR.

    A configure é a seguinte:

    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'
    }

Etapa 3: Filtrar arquiteturas de CPU SO

Se o seu projeto ainda não utilizou arquivos SO, adicione a seguinte configuração ao seu arquivo build.gradle.

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

Etapa 4: Solicitar permissões

  • Permissão obrigatória

    <uses-permission android:name="android.permission.INTERNET"/>
  • Permissões opcionais

    <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

Para o Android 6.0 e versões posteriores, você deve solicitar dinamicamente android.permission.READ_EXTERNAL_STORAGE e android.permission.WRITE_EXTERNAL_STORAGE.

Etapa 5: Adicionar código de integração

1. Adicionar arquivos de cabeçalho

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

2. Configurar a assinatura de dados

  1. defina um ID personalizado para cada usuário final. Isso permite configure as políticas de mitigação do WAF de forma mais flexível.

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

      • account: String. Uma string que identifica um usuário. Recomendamos o uso de um formato com dados mascarados.

    • Valor de retorno: int. Retorna 0 em caso de sucesso ou -1 em caso de falha.

    • Código de exemplo:

      // 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. Inicialize o SDK e execute uma coleta inicial de dados.

    Uma coleta inicial de dados reúne as informações do dispositivo uma única vez. Chame a função init novamente para realizar uma nova coleta com base nos requisitos do seu negócio. Três modos de coleta estão disponíveis: coleta completa, coleta de privacidade personalizada e coleta sem privacidade. O modo sem privacidade não coleta dados de campos relacionados à privacidade do usuário final, incluindo: imei, imsi, simSerial, wifiMac, wifiList, bluetoothMac e androidId.

    Nota

    Selecione um modo de coleta que esteja alinhado com seus requisitos de conformidade e garanta a integridade dos dados. Dados completos melhoram a identificação de riscos.

    // 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. O contexto do seu aplicativo.

      • appkey: String. A appkey do seu SDK.

      • collectType: int. O modo de coleta. Valores válidos:

        Parâmetro

        Descrição

        Exemplo

        TT_DEFAULT

        Coleta todos os dados.

        TigerTallyAPI.TT_DEFAULT

        TT_NO_BASIC_DATA

        Não coleta dados básicos do dispositivo.

        Inclui: nome do dispositivo (Build.DEVICE), versão do Android (Build.VERSION#RELEASE) e resolução da tela.

        TigerTallyAPI.X | TigerTallyAPI.Y

        (Indica que nem X nem Y são coletados. X e Y representam os nomes dos campos de itens específicos.)

        TT_NO_IDENTIFY_DATA

        Não coleta dados de identificação do dispositivo.

        Inclui: IMEI, IMSI, SimSerial, BuildSerial (SN) e endereço MAC.

        TT_NO_UNIQUE_DATA

        Não coleta dados de identificadores exclusivos.

        Inclui: OAID, Google Advertising ID e Android ID.

        TT_NO_EXTRA_DATA

        Não coleta dados estendidos do dispositivo.

        Inclui: lista de aplicativos maliciosos/área cinzenta, IP de LAN, IP de DNS, informações de Wi-Fi conectado (SSID, BSSID), lista de Wi-Fi próximo, informações de localização e informações de sensor.

        TT_NOT_GRANTED

        Não coleta nenhum dos dados de privacidade anteriores.

        TigerTallyAPI.TT_NOT_GRANTED

      • otherOptions: Map<String, String>. Parâmetros opcionais de coleta de dados. Pode ser nulo. Parâmetros disponíveis:

        Parâmetro

        Descrição

        Exemplo

        IPv6

        Especifique se deve usar um nome de domínio IPv6 para relatar informações do dispositivo.

        • 0 (padrão): Usa um nome de domínio IPv4.

        • 1: Usa um nome de domínio IPv6.

        1

        Intl

        Especifique se deve relatar informações do dispositivo para uma região fora da China continental.

        • 0 (padrão): Relata para a China continental.

        • 1: Relata para uma região fora da China continental.

        1

        CustomUrl

        O nome de domínio do servidor de relatório de dados.

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

        CustomHost

        O host do servidor de relatório de dados.

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

        Nota

        Para a maioria dos sites internacionais, defina apenas o parâmetro Intl. Para relatar dados para um site específico, defina os parâmetros CustomUrl e CustomHost. Os sites disponíveis incluem:

        • Se Intl estiver definido como 0, os dados são relatados para o site padrão em China (Shanghai): https://cloudauth-device.cn-shanghai.aliyuncs.com

        • Se Intl estiver definido como 1:

          • O site padrão é 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. O callback de inicialização do SDK. Use o callback para verificar o status de inicialização. Pode ser nulo.

        TTCode

        Código

        Descrição

        TT_SUCCESS

        0

        SDK inicializado com sucesso.

        TT_NOT_INIT

        -1

        SDK não inicializado.

        TT_NOT_PERMISSION

        -2

        O SDK não recebeu as permissões necessárias do Android.

        TT_UNKNOWN_ERROR

        -3

        Erro desconhecido do sistema.

        TT_NETWORK_ERROR

        -4

        Erro de rede.

        TT_NETWORK_ERROR_EMPTY

        -5

        Erro de rede: resposta vazia.

        TT_NETWORK_ERROR_INVALID

        -6

        Formato de resposta de rede inválido.

        TT_PARSE_SRV_CFG_ERROR

        -7

        Falha ao analisar a configuração do servidor.

        TT_NETWORK_RET_CODE_ERROR

        -8

        O gateway retornou uma resposta de falha.

        TT_APPKEY_EMPTY

        -9

        Appkey vazia.

        TT_PARAMS_ERROR

        -10

        Parâmetro inválido.

        TT_FGKEY_ERROR

        -11

        Erro de cálculo da chave.

        TT_APPKEY_ERROR

        -12

        Incompatibilidade entre a versão do SDK e a appkey.

    • Valor de retorno: int. Retorna 0 em caso de sucesso ou -1 em caso de falha.

    • Código de exemplo:

      // 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 o hash dos dados.

    Este método de assinatura personalizada calcula um hash dos dados de entrada e retorna a string whash gerada como os dados de assinatura personalizada. Para requisições POST, PUT e PATCH, passe o corpo da requisição. Para requisições GET e exclua, passe a URL completa. A string whash deve ser adicionada ao campo ali_sign_whash no cabeçalho da requisição 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. O tipo de requisição. Valores válidos:

      • GET: Uma requisição GET.

      • POST: Uma requisição POST.

      • PUT: Uma requisição PUT.

      • PATCH: Uma requisição PATCH.

      • exclua: Uma requisição exclua.

    • input: byte[]. Os dados para calcular o hash.

    • Valor de retorno: String. Retorna a string whash.

    • Código de exemplo:

      // 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 não é necessário para a assinatura padrão. Para assinatura personalizada, chame este método para calcular o hash dos dados antes de assiná-los.

  4. Assine os dados.

    Este método usa a tecnologia VMP para assinar os dados de entrada e retorna uma string wtoken para autenticação da requisição.

    /**
     * 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. O tipo de assinatura de dados. O valor deve ser 1.

      • input: byte[]. Os dados a serem assinados. Geralmente, é todo o corpo da requisição ou a string whash da assinatura personalizada.

    • Valor de retorno: String. Retorna a string wtoken.

    • Código de exemplo:

      // 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
      • Ao chamar vmpHash para assinatura personalizada, o parâmetro input do método vmpSign é a string whash gerada. Ao configure as políticas anti-bot do aplicativo, o valor do Custom Signature Field deve ser definido como ali_sign_whash.

      • Ao chamar vmpHash para gerar uma string whash para uma requisição GET, certifique-se de que a URL de entrada seja idêntica à URL final usada na requisição de rede. Preste atenção especial à codificação da URL, pois alguns frameworks codificam automaticamente caracteres chineses ou parâmetros.

      • O parâmetro input do método vmpHash não aceita um array de bytes vazio ou uma string vazia. Se a entrada for uma URL, ela deve conter um caminho ou parâmetros.

      • Ao chamar vmpSign, se o corpo da requisição estiver vazio (por exemplo, em uma requisição GET ou uma requisição POST com corpo vazio), passe um objeto null ou o valor em bytes de uma string vazia, como "".getBytes("UTF-8").

      • Se o valor de whash ou wtoken for uma das seguintes strings, o SDK retornou um erro:

        • you must call init first: O SDK não está inicializado. Chame init() primeiro.

        • you must input correct data: Dados de entrada inválidos.

        • you must input correct type: Tipo de entrada inválido.

3. Realizar a verificação secundária

  1. Avalie o resultado.

    Verifique os campos cookie e body na response para determinar se a verificação secundária é necessária. Antes de chamar este método, mescle várias entradas Set-Cookie em uma única string de cookie.

    /**
     * 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. Todos os cookies da response HTTP.

      • body: String. O body completo da response HTTP.

    • Valor de retorno: int. Retorna 0 se a requisição for aprovada ou 1 se a verificação secundária for necessária.

    • Código de exemplo:

      String cookie = "key1=value1;kye2=value2;";
      String body = "....";
      int recheck = TigerTallyAPI.cptCheck(cookie, body);
      Log.d("AliSDK", "recheck:" + recheck);
  2. crie um slider.

    Se cptCheck retornar 1, crie um objeto slider. O objeto TTCaptcha fornece os métodos show() e dismiss() para exibir e ocultar a janela do slider, respectivamente. TTOption encapsula os parâmetros configure do slider, e TTListener fornece callbacks para os estados de sucesso e falha. Para usar uma janela de slider personalizada, passe a URL da página personalizada. Há suporte tanto para arquivos HTML locais quanto para 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. A activity da página atual.

      • option: TTOption. Os parâmetros de configure do slider.

      • listener: TTListener. O callback de status do slider.

    • Valor de retorno: TTCaptcha. O objeto slider.

    • Código de exemplo:

    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

    Uma falha na verificação indica que uma exceção foi detectada após o usuário concluir o desafio do slider.

    A lista a seguir descreve os códigos de erro:

    • 1001: Falha na verificação.

    • 1002: Exceção do sistema.

    • 1003: Parâmetro inválido.

    • 1005: Verificação cancelada.

    • 8001: Falha ao exibir o slider.

    • 8002: Dados de verificação do slider anormais.

    • 8003: Exceção interna na verificação do slider.

    • 8004: Erro de rede.

Exemplo de melhor prática

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);
    }
}