全部產品
Search
文件中心

Web Application Firewall:Android應用整合SDK

更新時間:Dec 10, 2025

您需要在應用中整合SDK,才能在控制台BOT管理中配置App防爬情境化規則。本文介紹了如何為Android應用整合WAF App防護SDK(以下簡稱SDK)。

背景資訊

App防護SDK主要用於對通過App用戶端發起的請求進行簽名。WAF服務端通過校正App請求籤名,識別App業務中的風險、攔截惡意請求,實現App防護的目的。

使用限制

  • Android應用支援如下2個軟體版本的SO:arm64-v8aarmeabi-v7a

  • Android應用的API版本必須是16及以上。

  • init初始化介面存在耗時操作,為確保安全能力完整性,建議在調用init介面後,確保至少間隔2秒再調用後續的vmpSign簽名介面。此間隔為推薦值(非強制要求),旨在提升SDK的防護效果。實際調用中可根據業務需求靈活調整,但縮短間隔可能影響安全能力的完整生效。

  • 當使用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包含2個AAR檔案,檔案名稱為AliTigerTally_X.Y.Z.aar、AliCaptcha_X.Y.Z.aar,其中X.Y.Z表示版本號碼。

  • 已擷取SDK認證密鑰(即appkey)。

    開啟BOT管理後,即可在BOT管理 > App防护列表中,單擊获取并复制appkey,擷取SDK認證密鑰。該密鑰用於發起SDK初始化請求,需要在整合代碼中使用。

    image

    說明

    每個阿里雲帳號擁有唯一的appkey(適用於所有接入WAF防護的網域名稱),且Android、iOS和Harmony應用整合SDK時都使用該appkey。

    認證密鑰樣本:****OpKLvM6zliu6KopyHIhmneb_****u4ekci2W8i6F9vrgpEezqAzEzj2ANrVUhvAXMwYzgY_****vc51aEQlRovkRoUhRlVsf4IzO9dZp6nN_****Wz8pk2TDLuMo4pVIQvGaxH3vrsnSQiK****。

步驟一:建立工程

以Android Studio工具為例,建立一個Android工程,並按照設定精靈完成建立。建立好的工程目錄如下圖所示。

image.png

步驟二:整合AAR包

  1. 將擷取到的SDK檔案tigertally-X.Y.Z-xxxxxx-android.tgz包解壓,將檔案夾中的所有aar檔案拷貝到主工程模組下的libs目錄中(具體以工程實際配置為準)。image.png

  2. 開啟App的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')
      
        // 三方庫依賴
        implementation 'com.alibaba:fastjson:1.2.83_noneautotype'
        implementation 'com.squareup.okhttp3:okhttp:3.11.0'
        implementation 'com.squareup.okio:okio:1.14.0
    }

步驟三:過濾SO CPU架構

如果專案在此之前未使用過SO,需在build.gradle中添加以下配置。

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

步驟四:為應用申請許可權

  • 必備許可權

    <uses-permission android:name="android.permission.INTERNET"/>
  • 可選許可權

    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
說明

android.permission.READ_EXTERNAL_STORAGE和android.permission.WRITE_EXTERNAL_STORAGE許可權在Android 6.0及以上版本需要動態申請。

步驟五:添加整合代碼

1. 添加標頭檔

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

2. 設定資料簽名

  1. 設定業務自訂的終端使用者標識,方便您更靈活地配置WAF防護策略。

    /**
    * 設定使用者賬戶
    *
    * @param account 賬戶
    * @return 錯誤碼
    */
    public static int setAccount(String account)
    • 參數說明

      • account,String類型,表示標識一個使用者的字串,建議您使用脫敏後的格式。

    • 傳回值:int類型,返回是否設定成功,0表示成功,-1表示失敗。

    • 範例程式碼

      // 遊客身份可以暫時先不setAccount,直接初始化;登入以後調用setAccount和重新初始化
      String account = "user001";
      TigerTallyAPI.setAccount(account);
  2. 初始化SDK,執行一次初始化採集。

    一次初始化採集表示採集一次終端裝置資訊,您可以根據業務的不同,重新調用init函數進行初始化採集。初始化採集分為三種模式:全量採集、自訂隱私採集、非隱私採集(不採集涉及終端裝置使用者隱私的欄位,包括:imei、imsi、simSerial、wifiMac、wifiList、bluetoothMac、androidId等)。

    說明

    建議在符合內部合規要求的前提下,選擇適配的採集模式,確保資料擷取的完整性。完整資料有助於更有效地識別潛在風險。

    // 初始化回調
    public interface TTInitListener {
        // code表示介面調用狀態代碼
        void onInitFinish(int code);
    }
    
    /**
     * SDK 初始化,帶 callback
     *
     * @param 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:String類型,設定為您的SDK認證密鑰。

      • 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廣告ID、Android ID。

        TT_NO_EXTRA_DATA

        表示不採集擴充裝置資料。

        包括:黑灰產App列表、區域網路IP、DNS IP、串連的WIFI資訊(SSID、BSSID)、附近WIFI列表、定位資訊、感應器資訊。

        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

        設定資料上報伺服器host

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

        說明

        常見國際網站設定Intl參數即可,只有指定網站上報需要設定CustomUrl和CustomHost,網站列表如下:

        • Intl = 0時,預設為上海網站: https://cloudauth-device.cn-shanghai.aliyuncs.com

        • Intl = 1時:

          • 預設為新加坡網站: 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

        Code

        備忘

        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代表阿里雲客戶平台分配的認證密鑰
      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. 資料雜湊。

    自訂加簽介面,對輸入資料input進行雜湊計算,返回產生的whash字串作為自訂簽名資料。Post、Put、Patch請求需要傳入request body,Get、Delete請求傳入完整的URL地址。同時,whash字串需要添加到HTTP請求header的ali_sign_whash中。

    // 請求類型:
    public enum RequestType { GET, POST, PUT, PATCH, DELETE }
    
    /**
     *  自訂Hash簽名資料 
     *
     * @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技術對輸入資料input進行簽名處理,並且返回wtoken字串用於請求認證。

    /**
     * 資料簽名
     *
     * @param type 簽名類型
     * @param input 簽名資料
     * @return wtoken
     */
    public static String vmpSign(int type, byte[] input);
    • 參數說明

      • type:int類型,設定資料簽名類型,固定取值1

      • input:byte[]類型,表示待簽名的資料,一般是整個請求體request body,或者是自訂加簽的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字串,且在配置App防爬情境化策略時,自訂加簽欄位的值需設定為ali_sign_whash。

      • 調用vmpHash產生Get請求的whash時,必須保證輸入的URL地址和最終網路請求的URL一致,特別需要注意UrlEncode情況,部分架構會自動對中文或者參數進行UrlEncode編碼。

      • 介面vmpHash的參數input不支援位元組或者Null 字元串,輸入為URL時必須存在Path或者Param。

      • 調用vmpSign時,如果請求體為空白(例如,Post請求或Get請求的body為空白),則填寫Null 物件null或Null 字元串的Bytes值(例如"".getBytes("UTF-8"))。

      • 當whash或wtoken為以下字串時表示初始化流程存在異常:

        • you must call init first:表示未調用init函數。

        • you must input correct data:表示傳入資料錯誤。

        • you must input correct type:表示傳入類型錯誤。

3. 二次校正

  1. 判斷結果。

    根據responsecookiebody欄位判斷是否要進行二次校正。header中可能存在多個Set-Cookie,需要按照cookie格式合并後調用該介面。

    /**
     * 判斷是否進行二次校正
     *
     * @param cookie cookie
     * @param body body
     * @return 0:通過 1:二次校正
     */
    public static int cptCheck(String cookie, String body)
    • 參數說明

      • cookie:String類型,佈建要求response中全部cookie

      • body:String類型,佈建要求response中全部body

    • 傳回值:int類型,返回決策結果,0表示通過,1表示需要二次校正。

    • 範例程式碼

      String cookie = "key1=value1;kye2=value2;";
      String body = "....";
      int recheck = TigerTallyAPI.cptCheck(cookie, body);
      Log.d("AliSDK", "recheck:" + recheck);
  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 token, 預設為certifyId
         */
        void success(TTCaptcha captcha, String data);
    
        /**
         * 驗證失敗
         *
         * @param captcha 滑塊對象
         * @param code 錯誤碼
         */
        void failed(TTCaptcha captcha, String code);
    }
    • 參數說明

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