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

Web Application Firewall:Android SDK の統合

最終更新日:Apr 23, 2026

コンソールでボット管理のクローラー対策シナリオベースのルールを設定するには、WAF App Protection SDK を統合する必要があります。このトピックでは、SDK を Android アプリに統合する方法について説明します。

背景情報

App Protection SDK は、アプリケーションクライアントからのリクエストに署名します。Web Application Firewall (WAF) サーバーは、これらのリクエスト署名を検証してリスクを特定し、攻撃リクエストをブロックして、アプリケーションを保護します。

制限事項

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

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

  • init メソッドの呼び出しには時間がかかることがあります。完全な保護を確保するため、init メソッドの呼び出しから vmpSign メソッドの呼び出しまで、少なくとも 2 秒の間隔を空けることを推奨します。この遅延は最適な保護のために推奨されますが、必須ではありません。ビジネスニーズに応じてこの遅延を調整できますが、遅延が短いとセキュリティ機能が完全に有効にならない可能性があります。

  • ProGuard を使用してコードを難読化する場合は、-keep オプションを使用して SDK のメソッドを保持してください。例:

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

前提条件

  • ご利用の Android アプリ用の SDK を取得します。

    SDK を取得するには、製品技術エキスパートにチケットを送信してください。

    説明

    Android SDK には、AliTigerTally_X.Y.Z.aarAliCaptcha_X.Y.Z.aar の 2 つの AAR ファイルが含まれています。X.Y.Z はバージョン番号です。

  • SDK 認証キー (appkey とも呼ばれます) を取得済みであること。

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

    image

    説明

    Alibaba Cloud アカウントには、WAF によって保護されているすべてのドメイン名に対して一意の appkey があります。この appkey は、Android、iOS、および HarmonyOS アプリ全体の SDK 統合に使用されます。

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

ステップ 1:プロジェクトの作成

Android Studio で、構成ウィザードに従って新しい Android プロジェクトを作成します。プロジェクトディレクトリは下図のようになります。

image.png

ステップ 2:AAR の統合

  1. tigertally-X.Y.Z-xxxxxx-android.tgz SDK ファイルを展開し、展開したフォルダーからすべての 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 を、ご利用の AAR ファイルのバージョン番号に置き換えてください。

    構成は次のとおりです:

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

ステップ 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_STORAGEandroid.permission.WRITE_EXTERNAL_STORAGE を動的にリクエストする必要があります。

ステップ 5:統合コードの追加

1. ヘッダーファイルの追加

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

2. データ署名のセットアップ

  1. 各エンドユーザーにカスタム ID を設定します。これにより、WAF の軽減ポリシーをより柔軟に構成できます。

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

      • account:String。ユーザーを識別する文字列。非機密化されたフォーマットを使用することを推奨します。

    • 戻り値:int。成功した場合は 0、失敗した場合は -1 を返します。

    • サンプルコード

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

    初回のデータ収集では、デバイス情報が一度収集されます。ビジネス要件に応じて init 関数を再度呼び出し、新しい収集を実行できます。収集モードには、完全収集、カスタムプライバシー収集、非プライバシー収集の 3 つがあります。非プライバシーモードでは、imei、imsi、simSerial、wifiMac、wifiList、bluetoothMac、androidId など、エンドユーザーのプライバシーに関連するフィールドのデータは収集されません。

    説明

    コンプライアンス要件に合致し、データ整合性を確保できる収集モードを選択してください。データが完全であるほど、リスク識別の精度が向上します。

    // 初期化コールバック
    public interface TTInitListener {
        // code パラメーターはインターフェイス呼び出しのステータスコードを示します。
        void onInitFinish(int code);
    }
    
    /**
     * コールバック付きで SDK を初期化します。
     *
     * @param context アプリケーションコンテキスト。
     * @param appkey SDK の appkey。
     * @param collectType データ収集モード。
     * @param otherOptions オプションのパラメーター。
     * @param listener 初期化コールバック。
     * @return エラーコード。
     */
    public static int init(Context context, String appkey, int collectType,
                           Map<String, String> otherOptions, TTInitListener listener);
    • パラメーター

      • context:Context。アプリケーションのコンテキスト。

      • appkey:String。SDK の appkey。

      • 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:中国本土以外のリージョンにレポートします。

        1

        CustomUrl

        データレポートサーバーのドメイン名。

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

        CustomHost

        データレポートサーバーのホスト。

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

        説明

        ほとんどの国際サイトでは、Intl パラメーターのみを設定します。データを特定のサイトにレポートするには、CustomUrl および CustomHost パラメーターを設定します。利用可能なサイトは次のとおりです:

        • Intl0 に設定されている場合、データは中国 (上海) のデフォルトサイトにレポートされます:https://cloudauth-device.cn-shanghai.aliyuncs.com

        • Intl1 に設定されている場合:

          • デフォルトサイトはシンガポールです:https://cloudauth-device.ap-southeast-1.aliyuncs.com

          • インドネシア (ジャカルタ):https://cloudauth-device.ap-southeast-5.aliyuncs.com

          • 米国 (シリコンバレー):https://cloudauth-device.us-west-1.aliyuncs.com

          • ドイツ (フランクフルト):https://cloudauth-device.eu-central-1.aliyuncs.com

          • 中国 (香港):https://cloudauth-device.cn-hongkong.aliyuncs.com

      • listener:TTInitListener。SDK 初期化コールバック。コールバックを使用して初期化ステータスを確認します。null も可。

        TTCode

        コード

        説明

        TT_SUCCESS

        0

        SDK が正常に初期化されました。

        TT_NOT_INIT

        -1

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

        TT_NOT_PERMISSION

        -2

        SDK に必要な Android 権限が付与されていません。

        TT_UNKNOWN_ERROR

        -3

        不明なシステムエラー。

        TT_NETWORK_ERROR

        -4

        ネットワークエラー。

        TT_NETWORK_ERROR_EMPTY

        -5

        ネットワークエラー:空のレスポンス。

        TT_NETWORK_ERROR_INVALID

        -6

        無効なネットワークレスポンス形式。

        TT_PARSE_SRV_CFG_ERROR

        -7

        サーバー構成の解析に失敗しました。

        TT_NETWORK_RET_CODE_ERROR

        -8

        ゲートウェイが失敗レスポンスを返しました。

        TT_APPKEY_EMPTY

        -9

        空の appkey。

        TT_PARAMS_ERROR

        -10

        無効なパラメーター。

        TT_FGKEY_ERROR

        -11

        キー計算エラー。

        TT_APPKEY_ERROR

        -12

        SDK バージョンと appkey が一致しません。

    • 戻り値:int。成功した場合は 0、失敗した場合は -1 を返します。

    • サンプルコード

      // appkey は 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"); 
      
      // 初回のデータ収集では、デバイス情報が一度収集されます。ビジネス要件に応じて 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[]。ハッシュ化するデータ。

    • 戻り値:String。whash 文字列を返します。

    • サンプルコード

      // GET リクエスト
      String url = "https://tigertally.aliyun.com/apptest";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      
      // POST リクエスト
      String body = "hello world";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
      Log.d("AliSDK", "whash:" + whash);
      説明

      このメソッドはデフォルト署名には必要ありません。カスタム署名の場合は、このメソッドを呼び出してデータをハッシュ化してから署名します。

  4. データの署名。

    このメソッドは VMP テクノロジーを使用して入力データに署名し、リクエスト認証のための wtoken 文字列を返します。

    /**
     * データに署名します。
     *
     * @param type 署名タイプ。
     * @param input 署名するデータ。
     * @return wtoken 文字列。
     */
    public static String vmpSign(int type, byte[] input);
    • パラメーター

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

      • input:byte[]。署名するデータ。これは通常、完全なリクエストボディまたはカスタム署名からの whash 文字列です。

    • 戻り値:String。wtoken 文字列を返します。

    • サンプルコード

      // コンソールでデフォルト署名が構成されている場合 (カスタム署名は選択されていない) は、このコードを使用します。
      String body = "i am the request body, encrypted or not!";
      String wtoken = TigerTallyAPI.vmpSign(1, body.getBytes("UTF-8"));
      Log.d("AliSDK", "wToken:" + wtoken);
      
      // コンソールでカスタム署名が構成されている場合は、このコードを使用します。
      // GET リクエスト
      String url = "https://tigertally.aliyun.com/apptest";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.GET, url.getBytes());
      String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
      Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken);
      
      // POST リクエスト
      String body = "hello world";
      String whash = TigerTallyAPI.vmpHash(TigerTallyAPI.RequestType.POST, body.getBytes());
      String wtoken = TigerTallyAPI.vmpSign(1, whash.getBytes());
      Log.d("AliSDK", "whash:" + whash + ", wtoken:" + wtoken);
      説明
      • カスタム署名のために vmpHash を呼び出す場合、vmpSign メソッドの input パラメーターは生成された whash 文字列になります。アプリのボット対策ポリシーを構成する際、カスタム署名フィールドの値は ali_sign_whash に設定する必要があります。

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

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

      • vmpSign を呼び出す際、リクエストボディが空の場合 (たとえば、GET リクエストや空のボディを持つ POST リクエスト)、null オブジェクトまたは空の文字列のバイト値 (例:"".getBytes("UTF-8")) を渡してください。

      • whash または wtoken の値が次のいずれかの文字列である場合、SDK はエラーを返しています:

        • you must call init first: SDK が初期化されていません。最初に init() を呼び出してください。

        • you must input correct data: 無効な入力データ。

        • you must input correct type: 無効な入力タイプ。

3. 二次検証の実行

  1. 結果の評価。

    レスポンスcookiebody フィールドをチェックして、二次検証が必要かどうかを判断します。このメソッドを呼び出す前に、複数の Set-Cookie エントリを単一の cookie 文字列にマージしてください。

    /**
     * 二次検証を実行するかどうかを判断します。
     *
     * @param cookie cookie 文字列。
     * @param body body 文字列。
     * @return パスの場合は 0、二次検証が必要な場合は 1。
     */
    public static int cptCheck(String cookie, String body)
    • パラメーター

      • cookie:String。HTTP レスポンスからのすべての cookie

      • body:String。HTTP レスポンスからの完全な body

    • 戻り値:int。リクエストがパスした場合は 0、二次検証が必要な場合は 1 を返します。

    • サンプルコード

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

    cptCheck が 1 を返した場合、スライダーオブジェクトを作成します。TTCaptcha オブジェクトは、スライダーウィンドウを表示および非表示にするための show() および dismiss() メソッドを提供します。TTOption はスライダーの構成可能なパラメーターをカプセル化し、TTListener は成功および失敗状態のコールバックを提供します。カスタムスライダーウィンドウを使用するには、カスタムページの URL を渡します。ローカル 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(() -> {
            // SDK を初期化します。
            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);
    }
}