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 簽名;

  • 若主體 哪怕一個字元 改變,簽名值也會完全不同,
  • 簽名不符即拒絕請求。

2. 防止伺服器偽裝(spoofing)

即使攻擊者「假裝自己是伺服器」發送請求,

  • 若不知道秘密金鑰,無法產生正確簽名
  • 接收伺服器會判斷「簽名驗證失敗 → 不是真伺服器」

因此,伺服器之間可以以「只有知道秘密金鑰的人才是真」為前提進行通訊。

3. 防止重放攻擊(replay attack)

攻擊者可能把先前有效的請求複製並重複傳送。

通常 HMAC 簽名會同時包含:

  • 時間戳(如 X-HMAC-Timestamp
  • nonce(一次性隨機值)

接收伺服器會檢查:

  • 「請求時間太久遠」則拒絕
  • 「已處理過的 nonce」再次收到則拒絕

這樣就能降低重放攻擊的風險。

※當然,僅靠 HMAC 不能解決所有問題,必須在 HTTPS(TLS)上使用,HMAC 只是額外的完整性/認證層。


HMAC 簽名的限制:為什麼不適用於客戶端(App、Web 前端)



有些情況下不應該使用 HMAC 簽名。

「在客戶端通訊中無法使用(因逆向工程會洩露金鑰)」

1. 客戶端程式碼無法隱藏秘密金鑰

  • 移動 App、SPA 前端(JS)、桌面 App 等客戶端程式碼,最終都會被編譯或載入瀏覽器。
  • 攻擊者可以反編譯或查看 JS,直接提取 secret key

一旦金鑰洩露:

  • 攻擊者能用該金鑰產生任意 HMAC 簽名
  • 伺服器無法區分「真實客戶端」與「偽造請求」

因此,HMAC 的前提「金鑰只被兩台伺服器知道」被破壞。

結論:

  • 伺服器 ↔ 伺服器(能安全管理金鑰的環境)
  • 後端 ↔ 後端微服務

才適合使用;

  • 移動 App、Web 前端等已部署的客戶端,應使用 JWT、OAuth2、Session 等常規 API 認證。

2. HMAC 不是加密(內容仍可見)

HMAC 只是一個「簽名」;

  • 它不會加密請求主體,內容仍可被 TLS 之外的任何人看到。

因此:

  • 敏感資料仍需透過 TLS(HTTPS)保護
  • HMAC 只是確認「內容未被篡改」與「發送者可信」的手段。

何時該使用 HMAC 基礎的伺服器間認證?(場景)

以下情境下,HMAC 簽名非常有用。

1. Django 應用 → 分離的 DRF 認證伺服器

  • 多個服務共用一個 認證/會員伺服器(DRF)
  • 主 Django 應用需要「登入驗證」或「發放 token」時,向 DRF 伺服器 POST

此時:

  • Django 應用用 HMAC 簽名的請求發送
  • DRF 伺服器驗證簽名,確認是可信的 Django 應用發出的請求,然後回傳結果。

2. Django 應用 → AI 推論伺服器(DRF 或 FastAPI 等)

  • Django 承擔前端/後端角色
  • 重型 AI 推論工作交給獨立伺服器(DRF、FastAPI、Flask 等)

Django 會:

  • POST /v1/infer,傳送輸入文字、圖片 URL、選項等
  • 用 HMAC 簽名

AI 伺服器:

  • 驗證 HMAC,若有效才使用 GPU 執行推論

這樣即使內部網路,也能減少「偽造請求」的風險,並為內部流量增添一層信任。


Django/DRF 實戰模式:hmac_mixin.py + CBV

實際專案中,以下結構非常乾淨。

  1. 專案(或 app)根目錄下放 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 請求。"""
        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,就能輕鬆發送簽名請求。

2. 送出端 Django CBV 範例

# 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 伺服器並回傳結果。"""

    def post(self, request, *args, **kwargs):
        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,
        }

        url = settings.AI_SERVER_URL  # 例如 "https://ai-service.internal/v1/infer"

        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)

        return JsonResponse(ai_response.json(), status=ai_response.status_code)

這樣,新增任何伺服器間呼叫,只需:

  • 在 View 加入 HMACRequestMixin
  • 呼叫 self.post_with_hmac(url, payload)

Django/DRF 使用者強烈建議採用此模式,能讓 View 代碼更簡潔、重用性更高。


DRF 接收端:HMAC 簽名驗證 Authentication 類別範例

接收方(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")

        # 伺服器間通訊,返回 (None, None) 或自訂服務帳號
        return (None, None)

在 DRF View 中使用

# 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):
        input_text = request.data.get("text", "")
        result = {"answer": f"AI result for: {input_text}"}
        return Response(result)

這樣即可完成:

  • 發送端 Django:使用 HMACRequestMixin 送出簽名請求
  • 接收端 DRF:用 HMACSignatureAuthentication 驗證簽名,驗證通過後執行業務

總結

  • HMAC 簽名利用 共用秘密金鑰,同時驗證請求的 完整性認證
  • 能有效防止:
  • 請求主體被篡改
  • 伺服器偽裝
  • 重放攻擊(配合時間戳/nonce)
  • 不適用於客戶端,因為金鑰無法隱藏,且 HMAC 不是加密。
  • 在 Django/DRF 專案中,將簽名邏輯抽成 hmac_mixin.py,在 CBV 中繼承即可;
  • 接收端 DRF 可寫自訂 Authentication 類別驗證簽名,確保伺服器間通訊安全。

image