Django/DRFでHMAC署名を使ってサーバ間リクエストの整合性を確保する

サーバとサーバが通信する際に、 「本当に自分が送ったリクエストか」「途中で改ざんされていないか」 をどう確認するのでしょうか? この記事では、Django / DRF 環境で HMAC署名ベースの認証 を用いて、サーバ間リクエストの整合性と信頼性を確保する方法をまとめます。

特に、

  • この手法の目的は何か
  • どんな事故を防げるか
  • なぜ クライアント-サーバ通信には適さないか(限界点)
  • 実際の Django/DRF プロジェクトで hmac_mixin.py + CBV でどう構成できるか

をコード例とともに解説します。


HMAC署名認証とは?



HMAC (Hash-based Message Authentication Code)

「共有された秘密鍵 + メッセージ」を使って 署名値(signature) を生成する方式

です。

  • サーバAとサーバBが同じ secret key を知っていると仮定します。
  • AサーバがBサーバへ POST リクエストを送るとき:

  • リクエスト 本文(body)タイムスタンプ(timestamp) などを結合して HMAC で署名値を作り、

  • この署名値を HTTP Header などに添えて送ります。
  • Bサーバは同じ secret key で同じように署名を計算し、

  • 値が同じなら = 「Aが送った正規のリクエスト」

  • 値が違えば = 「途中で改ざんされた、または誰かが偽造したリクエスト」

と判断できます。

つまり、HMAC署名は

  • 整合性(integrity):内容が途中で変更されていないか
  • 認証(authentication):"秘密鍵を知っている側が送ったか"

を同時に確認する手法です。

※ 前提:サーバ間でのみ秘密鍵を安全に共有できるという仮定が必要です。


この手法の目的と防げる事故

1. リクエスト本文の改ざん防止

中間で攻撃者がトラフィックを傍受したと仮定します。

  • リクエスト本文を少し変えて別のアカウントへ送金を試みる
  • パラメータを変えてより大きな金額や別のオプションに操作しようとする

しかし受信サーバは "リクエスト本文 + タイムスタンプ" を持って 再度 HMAC署名を計算 します。

  • 本文が 1文字でも 変われば署名値も完全に変わるので
  • 署名が合わなければリクエストを拒否できます。

2. サーバ偽装(スプーフィング)防止

攻撃者が "自分もそのサーバだと偽装してリクエストを送る" としても、

  • 秘密鍵を知らなければ 正しい署名値を計算できません
  • 受信サーバは "署名検証失敗 → 本当のサーバではない" と判断します。

つまり、サーバ同士は "秘密鍵を知っている人だけが本物" という信頼を前提に通信できます。

3. リプレイ攻撃防止(タイムスタンプ/nonceと併用)

攻撃者が 以前有効だったリクエストをそのままコピーして再送 するリスクもあります。

そこで HMAC署名には通常、

  • タイムスタンプ (X-HMAC-Timestamp などのヘッダー)
  • nonce(使い捨てのランダム値)

を同時に含め、受信サーバで

  • "リクエスト時刻が古すぎる場合は拒否"
  • "一度処理した nonce は再度受け取らない"

といったポリシーを適用して リプレイ攻撃 も抑制します。

※ ただし、HMACだけで全てを解決できるわけではなく、必ず HTTPS(TLS) 上で使用 することが前提です。HMACは 追加の整合性/認証層 です。


HMAC署名の限界:なぜクライアント(アプリ、ウェブフロント)には使えないのか



HMAC署名を使ってはいけないケースがあります。

「クライアントとの通信では使えない。リバースエンジニアリングでキーが漏れる」

1. クライアントコードには秘密鍵を隠せない

  • モバイルアプリ、SPAフロントエンド(JS)、デスクトップアプリなど クライアントコード には、結局ビルドされたバイナリ/JavaScript にキーを入れなければなりません。
  • 攻撃者がアプリを逆コンパイルしたり、ブラウザで JS を覗き見ると secret key を抽出 できます。

一度キーが漏れると:

  • 攻撃者はそのキーで 任意の HMAC署名 を作成でき、
  • サーバ側では "このリクエストが本当のクライアントから来たか、攻撃者が作ったか" を区別できません。

つまり、HMACの前提("キーは二者だけが知っている")が崩れます

したがって HMAC署名は:

  • サーバ ↔ サーバ(秘密鍵を安全に管理できる環境)
  • バックエンド ↔ バックエンドマイクロサービス

などの信頼できる環境で使うのが正しいです。

2. 暗号化ではない(本文はそのまま)

HMACは 「署名」 です。 本文を暗号化して隠すわけではなく、

  • "本文がこのままであるか"
  • "誰が変更していないか"

を確認するためのものです。

したがって:

  • 機密データは依然として TLS(HTTPS) で保護 する必要があります。
  • HMAC が本文を隠すわけではないことを理解してください。

いつこの HMACベースのサーバ間認証を使うべきか?(シナリオ)

以下のような状況で HMAC署名は有効です。

1. Django アプリケーション → 別の DRF 認証サーバ

  • 複数サービスが共通で使う 認証/会員サーバ が DRF で別に設置されている構成
  • メインの Django アプリは "ログイン検証"、"トークン発行検証" などで DRF サーバへ POST リクエスト

このとき:

  • Django アプリが DRF サーバへ HMAC署名付きリクエスト を送り、
  • DRF サーバは "これは信頼できる Django アプリからのリクエスト" と検証して結果を返します。

2. Django アプリケーション → AI 推論サーバ(DRF または FastAPI 等)

  • Django がフロント/バックエンドを担い、
  • 重い AI 推論は 別サーバ(DRF、FastAPI、Flask 等)に任せる構成

Django が AI サーバへ:

  • POST /v1/infer を送り、
  • 入力テキスト、画像URL、オプションなどを送信し、
  • このリクエストを HMAC で署名

AI サーバは:

  • 受信したリクエストの HMAC署名を検証し、
  • 有効なら GPU リソースを使って推論実行

こうすることで:

  • 内部ネットワークでも "サービス間の偽造リクエスト" を減らせ、
  • 内部トラフィックに追加の信頼層を設けられます。

Django/DRFで使う実践パターン:hmac_mixin.py + CBV

実際のプロジェクトでは次の構成がとてもきれいです。

  1. プロジェクト(またはアプリ)直下に hmac_mixin.py を置く
  2. 共通 HMAC署名 + POSTリクエストロジックを Mixin に抽出
  3. POST が必要な CBV で この Mixin を継承 して使用

このパターンを例で解説します。

1. hmac_mixin.py – 共通署名 + POSTメソッド

# hmac_mixin.py
import hmac
import hashlib
import json
import time

import requests
from django.conf import settings


class HMACRequestMixin:
    # settings.py に定義しておく想定
    # 例: HMAC_SECRET_KEY = "super-secret-key-from-env"
    HMAC_SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")

    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    HMAC_ALGORITHM = hashlib.sha256

    def build_hmac_signature(self, body: bytes, timestamp: str) -> str:
        """
        body と timestamp を使って HMAC 署名値を作る。
        """
        message = timestamp.encode("utf-8") + b"." + body
        digest = hmac.new(self.HMAC_SECRET_KEY, message, self.HMAC_ALGORITHM).hexdigest()
        return digest

    def post_with_hmac(self, url: str, payload: dict, timeout: int = 5):
        """
        指定した URL に HMAC 署名付き POST リクエストを送る。
        """
        # JSON シリアライズ時は sort_keys + separators で両側フォーマットを揃えると安全。
        body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
        timestamp = str(int(time.time()))
        signature = self.build_hmac_signature(body, timestamp)

        headers = {
            "Content-Type": "application/json",
            self.HMAC_HEADER_NAME: signature,
            self.HMAC_TIMESTAMP_HEADER: timestamp,
        }

        response = requests.post(url, data=body, headers=headers, timeout=timeout)
        response.raise_for_status()
        return response

これでどの CBV でも HMACRequestMixin だけ継承 すれば簡単に HMAC 署名付きリクエストを送れます。

2. 送信側 Django CBV の例

例:Django が AI 推論サーバ(DRF)へリクエストを送るビュー

# views.py
from django.conf import settings
from django.http import JsonResponse
from django.views import View

from .hmac_mixin import HMACRequestMixin


class AIInferenceRequestView(HMACRequestMixin, View):
    """
    クライアントからリクエストを受け取り、内部 AI サーバへ HMAC 署名付き POST を送るビュー
    """

    def post(self, request, *args, **kwargs):
        # クライアントリクエストを基に payload を構築
        data = json.loads(request.body.decode("utf-8"))
        payload = {
            "text": data.get("text", ""),
            "user_id": request.user.id if request.user.is_authenticated else None,
        }

        # AI サーバ URL(settings で管理)
        url = settings.AI_SERVER_URL  # 例: "https://ai-service.internal/v1/infer"

        # Mixin が提供する post_with_hmac を使用
        try:
            ai_response = self.post_with_hmac(url, payload)
        except requests.RequestException as e:
            # 必要に応じてエラーハンドリング
            return JsonResponse({"detail": "AI server error", "error": str(e)}, status=502)

        # AI サーバの JSON 応答をそのままクライアントへ返す
        return JsonResponse(ai_response.json(), status=ai_response.status_code)

これで、

  • 新しいサーバ間呼び出しが必要になったら
  • そのビューに HMACRequestMixin を継承し
  • self.post_with_hmac(url, payload) だけ呼び出せば OK

Django/DRF を使っているならこの方法を強く推奨します。ビューコードが非常に簡潔になり、再利用性が高まります


DRF 受信側:HMAC署名検証 Authentication クラスの例

次に、リクエストを受ける側(DRF サーバ)では HMAC署名を検証する必要があります。 DRF では カスタム Authentication クラス を作って適用するパターンが便利です。

# authentication.py
import hmac
import hashlib
import time

from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed


class HMACSignatureAuthentication(BaseAuthentication):
    SECRET_KEY = settings.HMAC_SECRET_KEY.encode("utf-8")
    HMAC_HEADER_NAME = "X-HMAC-Signature"
    HMAC_TIMESTAMP_HEADER = "X-HMAC-Timestamp"
    ALGORITHM = hashlib.sha256
    MAX_SKEW_SECONDS = 60  # 許容時間ズレ(例: ±60秒)

    def _build_signature(self, body: bytes, timestamp: str) -> str:
        message = timestamp.encode("utf-8") + b"." + body
        return hmac.new(self.SECRET_KEY, message, self.ALGORITHM).hexdigest()

    def authenticate(self, request):
        signature = request.headers.get(self.HMAC_HEADER_NAME)
        timestamp = request.headers.get(self.HMAC_TIMESTAMP_HEADER)

        if not signature or not timestamp:
            raise AuthenticationFailed("Missing HMAC headers")

        # タイムスタンプ検証(リプレイ防止)
        try:
            ts = int(timestamp)
        except ValueError:
            raise AuthenticationFailed("Invalid timestamp")

        now = int(time.time())
        if abs(now - ts) > self.MAX_SKEW_SECONDS:
            raise AuthenticationFailed("Request timestamp too old")

        expected_signature = self._build_signature(request.body, timestamp)

        if not hmac.compare_digest(signature, expected_signature):
            raise AuthenticationFailed("Invalid HMAC signature")

        # サーバ間通信なら別の 'service アカウント' 概念を返すか、
        # (user, auth) タプルの代わりに (None, None) を返しても OK。
        return (None, None)

DRF ビューへの適用

# views.py (DRF サーバ側)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny

from .authentication import HMACSignatureAuthentication


class AIInferenceView(APIView):
    authentication_classes = [HMACSignatureAuthentication]
    permission_classes = [AllowAny]  # HMAC で保護していると仮定

    def post(self, request, *args, **kwargs):
        # ここまで来たら HMAC 検証に成功
        input_text = request.data.get("text", "")

        # ここで実際の AI 推論を実行
        result = {"answer": f"AI result for: {input_text}"}

        return Response(result)

こう構成すると:

  • 送信側 DjangoHMACRequestMixin で簡単に署名付きリクエスト送信
  • 受信側 DRFHMACSignatureAuthentication で署名検証

という明確な構造が完成します。


まとめ

  • HMAC署名は サーバ間で共有された秘密鍵 を使い、 リクエストの 整合性(改ざん防止)認証(誰が送ったか) を検証する手法です。
  • この手法は
  • リクエスト本文の改ざん
  • サーバ偽装(スプーフィング)
  • リプレイ攻撃(タイムスタンプ/nonce と併用) などを減らすのに効果的です。
  • ただし、
  • クライアントコード(アプリ、JS)には秘密鍵を隠せない ため、 クライアント-サーバ認証には不適切です。
  • 実際の Django/DRF プロジェクトでは
  • hmac_mixin.py共通 HMAC署名 + POSTリクエストメソッド を実装し、
  • CBV でこのミックスインを継承して簡単に post_with_hmac を呼び出せます。
  • 受信側 DRF は カスタム Authentication クラス で HMAC署名を検証し、 サーバ間通信を安全に保護できます。

image