すべてのプロダクト
Search
ドキュメントセンター

Web Application Firewall:Android アプリ用 SDK の統合

最終更新日:Aug 28, 2025

Android アプリにアンチクローラー ルールを設定するには、まずアプリ保護 SDK を統合する必要があります。このトピックでは、必要な統合手順について説明します。

背景情報

アプリ保護 SDK は、アプリから開始されたすべてのリクエストに署名することで、信頼できる通信を保証します。その後、Web Application Firewall(WAF)サーバーがこれらの署名を確認します。このプロセスにより、WAF はボットやその他の攻撃者からの悪意のあるリクエストを識別してブロックし、アプリケーションのサービスを保護できます。

制限事項

  • SDK は、Android OS の arm64-v8a および armeabi-v7a アーキテクチャをサポートしています。

  • Android OS の API レベルは 16 以上である必要があります。

  • init メソッドは時間のかかる操作を実行します。セキュリティ機能が完全に初期化されるようにするには、init を呼び出してから少なくとも 2 秒待ってから、vmpSign メソッドを呼び出します。これは、保護を最大限にするための推奨遅延であり、必要に応じて調整できます。ただし、遅延が短すぎると、セキュリティ機能が完全に有効にならない場合があります。

  • コードの難読化に ProGuard を使用する場合、SDK の API メソッドを保持するために -keep オプションを使用します。次に例を示します。

-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 には、2 つの AAR ファイル(AliTigerTally_X.Y.Z.aarAliCaptcha_X.Y.Z.aar)が含まれています。ここで、X.Y.Z はバージョン番号を表します。

  • SDK 認証キー(appkey)を取得済みであること。

    BOT Management を有効にした後、Bot Management > アプリ 保護 ページに移動します。アプリ一覧で、AppKey の取得と複製 をクリックして SDK 認証キーを取得します。このキーは SDK 初期化リクエストに必要であり、統合コードに含める必要があります。

    image

    説明

    各 Alibaba Cloud アカウントには、WAF によって保護されているすべてのドメイン名に適用される一意の appkey があります。この appkey は、Android、iOS、および Harmony アプリケーションの SDK 統合に使用されます。

    認証キーの例:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。

ステップ 1:プロジェクトを作成する

Android Studio を例として使用します。

設定ウィザードに従って Android プロジェクトを作成します。プロジェクト ディレクトリを次の図に示します。

image.png

ステップ 2:AAR パッケージを統合する

  1. tigertally-X.Y.Z-xxxxxx-android.tgz パッケージを展開します。結果のフォルダからすべての AAR ファイルをメイン モジュールの libs ディレクトリにコピーします(具体的なパスはプロジェクトの構成によって異なる場合があります)。image.png

  2. アプリの build.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 を実際のバージョン番号に置き換える必要があります。

    構成は次のとおりです。

    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
    }

ステップ 3:SO CPU アーキテクチャをフィルタリングする

プロジェクトで SO ファイルをまだ使用していない場合は、build.gradle ファイルに次の構成を追加します。

android {
    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', '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 6.0 以降では、android.permission.READ_EXTERNAL_STORAGE 権限と android.permission.WRITE_EXTERNAL_STORAGE 権限を動的にリクエストする必要があります。

ステップ 5:統合コードを追加する

ヘッダー ファイルを追加する

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

データ署名を設定する

  1. ビジネスのカスタム エンドユーザー ID を設定します。

    これにより、WAF 軽減ポリシーを柔軟に構成できます。

    /**
    * ユーザー アカウントを設定する
    *
    * @param account アカウント
    * @return エラーコード
    */
    public static int setAccount(String account)
    • パラメーター

      account:ユーザーを識別する文字列。機密性の低い形式を使用することをお勧めします。

    • 戻り値:設定が成功した場合は 0 を返し、失敗した場合は -1 を返します。データ型:int。

    • サンプル コード

      // ゲストの場合は、setAccount をスキップして SDK を直接初期化できます。ログイン後、setAccount を呼び出して再初期化します。
      String account = "user001";
      TigerTallyAPI.setAccount(account);
  2. SDK を初期化し、データ収集を実行します。

    初期化プロセスでは、デバイス情報が 1 回収集されます。さまざまなビジネス要件に基づいて新しい収集プロセスを開始するには、init メソッドを再度呼び出すことができます。初期化には、完全収集、カスタム プライバシー収集、非プライバシー収集の 3 つのモードがあります。非プライバシー モードでは、imei、imsi、simSerial、wifiMac、wifiList、bluetoothMac、androidId など、ユーザーのプライバシーに関連するフィールドは収集されません。

    説明

    内部コンプライアンス要件に準拠し、データの整合性を確保する収集モードを選択することをお勧めします。完全なデータは、潜在的なリスクをより効果的に特定するのに役立ちます。

    // 初期化コールバック
    public interface TTInitListener {
        // code は、インターフェイス呼び出しの状態コードを示します
        void onInitFinish(int code);
    }
    
    /**
     * コールバック付き SDK 初期化
     *
     * @param appkey 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:SDK appkey。データ型:String。

      • 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 Advertising ID、Android ID が含まれます。

        TT_NO_EXTRA_DATA

        拡張デバイス データを収集しません。悪意のある/グレーエリアのアプリ リスト、LAN IP、DNS IP、接続されている Wi-Fi 情報(SSID、BSSID)、近くの Wi-Fi リスト、位置情報、センサー情報が含まれます。

        TT_NOT_GRANTED

        上記のプライバシー データを一切収集しません。

        TigerTallyAPI.TT_NOT_GRANTED

      • otherOptions:オプション。データ型:Map<String,String>。デフォルト値:null。有効な値:

        フィールド名

        説明

        IPv6

        デバイス情報を報告するために IPv6 ドメイン名を使用するかどうかを指定します。有効な値:

        • 0(デフォルト):IPv4 ドメイン名を使用します。

        • 1:IPv6 ドメイン名を使用します。

        1

        Intl

        デバイス情報が報告されるリージョンを指定します。有効な値:

        • 0(デフォルト):中国本土

        • 1:中国本土以外

        中国本土以外の WAF インスタンスの場合は、これを 1 に設定します。それ以外の場合は、デフォルトの 0 を使用します。

        1

        CustomUrl

        データ レポーティング サーバーのドメイン名を設定します。

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

        CustomHost

        データ レポーティング サーバーのホストを設定します。

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

        説明

        一般的な国際サイトの場合は、Intl パラメーターを設定するだけで十分です。CustomUrl パラメーターと CustomHost パラメーターは、特定のサイト(米国西部サイトなど:https://cloudauth-device.us-west-1.aliyuncs.com)に報告する場合にのみ必要です。

      • listener:SDK 初期化コールバック インターフェイス。データ型:TTInitListener。コールバックで初期化結果の具体的なステータスを確認できます。デフォルト値:null。

        TTCode

        コード

        備考

        TT_SUCCESS

        0

        SDK が初期化されています。

        TT_NOT_INIT

        -1

        SDK が初期化されていません。

        TT_NOT_PERMISSION

        -2

        必要な基本的な Android 権限が SDK に完全に付与されていません。

        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 バージョンと一致しません。

    • 戻り値:初期化が成功した場合は 0 を返し、失敗した場合は -1 を返します。データ型:int。

    • サンプル コード

      // Appkey は、Alibaba Cloud カスタマー プラットフォームによって割り当てられた認証キーを表します。
      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"); 
      
      // 初期化コレクションは、デバイス情報が 1 回収集されることを示します。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. データをハッシュ化します。

    このカスタム署名インターフェイスは、入力データのハッシュを計算し、生成された whash 文字列をカスタム署名として返します。POST、PUT、PATCH リクエストの場合はリクエスト本文を入力として渡す必要がありますが、GET および DELETE リクエストの場合は完全な URL を入力として使用する必要があります。また、whash 文字列を HTTP リクエスト ヘッダーの ali_sign_whash フィールドに追加する必要があります。

    // リクエスト タイプ:
    public enum RequestType { GET, POST, PUT, PATCH, DELETE }
    
    /**
     *  カスタム ハッシュ署名データ
     *
     * @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[]。

  • 戻り値:whash 文字列。データ型:String。

  • サンプル コード

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

    このインターフェイス呼び出しは、デフォルトの署名を使用する場合は不要です。カスタム署名の場合は、データに署名する前に、ハッシュ検証のためにインターフェイスを呼び出す必要があります。

  1. データに署名します。

    このメソッドは vmp 技術を使用して入力データに署名し、リクエスト認証用の wtoken 文字列を返します。

    /**
     * データ署名
     *
     * @param type 署名タイプ
     * @param input 署名データ
     * @return wtoken
     */
    public static String vmpSign(int type, byte[] input);
  • パラメーター

    • type:データ署名タイプ。値は 1 である必要があります。データ型:int。

    • input:署名されるデータ。これは通常、リクエスト全体またはカスタム署名の whash です。データ型:byte[]。

  • 戻り値:wtoken 文字列。データ型:String。

  • サンプル コード

    // コンソールはデフォルトの署名で構成されています。つまり、カスタム署名は選択されていません。
    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 文字列です。さらに、アプリのシナリオ固有のボット対策ポリシーを構成する場合、カスタム署名フィールドの値を ali_sign_whash に設定する必要があります。

    • GET リクエストの whash を生成するために vmpHash を呼び出す場合、入力 URL がネットワーク リクエストで使用される最終 URL と同一であることを確認してください。URL エンコーディングに特に注意してください。一部のフレームワークは、漢字またはパラメーターの URL エンコーディングを自動的に実行します。

    • vmpHash メソッドの input パラメーターは、空の文字列をサポートしていません。入力が URL の場合は、パスまたはパラメーターが存在する必要があります。

    • vmpSign を呼び出すときに、リクエスト本文が空の場合(たとえば、POST または GET リクエストの本文が空の場合)、null オブジェクトまたは空の文字列のバイト値("".getBytes("UTF-8") など)を渡します。

    • whash または wtoken が次のいずれかの文字列である場合、初期化中に例外が発生しました。

      • you must call init first: init メソッドが呼び出されなかったことを示します。

      • you must input correct data: 入力データが正しくないことを示します。

      • you must input correct type: 入力タイプが正しくないことを示します。

2 要素認証を実行する

  1. 結果を評価します。

    応答cookie フィールドと body フィールドをチェックして、二次検証が必要かどうかを判断します。ヘッダーには複数の Set-Cookie エントリが含まれている場合があり、このインターフェイスを呼び出す前に cookie 形式にマージする必要があります。

    /**
     * 二要素認証を実行するかどうかを評価します
     *
     * @param cookie クッキー
     * @param body 本文
     * @return 0: パス 1: 二要素認証
     */
    public static int cptCheck(String cookie, String body)
    • パラメーター

      • cookie:リクエスト応答のすべての cookie。データ型:String。

      • body:リクエスト応答の本文全体。データ型:String。

    • 戻り値:決定結果を返します。0 はリクエストが合格したことを示します。1 は二要素認証が必要であることを示します。データ型:int。

    • サンプル コード

      String cookie = "key1=value1;kye2=value2;";
      String body = "....";
      int recheck = TigerTallyAPI.cptCheck(cookie, body);
      Log.d("AliSDK", "recheck:" + recheck);
  2. スライダーを作成します。

    cptCheck から返された結果に基づいて、スライダー オブジェクトを作成します。TTCaptcha オブジェクトは、スライダー ウィンドウを表示および非表示にする show メソッドと dismiss メソッドを提供します。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 トークン、デフォルトは certifyId
         */
        void success(TTCaptcha captcha, String data);
    
        /**
         * 検証失敗
         *
         * @param captcha スライダー オブジェクト
         * @param code エラーコード
         */
        void failed(TTCaptcha captcha, String code);
    }
    • パラメーター

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