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

Web Application Firewall:iOS アプリケーションの SDK を統合する

最終更新日:Oct 16, 2025

BOT Management コンソールでアプリ保護のためのシナリオベースの軽減ポリシーを設定するには、Web Application Firewall (WAF) App Protection ソフトウェア開発キット (SDK) をアプリケーションに統合する必要があります。このトピックでは、iOS アプリケーションに SDK を統合する方法について説明します。

背景情報

App Protection SDK は、主にアプリクライアントから開始されるリクエストに署名するために使用されます。WAF サーバーは、アプリリクエストの署名を検証して、アプリケーションサービス内の脅威を検出し、悪意のあるリクエストをブロックします。これにより、アプリケーションを保護できます。

制限事項

  • iOS SDK には、広告主識別子 (IDFA) バージョンと非 IDFA バージョンの 2 つのバージョンがあります。対応する SDK ファイルは次のとおりです。

    • AliTigerTally_IDFA.framework

    • AliTigerTally_NOIDFA.framework

    iOS プロジェクトで IDFA を使用する場合は、SDK の AliTigerTally_IDFA バージョンを統合できます。それ以外の場合は、AliTigerTally_NOIDFA バージョンを使用できます。

  • init インターフェイスの呼び出しには時間がかかります。完全なセキュリティ保護を確保するために、init インターフェイスを呼び出した後、vmpSign 署名インターフェイスを呼び出す前に、少なくとも 2 秒待つことを推奨します。この間隔は推奨事項であり、要件ではありません。SDK の保護効果を向上させることを目的としています。ビジネスニーズに応じて間隔を調整できます。ただし、間隔が短いと、セキュリティ機能が完全に有効にならない場合があります。

  • iOS アプリケーションは、iOS バージョン 9.0 以降で実行されている必要があります。App Protection SDK は、以前のバージョンと統合することはできません。

前提条件

  • iOS アプリケーション用の SDK を取得済みであること。

    SDK を取得するには、チケットを送信して、プロダクト技術専門家にお問い合わせください。

    説明

    iOS アプリケーションの SDK ファイル名は tigertally-X.Y.Z-xxxx-ios.zip です。ここで、X.Y.Z はバージョン番号を表します。このファイルには、2 つの framework ファイルと 2 つの xcframework ファイルが含まれています。

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

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

    image

    説明

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

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

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

この例では Xcode 環境を使用します。新しい iOS プロジェクトを作成し、セットアップウィザードを完了します。プロジェクトディレクトリは、次の図に示すように作成されます。image.png

ステップ 2: フレームワークパッケージの統合

SDK ファイル AliTigerTally_IDFA.framework または AliTigerTally_NOIDFA.framework をプロジェクトに追加します。

  • IDFA バージョンimage.png

  • 非 IDFA バージョンimage.png

Captcha モジュール SDK ファイル AliCaptcha.framework をプロジェクトに追加します。

image.png

リソースファイル AliCaptcha.bundle をプロジェクトに追加します。

image.png

ステップ 3: 依存ライブラリの追加

依存ライブラリ

IDFA バージョンで必須

非 IDFA バージョンで必須

libc++.tbd

はい

はい

libresolv.9.tbd

はい

はい

CoreTelephony.framework

はい

はい

AdSupport.framework

はい

いいえ

AppTrackingTransparency.framework

はい

いいえ

image.png

ステップ 4: オプションの編集

[Other Linker Flags] オプションで、-ObjC を追加します。image.png

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

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

  • IDFA バージョンの場合は、次の構成を使用します。

    #import <AliTigerTally_IDFA/AliTigerTally.h>
  • 非 IDFA バージョンの場合は、次の構成を使用します。

    #import <AliTigerTally_NOIDFA/AliTigerTally.h> 

2. データ署名の設定

  1. ビジネスに合わせてカスタムのエンドユーザー ID を設定できます。これにより、WAF 軽減ポリシーをより柔軟に設定できます。

    /**
    * ユーザーアカウントを設定します。
    *
    * @param account   アカウント情報。
    */
    - (void)setAccount:(NSString *)account;
    • パラメーター

      • account: ユーザーを識別する文字列。非識別化されたフォーマットを使用してください。型: NSString。

    • 戻り値: なし。

    • // ゲストユーザーの場合、setAccount をスキップして初期化に進むことができます。ユーザーがログオンした後、setAccount を呼び出して再初期化します。
      [[AliTigerTally sharedInstance] setAccount:@"testAccount"];
  2. SDK を初期化して、最初のデータ収集を実行します。

    各初期化では、デバイス情報が 1 回収集されます。init 関数を再度呼び出して、さまざまなビジネスシナリオに合わせてデータ収集を再初期化できます。

    初期化は、完全、カスタムプライバシー、非プライバシーの 3 つのデータ収集モードをサポートしています。非プライバシーモードでは、IDFA や IDFV など、エンドユーザーのプライバシーに関わるフィールドは収集されません。

    説明

    内部要件に準拠するには、データ整合性を確保する適切なデータ収集モードを選択してください。完全なデータは、潜在的な脅威をより効果的に検出するのに役立ちます。

    // 初期化コールバック。コールバックはインターフェイス呼び出しのステータスコードを返します。
    typedef void (^TTInitListener)(int);
    
    /**
     * SDK を初期化します。
     *
     * @param appkey        キー。
     * @param options       オプションのパラメーター。
     * @param onInitFinish  初期化完了時のコールバック。
     * @return 初期化が成功したかどうかを示します。
     */
    - (int)init:(NSString *)appkey collectType:(TTCollectType)type options:(NSMutableDictionary *_Nullable)options listener:(TTInitListener _Nullable)onInitFinish;
    • パラメーター

      • appkey: SDK 認証キー。型: NSString。

      • collectType: データ収集モード。型: TTCollectType。有効な値:

        フィールド

        説明

        TT_DEFAULT

        完全なデータを収集します。

        TT_DEFAULT

        TT_NO_BASIC_DATA

        基本的なデバイスデータを収集しません。

        これには、デバイス名、システムバージョン番号、画面の解像度が含まれます。

        TT_NO_X | TT_NO_Y

        (X も Y も収集されないことを示します。X と Y は、特定の収集項目のフィールドタイプ名を表します。)

        TT_NO_UNIQUE_DATA

        一意の識別子データを収集しません。

        これには IDFV と IDFA が含まれます。

        TT_NO_EXTRA_DATA

        拡張デバイスデータを収集しません。

        これには、接続されている Wi-Fi 情報 (SSID、BSSID) と近くの Wi-Fi ネットワークのリストが含まれます。

        TT_NOT_GRANTED

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

        TT_NOT_GRANTED

      • options: データ収集のオプションパラメーター。デフォルト値は nil です。型: NSMutableDictionary。次のパラメーターが使用可能です。

        フィールド

        説明

        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 は、米国 (西部) サイト (https://cloudauth-device.us-west-1.aliyuncs.com) など、特定のサイトにデータを報告する必要がある場合にのみ設定できます。

      • listener: SDK 初期化コールバックインターフェイス。このコールバックを使用して、初期化のステータスを判断できます。デフォルト値は nil です。型: TTInitListener。

        TTCode

        コード

        説明

        TT_SUCCESS

        0

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

        TT_NOT_INIT

        -1

        SDK の初期化が呼び出されていません。

        TT_NOT_PERMISSION

        -2

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

        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 は成功を示します。負の数は失敗を示します。型: int。

    • 例:

      // appkey は Alibaba Cloud プラットフォームで割り当てられた認証キーです。
      NSString *appKey = @"xxxxxxxxxxxxxxxxxxxxx";
      // オプションのパラメーター。IPv6 と国際レポートを設定できます。
      NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
      [options setValue:@"0" forKey:@"IPv6"];     // IPv4 用に設定します。
      [options setValue:@"0" forKey:@"Intl"];     // 中国本土からの報告用に設定します。
      // [options setValue:@"1" forKey:@"Intl"];  // 中国本土以外からの報告用に設定します。
      // 米国 (西部) サイトに報告します。
      // [options setValue:@"https://cloudauth-device.us-west-1.aliyuncs.com" forKey:@"CustomUrl"];
      // [options setValue:@"cloudauth-device.us-west-1.aliyuncs.com" forKey:@"CustomHost"];
      
      // 1 回の初期化呼び出しは、1 回のデバイス情報収集を表します。init 関数を再度呼び出して、さまざまなビジネスシナリオに合わせて収集を再初期化できます。
      // 完全なデータ収集。
      if (0 == [[AliTigerTally sharedInstance] init:appkey collectType:TT_DEFAULT options:options listener:nil]) {
          NSLog(@"初期化に成功しました");
      } else {
          NSLog(@"初期化に失敗しました");
      }
      
      // 収集するプライバシーデータを指定します。「|」を使用して、異なるプライバシーデータ型を連結できます。
      TTCollectType collectPrivacy = TT_NO_BASIC_DATA | TT_NO_EXTRA_DATA;
      int ret = [[AliTigerTally sharedInstance] init:appkey collectType:collectPrivacy options:options listener:nil];
      
      // プライバシーフィールドを収集しません。
      int ret = [[AliTigerTally sharedInstance] init:appkey collectType:TT_NOT_GRANTED options:options listener:nil];
  3. データハッシュ。

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

    // リクエストタイプ:
    typedef NS_ENUM(NSInteger, TTRequestType) {
        TT_GET=0, TT_POST, TT_PUT, TT_PATCH, TT_DELETE
    };
    
    /**
     * カスタム署名データをハッシュ化します。
     * @param type  データの型。
     * @param input 署名するデータ。
     * @return whash 文字列。
     */
    - (NSString *)vmpHash:(TTRequestType)type input:(NSData *)input;
    • パラメーター:

      • type: データの型。型: TTTypeRequest。有効な値:

        • GET: GET リクエストデータ。

        • POST: POST リクエストデータ。

        • PUT: PUT リクエストデータ。

        • PATCH: PATCH リクエストデータ。

        • DELETE: DELETE リクエストデータ。

      • input: 署名するデータ。type に基づいて本文または URL を渡します。型: NSData。

    • 戻り値: whash 文字列。型: NSString。

    • 例:

      // GET リクエスト
      NSString *url = @"https://tigertally.aliyun.com/apptest";
      NSString *whash = [[AliTigerTally sharedInstance] vmpHash:TT_GET input:[url dataUsingEncoding:NSUTF8StringEncoding]];
      NSLog(@"whash: %@", whash);
      
      // POST リクエスト
      NSString *body = @"hello world";
      NSString *whash = [[AliTigerTally sharedInstance] vmpHash:TT_POST input:[body dataUsingEncoding:NSUTF8StringEncoding]];
      NSLog(@"whash: %@", whash);
    説明

    コンソールでデフォルトの署名を設定し、カスタム署名オプションが選択されていない場合は、このインターフェイスを呼び出す必要はありません。カスタム署名オプションを選択した場合は、データ署名の前にハッシュ検証のためにこのインターフェイスを呼び出す必要があります。

  4. データ署名。

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

    /**
     * データに署名します。
     * @param input 署名するデータ。
     * @return wtoken 文字列。
     */
    - (NSString *)vmpSign:(NSData *)input;
    • パラメーター:

      • input: 署名するデータ。これは通常、リクエスト本文全体またはカスタム署名からの whash です。型: NSData。

    • 戻り値: wtoken 文字列。型: NSString。

    • 例:

      // コンソールで設定されたデフォルト署名 (カスタム署名は選択されていません)
      NSString *body = @"hello world";
      NSString *wtoken = [[AliTigerTally sharedInstance] vmpSign:[body dataUsingEncoding:NSUTF8StringEncoding]];
      NSLog(@"wtoken: %@", wtoken);
      
      // コンソールで設定されたカスタム署名
      // POST リクエストのカスタム署名
      NSString *whash = [[AliTigerTally sharedInstance] vmpHash:TT_POST input:[body dataUsingEncoding:NSUTF8StringEncoding]];
      NSString *wtoken = [[AliTigerTally sharedInstance] vmpSign:[whash dataUsingEncoding:NSUTF8StringEncoding]];
      NSLog(@"whash: %@, wtoken: %@", whash, wtoken);
      
      // GET リクエストのカスタム署名
      NSString *url = @"https://tigertally.aliyun.com/apptest";
      NSString *whash = [[AliTigerTally sharedInstance] vmpHash:TT_GET input:[url dataUsingEncoding:NSUTF8StringEncoding]];
      NSString *wtoken = [[AliTigerTally sharedInstance] vmpSign:[whash dataUsingEncoding:NSUTF8StringEncoding]];
      NSLog(@"whash: %@, wtoken: %@", whash, wtoken);
      説明
      • カスタム署名のために vmpHash を呼び出す場合、vmpSign インターフェイスの input パラメーターは生成された whash 文字列です。アプリ保護のためのシナリオベースのポリシーを設定する場合、[カスタム署名フィールド] を ali_sign_whash に設定する必要があります。

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

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

      • vmpSign を呼び出すときに、POST または GET リクエストの本文など、リクエスト本文が空の場合は、null オブジェクト (nil) または空の文字列の NSData 値を渡します。たとえば、[@"\"\" dataUsingEncoding:NSUTF8StringEncoding] です。

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

        • you must call init first: init 関数が呼び出されませんでした。

        • you must input correct data: 入力データが正しくありません。

        • you must input correct type: 入力タイプが正しくありません。

3. 二次検証

  1. 結果を判断します。

    応答cookie フィールドと body フィールドに基づいて、二次検証を実行するかどうかを判断できます。ヘッダーには複数の Set-Cookie フィールドが含まれている場合があります。このインターフェイスを呼び出す前に、それらを cookie フォーマットにマージする必要があります。

    /**
     * 二次検証を実行するかどうかを確認します。
     *
     * @param cookie  応答クッキー。
     * @param body    応答本文。
     * @return 0: パス、1: 二次検証が必要。
     */
    - (int)cptCheck:(NSString *)cookie body:(NSData *)body;
    • パラメーター:

      • cookie: リクエスト応答内のすべてのクッキー。型: NSString。

      • body: リクエスト応答本文全体。型: NSData。

    • 戻り値: 決定結果。0 はパスを示し、1 は二次検証が必要であることを示します。型: int。

      • 例:

      NSString *cookie = @"key1=value1;key2=value2;";
      NSData *body = xxx;
      int recheck = [[AliTigerTally sharedInstance] cptCheck:cookie body:body];
      NSLog(@"recheck: %d", recheck);
  2. スライダーを作成します。

    cptCheck によって返される結果に基づいて、スライダーオブジェクトを作成するかどうかを決定できます。TTCaptcha オブジェクトは、スライダーウィンドウを表示および非表示にするための show メソッドと dismiss メソッドを提供します。TTOption は、スライダーの設定可能なパラメーターをカプセル化します。TTListener には、2 つのスライダーステータスのコールバックが含まれています。スライダーウィンドウをカスタマイズするには、カスタムページのアドレスを渡す必要があります。ローカルの HTML ファイルまたはリモートページを使用できます。

    /**
     * 検証用のスライダーを表示します。
     *
     * @param view      親コンポーネント。
     * @param option    パラメーター。
     * @param detegate  コールバックプロトコル。
     */
    - (TTCaptcha *)cptCreate:(UIView *)view option:(TTOption *)option delegate:(id<TTDelegate>)detegate;
    
    
    @protocol TTDelegate <NSObject>
    @required
    // スライダー検証が成功しました。
    - (void)success:(TTCaptcha *)captcha data:(NSString *)data;
    
    // スライダー検証に失敗しました。
    - (void)failed:(TTCaptcha *)captcha code:(NSString *)code;
    @end
    
    
    @interface TTOption : NSObject
    // クリックしてキャンセル。
    @property (nonatomic, assign) BOOL cancelable;
    
    // カスタムページ。
    @property (nonatomic, strong) NSString *customUri;
    
    // 言語。
    @property (nonatomic, strong) NSString *language;
    @end
    
    
    @interface TTCaptcha : NSObject
    
    - (instancetype)init:(UIView *)view option:(TTOption *)option delegate:(id<TTDelegate>)delegate;
    
    // データ統計用のスライダー traceId を取得します。
    - (NSString *)getTraceId;
    
    // スライダーを表示します。
    - (void)show;
    
    // スライダーをキャンセルします。
    - (void)dismiss;
    
    @end
    • パラメーター:

      • view: 現在のページビュー。型: View。

      • option: スライダー構成パラメーター。型: TTOption。

      • listener: スライダーステータスコールバック。型: TTDelegate。

    • 戻り値: スライダーオブジェクト。型: TTCaptcha。

    • 例:

      #pragma mark - TTDelegate
      - (void)failed:(TTCaptcha *)captcha code:(nonnull NSString *)code {
          NSLog(@"captcha failed: %@", code);
      }
      
      - (void)success:(TTCaptcha *)captcha data:(nonnull NSString *)data {
          NSLog(@"captcha success: %@", data);
      }
      
      TTOption *option = [[TTOption alloc] init];
      // option.customUri = @"ali-tt-captcha-demo-ios";
      option.language   = @"cn";
      option.cancelable = true;
      
      TTCaptcha *captcha = [[AliTigerTally sharedInstance] cptCreate:[self view] option:option delegate:self];
      [captcha show];
      説明

      「検証に失敗しました」というエラーは、ユーザーがスライダーを移動中または移動後に例外が検出されたことを示します。

      次の表にエラーコードを示します。

      • 1001: 検証に失敗しました。

      • 1002: システム例外。

      • 1003: パラメーターエラー。

      • 1005: 検証がキャンセルされました。

      • 8001: スライダーの呼び出しに失敗しました。

      • 8002: スライダー検証データが異常です。

      • 8003: スライダー検証中の内部例外。

      • 8004: ネットワークエラー。

ベストプラクティスの例

#import "DemoController.h"
#if __has_include(<AliTigerTally_NOIDFA/AliTigerTally_NOIDFA.h>)
    #import <AliTigerTally_NOIDFA/AliTigerTally_NOIDFA.h>
#else
    #import <AliTigerTally_IDFA/AliTigerTally_IDFA.h>
#endif

@interface DemoController () <TTDelegate>

@end

static NSString *kAppHost = @"******";
static NSString *kAppUrl  = @"******";
static NSString *kAppkey  = @"******";


@implementation DemoController

- (void)viewDidLoad {
    [super viewDidLoad];
    // ビューの読み込み後に追加のセットアップを行います。


    [self doTest];
}

- (void)doTest {

    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        // 初期化
        NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
        // [options setValue:@"1" forKey:@"Intl"];     // 国際レポート用に設定します。
        int code = [[AliTigerTally sharedInstance] init:kAppkey collectType:TT_DEFAULT options:options listener:nil];
        NSLog(@"tigertally init: %d", code);

        // すぐに同期的に呼び出さないでください。
        [NSThread sleepForTimeInterval:2.0];

        // 署名
        NSString *body = @"hello world";
        NSString *whash = nil, *wtoken = nil;
        // カスタム署名
        whash  = [[AliTigerTally sharedInstance] vmpHash:TT_Post input:[body dataUsingEncoding:NSUTF8StringEncoding]];
        wtoken = [[AliTigerTally sharedInstance] vmpSign:[whash dataUsingEncoding:NSUTF8StringEncoding]];
        NSLog(@"tigertally vmp: %@, %@", whash, wtoken);

        // デフォルトの通常署名
        // wtoken = [[AliTigerTally sharedInstance] vmpSign:[body dataUsingEncoding:NSUTF8StringEncoding]];
        // NSLog(@"tigertally vmp: %@", wtoken);

        [self doPost:kAppUrl host:kAppHost whash:whash wtoken:wtoken body:[body dataUsingEncoding:NSUTF8StringEncoding] callback:^(NSInteger code, NSString * _Nonnull cookie, NSData * _Nonnull body) {
            int check = [[AliTigerTally sharedInstance] cptCheck:cookie body:body];
            NSLog(@"captcha result:%d", check);

            if (check == 0) return;
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                [self doShow];
            }];
        }];
    }];
    [thread start];
}

- (void)doShow {
    NSLog(@"captcha show");
    
    TTOption *option = [[TTOption alloc] init];
//    option.customUri = @"ali-tt-captcha-demo-ios";
    option.language   = @"cn";
    option.cancelable = true;
    
    TTCaptcha *captcha = [[AliTigerTally sharedInstance] cptCreate:[self view] option:option delegate:self];
    [captcha show];
}

- (void)doPost:(NSString *)url host:(NSString *)host whash:(NSString *)whash wtoken:(NSString *)wtoken body:(NSData *)body callback:(void(^)(NSInteger code, NSString* cookie, NSData *body))callback {
    NSLog(@"start reqeust post");
    
    NSURL* requestUrl = [NSURL URLWithString: url];
    NSData* requestBody = body;
    
    NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:requestUrl cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
    [request setValue: @"text/x-markdown" forHTTPHeaderField: @"Content-Type"];
    [request setValue: host forHTTPHeaderField: @"HOST"];
    [request setValue: wtoken forHTTPHeaderField:@"wToken"];
    if (whash) {
        [request setValue:whash forHTTPHeaderField:@"ali_sign_whash"];
    }
    request.HTTPMethod = @"post";
    request.HTTPBody = requestBody;
    
    NSURLSessionDataTask* dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"tiger tally 署名に失敗しました: %@", [error description]);
            callback(-1, nil, [[error description] dataUsingEncoding:NSUTF8StringEncoding]);
            return;
        }
        
        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*) response;
        
        NSInteger code = httpResponse.statusCode;
        NSString* body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSMutableString* cookies = [[NSMutableString alloc] init];
        for (NSHTTPCookie* cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
            if ([url containsString:[cookie domain]]) {
                NSLog(@"domain:%@, path: %@, name:%@, value: %@", [cookie domain], [cookie path], [cookie name], [cookie value]);
                [cookies appendFormat: @"%@=%@;", [cookie name], [cookie value]];
            }
        }
        NSLog(@"応答コード: %ld", code);
        NSLog(@"応答クッキー: %@", cookies);
        NSLog(@"応答本文: %@", body ? (body.length > 100 ? [body substringToIndex:100]:body) : @"");
        
        callback(code, cookies, data);
    }];
    [dataTask resume];
}

#pragma mark - TTDelegate
- (void)failed:(TTCaptcha *)captcha code:(nonnull NSString *)code {
    NSLog(@"captcha に失敗しました: %@", code);

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:code preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:alert animated:true completion:nil];
}

- (void)success:(TTCaptcha *)captcha data:(nonnull NSString *)data {
    NSLog(@"captcha に成功しました: %@", data);

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:data preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:alert animated:true completion:nil];
}

@end