Защита целостности запросов между серверами в Django/DRF с помощью HMAC‑подписей

Когда два сервера обмениваются данными, как убедиться, что запрос действительно пришёл от нужного сервера и не был изменён в пути? В этой статье рассматривается, как в среде Django / DRF использовать подпись на основе HMAC для обеспечения целостности и доверия между серверами.

Особенно:

  • Зачем нужна эта техника
  • Какие угрозы она предотвращает
  • Почему она не подходит для клиент‑серверной коммуникации (ограничения)
  • Как реализовать hmac_mixin.py + CBV в реальном проекте

Что такое HMAC‑подпись?



HMAC (Hash-based Message Authentication Code) – это способ создания подписи из «секретного ключа + сообщения».

  • Два сервера (A, B) знают один и тот же секретный ключ.
  • Сервер A, отправляя POST к серверу B:
  • Собирает тело запроса и таймстамп
  • Создаёт подпись HMAC
  • Прикладывает подпись к заголовку HTTP Header и отправляет.
  • Сервер B, получив запрос, делает то же самое с тем же ключом и сравнивает подписи:
  • Если совпадают – запрос считается надёжным
  • Если нет – отклоняется.

Таким образом, HMAC‑подпись проверяет одновременно:

  • Целостность – содержимое не изменилось
  • Аутентификацию – только владелец ключа мог создать подпись

※ Предполагается, что секретный ключ может быть безопасно обменян между серверами.


Цели и предотвращаемые угрозы

1. Защита от подмены тела запроса

Если злоумышленник перехватывает трафик, он может изменить тело запроса, например, изменить сумму перевода. Сервер, пересчитав подпись, обнаружит изменение и отклонит запрос.

2. Защита от подмены сервера (spoofing)

Без знания секретного ключа злоумышленник не сможет сформировать корректную подпись. Сервер отклонит запрос, считая его фальшивым.

3. Защита от replay‑атак

Подпись обычно сопровождается таймстампом или nonce. Сервер отклоняет запросы, которые слишком старые или уже использованы.

※ HMAC – дополнительный слой над HTTPS/TLS; не заменяет шифрование.


Почему HMAC не подходит для клиент‑серверных взаимодействий



1. Ключ не может быть скрыт в клиентском коде

Мобильные приложения, SPA‑frontend, десктопные клиенты – все они вынуждены хранить ключ в исполняемом коде. При реверс‑инжиниринге ключ может быть извлечён, и злоумышленник сможет генерировать валидные подписи.

2. HMAC не шифрует тело

Подпись лишь подтверждает неизменность, но не скрывает данные. Чувствительные данные всё равно защищаются TLS.

Итого: HMAC‑подпись предназначена для сервер ↔ сервер и backend‑backend микросервисов, но не для клиентских приложений.


Когда стоит использовать HMAC‑подпись? (Сценарии)

1. Django‑приложение → отдельный DRF‑сервер аутентификации

Много сервисов используют общий сервер аутентификации. Основное приложение отправляет POST‑запросы с HMAC‑подписью, а DRF‑сервер проверяет и возвращает токены.

2. Django‑приложение → AI‑сервер (DRF, FastAPI и т.д.)

Джанго обрабатывает фронтенд, а тяжёлые AI‑инференсы делегируются отдельному серверу. Запросы к AI‑серверу подписываются HMAC, чтобы предотвратить подделку.


Практический паттерн в Django/DRF: hmac_mixin.py + CBV

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:
    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:
        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):
        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

2. Пример 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):
    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

        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)

3. Приёмник 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

    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")

        return (None, None)
# 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]

    def post(self, request, *args, **kwargs):
        input_text = request.data.get("text", "")
        result = {"answer": f"AI result for: {input_text}"}
        return Response(result)

Итоги

  • HMAC‑подпись обеспечивает целостность и аутентификацию запросов между серверами.
  • Эффективно защищает от:
  • Подмены тела запроса
  • Подмены сервера
  • Replay‑атак (с таймстампом/nonce)
  • Не подходит для клиент‑серверных взаимодействий из‑за раскрытия ключа и отсутствия шифрования.
  • В Django/DRF удобно реализовать через hmac_mixin.py и CBV, а на стороне DRF – через кастомный Authentication.

image