1. 「ログインしたのに、なぜ私を認識できないのか?」(問題の発端)

OAuth2、JWT、セッション認証… 認証方式は数多くあり、ほとんどの場合はこれで十分です。私もそう思っていました。

  • 自前のメールクライアントや ChatGPT の MyGPT に OAuth2 を組み込んだときは「これが本当のユーザー体験だ」と感動し、
  • 単体の Django サーバだけで完結する Web アプリは セッション認証 が最適、
  • フロントエンドとバックエンドが分離した構成では JWT が最もスマートでした。

しかし、この組み合わせが一瞬で崩れた瞬間がありました。

それは 非同期タスク(Celery) です。ユーザーがボタンを押すと、バックエンドは遠くにある AI 計算サーバやワーカーに仕事を委託します。するとワーカーはこう言います。

「すみません…リクエストは受け取ったのですが、誰の代理で実行すればいいんですか? request.user がありません」

ロボットワーカーが API キーを持って手紙を渡すイメージ

2. 「バックエンド ↔ バックエンド」+「非同期ワーカー(Celery)」の問題

API Key を導入した決定的なきっかけは、バックエンド同士が通信し、その間に Celery ワーカーが介在する 構成でした。

  1. ユーザーが Web リクエストを送信
  2. バックエンドが「ジョブ」をキューに投入
  3. Celery ワーカーがキューを消費し、別のバックエンド/計算サーバへ非同期リクエスト を送信

このとき最も痛かった点は次の通りです。

  • ワーカーには request.user が存在しない
  • セッションもない(ブラウザではないから)
  • JWT も扱いが難しい(トークンの発行・保管・伝搬が複雑)
  • OAuth2 は「ユーザーの操作」が前提なので、そもそも適用不可

結局、JWT とセッションが機能しなくなったときに残る問いは一つです。

「計算サーバ側でこのジョブを実行する主体(テナント/ユーザー)を、ワーカーはどうやって伝えるか?」

3. ワーカーの世界では『認証』より『識別』が先に必要だった

Web リクエストでは「認証=ログイン」「ログイン=ユーザー」が自然に結びつきます。 しかしワーカーは 人間ではなく、アプリケーションが自動的に CPU を借りて動く存在 です。したがって「認証」より先に「識別」が必要になります。認証はサーバ間で HMAC や secretKey を用いれば解決できる問題です。

  • このジョブは顧客 A のデータで実行されるべき
  • 結果は顧客 A のリソースに保存されるべき
  • 課金・権限・クォータは顧客 A 基準で消費されるべき

これを JWT やセッションで無理に詰め込もうとすると、トークンの発行・保管・有効期限・再発行設計が肥大化し、何よりも "ユーザーがブラウザを直接操作していないのに、バックエンドが JWT を発行する?」という根拠のない抵抗感が生まれます。実装は可能でも、絶対にやってはいけない感覚に至り、すぐに破棄しました。

4. 解決策:API Key がシンプルかつ強力だった

そこで採用したのが API Key です。一度で問題を整理してくれました。

  • ワーカーが内部リクエストを送るとき、ヘッダー一つで認証+識別を同時に処理
  • サーバはそのキーを見て どのユーザー/顧客のリクエストか即座にマッピング
  • キーの回収・交換(ローテーション)も非常に明快

例としてサーバが計算サーバへ非同期リクエストを送る形式は次の通りです。

POST /v1/ai/jobs
Authorization: Api-Key <KEY>
Content-Type: application/json

{ "job_id": "...", "payload": {...} }

ワーカーに request.user がなくても問題ありません。そのまま送れば、受信側バックエンドが API Key を用いてユーザーを特定できます。

5. 決定的な改善:API Key を USER と結びつけたことで運用が楽になった

特に満足した点はここです。

従来の rest_framework_api_key 系ライブラリは API Key 自体は提供しますが、私のケースでは 「キー ↔ ユーザー(顧客)の結合」 が鍵でした。 そこで AbstractAPIKey を継承し CustomAPIKey として拡張、AUTH_USER モデルと FK で紐付け しました。

結果は大満足でした。

from django.conf import settings
from rest_framework_api_key.models import AbstractAPIKey
from django.db import models

class CustomAPIKey(AbstractAPIKey):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="api_keys"
    )
    is_test = models.BooleanField(default=False)  # ステージ/テストキーの区別

このように結びつけることで、単なる「認証」以上に 運用機能 が次々と開放されます。

6. ユーザーごとのキー自動発行がもたらした運用上のメリット

会員登録時にユーザーにキーを自動付与する方式にした結果、次の点が格段に楽になりました。

1) 有効性管理が容易

  • 特定ユーザーのキーを取得/無効化/削除がシンプル
  • ユーザー退会時に関連キーが FK カスケードで自動整理

2) キーローテーションが簡単

  • 「キー漏洩疑惑」時に新キー発行 → 旧キー廃止のフローが明快
  • 複数キーを許容すれば 無停止交換 も可能(新キー配布後に旧キー停止)

3) 課金/クォータ/権限管理がユーザー単位に結びつく

  • キー単位ではなく ユーザー単位で課金/制限 を設定できる
  • 「この API Key は誰のリクエストか?」を毎回推測する必要がなくなる

4) 1 ユーザーに複数キーを発行可能

is_test フラグが活躍します。APIKey が OneToOne ではなく FK で結びつくため、1 ユーザーに用途別に複数キーを付与できます。 - 同一ユーザーが ステージ用キー本番用キー を同時に所有可能 - 開発/運用フローをアカウント単位で分離しやすい - ログ/モニタリングでも “テストトラフィック vs 本番トラフィック” を明確に分割

7. 認証方式は優劣ではなく「シチュエーション別の武器選択」

まとめると、私が今も感じている「状況別最適組み合わせ」は次の通りです。

  • OAuth2: 外部サービス/クライアント連携、ユーザー同意が重要なフローに強い
  • セッション認証: Django 単体の Web アプリで開発速度とシンプルさが最上
  • JWT: フロント/バックエンド分離、モバイル/SPA など多様なクライアントにバランス良く対応
  • API Key: バックエンド‑バックエンド、Automation/Worker/バッチのような “ユーザーリクエストでないリクエスト” に圧倒的に便利

特に Celery ワーカーが介在する瞬間、"ログインベースの認証" で世界を統一しようとするとかえって複雑度が上がります。そのとき API Key は本当にすっきりした脱出路でした。

8. 終わりに

人(ブラウザ/アプリ)はセッション/JWT/OAuth2 で扱うのが自然です。 しかしワーカーは人間ではなくプロセスであり、プロセスは "誰の仕事か" を 識別 できなければなりません。

私が API Key に切り替えたのは大きなセキュリティ議論のためではなく、その区間で最もシンプルに問題を解決できたからです。そして USER と結びつけたことで、キー管理はツールではなく 運用レバー となりました。

皆さんも API Key を頻繁に使いますか? 私が紹介した手法は API Key の便利さの一例です。この小さな経験が読者の皆様にとって有益なヒントになれば幸いです。


関連記事