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. 서버 위장(스푸핑) 방지

공격자가 “나도 저 서버인 척 해볼까?” 하고 요청을 보내도,

  • 비밀 키를 모르면 올바른 서명값을 계산할 수 없습니다.
  • 수신 서버는 “서명 검증 실패 → 이건 진짜 서버가 아니다”라고 판단합니다.

즉, 서버끼리 서로 “비밀 키를 아는 사람만 진짜” 라는 신뢰를 전제로 통신할 수 있습니다.

3. 리플레이 공격 방지(타임스탬프/nonce와 함께)

공격자가 이전에 유효했던 요청을 그대로 복사해서 반복 전송하는 것도 위험합니다.

그래서 보통 HMAC 서명에는:

  • 타임스탬프 (X-HMAC-Timestamp 같은 헤더)
  • 또는 nonce(한 번 쓰고 버리는 랜덤 값)

을 같이 포함시키고, 수신 서버에서:

  • “요청 시각이 너무 오래된 요청이면 거부”
  • “한 번 처리한 nonce는 다시 받지 않음”

과 같은 정책을 적용하여 리플레이 공격도 줄일 수 있습니다.

※ 물론, HMAC만으로 모든 걸 해결할 수 있는 건 아니고, 반드시 HTTPS(TLS) 위에서 사용하는 것이 기본 전제입니다. HMAC은 추가적인 무결성/인증 계층입니다.


HMAC 서명의 한계: 왜 클라이언트(앱, 웹 프론트)에는 쓰면 안 되는가



HMAC 서명을 사용해서는 안되는 경우가 있습니다.

“클라이언트와의 통신에서는 사용할 수 없다. (리버스엔지니어링 때문에 key 노출)”

1. 클라이언트 코드에는 비밀 키를 숨길 수 없다

  • 모바일 앱, SPA 프론트엔드(JS), 데스크톱 앱 등 클라이언트 코드에는 결국 빌드된 바이너리/자바스크립트 안에 키를 넣어야 합니다.
  • 공격자가 앱을 디컴파일하거나, 브라우저에서 JS를 들여다보면 secret key를 추출할 수 있습니다.

한 번 키가 노출되면:

  • 공격자는 그 키로 임의의 HMAC 서명을 만들 수 있고
  • 서버 입장에서는 “이 요청이 진짜 클라이언트에서 온 건지, 공격자가 찍어낸 건지” 구별할 방법이 없습니다.

즉, HMAC의 전제(“키는 둘만 안다”)가 깨집니다.

그래서 HMAC 서명은:

  • 서버 ↔ 서버 (비밀 키를 안전하게 관리 가능한 환경)
  • 백엔드 ↔ 백엔드 마이크로서비스

같은 신뢰된 환경 사이에 쓰는 것이 맞고,

  • 모바일 앱, 웹 프론트엔드 같은 배포된 클라이언트와의 통신에는 일반적인 API 인증(JWT, OAuth2, 세션 등)을 사용해야 합니다.

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)

이렇게 하면,

  • 새로운 서버-서버 호출이 필요할 때마다

  • 해당 View에 HMACRequestMixin을 상속하고

  • self.post_with_hmac(url, payload)만 호출하면 됩니다.

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)을 리턴해도 된다.
        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):
        # 여기까지 들어왔다는 건 HMAC 검증을 통과했다는 의미
        input_text = request.data.get("text", "")

        # 여기서 실제 AI 추론 수행
        result = {"answer": f"AI result for: {input_text}"}

        return Response(result)

이렇게 구성하면:

  • 송신 측 Django: HMACRequestMixin으로 쉽게 서명된 요청 전송
  • 수신 측 DRF:

  • HMACSignatureAuthentication으로 서명 검증

  • 검증 통과 시에만 비즈니스 로직 실행

이라는 명확한 구조가 만들어집니다.


정리

  • HMAC 서명은 서버-서버 간 공유된 비밀 키를 이용해 요청의 무결성(변조 방지)인증(누가 보냈는지) 을 검증하는 기법입니다.
  • 이 기법은

  • 요청 본문 위·변조

  • 서버 위장(스푸핑)
  • 리플레이 공격(타임스탬프/nonce와 함께) 같은 문제를 줄이는 데 효과적입니다.
  • 하지만,

  • 클라이언트 코드(앱, JS)에는 비밀 키를 숨길 수 없기 때문에 HMAC 서명을 클라이언트-서버 인증 용도로 쓰는 것은 부적절합니다.

  • 실제 Django/DRF 프로젝트에서는

  • hmac_mixin.py공통 HMAC 서명 + POST 요청 메서드를 구현하고

  • CBV에서 이 믹스인을 상속하여 간단히 post_with_hmac를 호출하는 패턴이 매우 편리합니다.
  • 수신 측 DRF 서버는 커스텀 Authentication 클래스로 HMAC 서명을 검증하여 서버-서버 통신을 안전하게 보호할 수 있습니다.

image