TiktokAPIの認証情報を取得する手法

## 🎯 目的

TikTokのAPI：

> https://ads.tiktok.com/creative_radar_api/v1/popular_trend/list
> 

から**トレンド動画リスト**を取得したい。

![postman.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4055742/eb0829cb-cd52-4e63-b508-4942b225e2c9.png)

しかし、このAPIを直接呼び出すには以下のヘッダーが必要です：

- `anonymous-user-id`
- `user-sign`
- `timestamp`

これらはブラウザ内で動的に生成されるため、**通常のHTTPクライアントでは再現できません**。

そこで、本コードでは**仮想ブラウザ環境（jsdom）を使ってヘッダーを抽出する**というアプローチをとっています。

## 🔎 なぜ `anonymous-user-id` や `user-sign` が必要なのか？

TikTokは非公開APIへのアクセスに対して、リクエストの正当性を確認するための署名ヘッダーを要求します。

- `anonymous-user-id` は匿名ユーザーの識別用
- `user-sign` はユーザーIDやタイムスタンプを元に生成される署名トークン

これらがないと、APIはエラーや空データを返します。

## ❗ 課題

- 必要なヘッダーはブラウザ上でJavaScriptにより生成される
- 単純なHTTPリクエストではヘッダーを再現できず、APIアクセスが拒否される

## ✅ 解決方法

本コードは、以下のステップでヘッダーを取得します：

- `got-scraping` でページHTMLを取得
- `jsdom` で仮想DOMを生成し、JavaScriptを実行
- `XMLHttpRequest` の `setRequestHeader` をフックしてヘッダーを傍受
- 全ヘッダーが揃うまで待機して値を取得

## 🧠 コードの概要（補足）

```javascript
// TikTok API用の認証ヘッダーを生成する
import { JSDOM, VirtualConsole } from 'jsdom'; // Layer jsdomから
import { gotScraping } from 'got-scraping'; // Layer got-scrapingから

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getApiUrlWithVerificationToken(body, url) {
  // JSDOMを使用してAPIセッションヘッダーを取得
  console.log('JSDOMでAPIセッションヘッダーを取得開始...');
  console.log('ページURL: ', url);
  console.log('ページ内容の長さ: ', body.length);

  const virtualConsole = new VirtualConsole();
  const { window } = new JSDOM(body, {
    url,
    contentType: 'text/html',
    runScripts: 'dangerously',
    resources: 'usable',
    pretendToBeVisual: false,
    virtualConsole,
  });

  virtualConsole.on('error', (err) => {
    // JSDOMエラーをログ
    console.error('JSDOMエラー: ', err);
  });

  const apiHeaderKeys = ['anonymous-user-id', 'timestamp', 'user-sign'];
  const apiValues = {};
  let retries = 10;
  let headersFound = false;

  window.XMLHttpRequest.prototype.setRequestHeader = (name, value) => {
    if (apiHeaderKeys.includes(name)) {
      apiValues[name] = value;
      console.log(`取得したヘッダー: ${name}=${value}`);
      if (Object.keys(apiValues).length === apiHeaderKeys.length) {
        headersFound = true;
        console.log('すべてのヘッダーを取得: ', apiValues);
      }
    }
  };

  window.XMLHttpRequest.prototype.open = (method, urlToOpen) => {
    console.log(`XHRリクエスト開始: ${method} ${urlToOpen}`);
  };

  // ヘッダー取得ロジック
  console.log('ヘッダー取得ループ開始...');
  while (retries > 0 && !headersFound) {
    console.log(`試行残り: ${retries}, 取得済みヘッダー: `, Object.keys(apiValues));
    await sleep(1000);
    retries--;
  }

  console.log('ヘッダー取得ループ終了、headersFound: ', headersFound);
  window.close();

  if (!headersFound) {
    console.error('最終的に取得できたヘッダー: ', apiValues);
    throw new Error('トークン生成に失敗: 必要なヘッダーをすべて抽出できませんでした。');
  }

  console.log('API認証ヘッダーの取得に成功: ', apiValues);
  return apiValues;
}

export async function getHeaders() {
  // 初期ページをフェッチしてヘッダーを生成
  console.log('ヘッダー生成のために初期ページをフェッチ中...');
  const url = 'https://ads.tiktok.com/business/creativecenter/inspiration/popular/hashtag/pad/en';
  try {
    console.log('gotScrapingリクエスト開始: ', url);
    const response = await gotScraping({
      url,
      timeout: { request: 30000 },
    });
    console.log('ページフェッチ成功、ステータスコード: ', response.statusCode);
    return await getApiUrlWithVerificationToken(response.body.toString(), url);
  } catch (error) {
    console.error(`${url} からページフェッチ失敗: `, error.message);
    throw error;
  }
}
```

- `getHeaders()`
    - 初期ページのHTMLを取得し、`getApiUrlWithVerificationToken()` に渡す
- `getApiUrlWithVerificationToken(body, url)`
    - ヘッダーを検出するため、仮想DOM内でスクリプトを実行
    - `XMLHttpRequest` を上書きし、必要なヘッダー値を取得

## 💡 工夫したこと

- jsdomの virtualConsole を使って、仮想ブラウザ内のエラーもログに出力
- ヘッダーが揃うまで最大10回リトライし、安定して値を取得できるように設計
- 本来セキュリティ対策として隠されている内部通信を、副作用（XHR）を利用して自然に取得

## 📌 まとめ

TikTokの内部APIを扱う際に必要な認証ヘッダーを、仮想ブラウザで抽出する手法です。

- 公開されていないAPIにアクセスしたい
- JavaScript内で生成される認証ヘッダーが必要
- 通常のクローリング手法では不十分

といった場面で、**このような仮想実行＋傍受**のアプローチが有効です。