Django | 서명 알고리즘 변경 HS256 →RS256

기존 Django 인증 긴응에서는 HS256알고리즘이 적용된 서명 알고리즘을 사용하고 있었다.
istio에서 RequestAuthentication를 이용해서 토큰인증을 처리하게 구성하려고 방식을좀 변경해야 했다.

서명 알고리즘 방식

HS256 (HMAC with SHA-256)

  • 대칭키 방식 → 하나의 비밀키(SECRET_KEY)로 토큰을 발급하고 검증.
  • 서버가 여러 개라면 모든 서버에 동일한 비밀키를 공유해야 함.
  • 관리가 간단하지만, 키 유출 시 보안 위험이 큼.

RS256 (RSA with SHA-256)

  • 비대칭키 방식 → Private Key로 서명, Public Key로 검증.
  • 토큰 발급 서버는 Private Key만 보관.
  • 검증 서버들은 Public Key만 있으면 됨 → 여러 서비스/마이크로서비스 환경에 적합.
  • Istio, Keycloak, Auth0 등 대부분의 IDP/게이트웨이가 RS256 + JWKS(JSON Web Key Set) 방식 권장.

Auth 애플리케이션 적용

Django settings.py 설정 변경

기존 코드

기존에 사용하던 코드는 HS256이 적용되어있다.

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True, # 사용한 토큰은 갱신하면 블랙리스트처리됨
}

변경 코드는 RS256으로 변경할 것이다. 빠져있던 ISSUER도 추가.

SIMPLE_JWT = {
    "ALGORITHM": "RS256",
    "SIGNING_KEY": PRIVATE_KEY,
    "VERIFYING_KEY": PUBLIC_KEY,
    "ISSUER": "msa-user", # 발급자 이름 검증단계에서 값 비교에 사용. HS256은 생략해도 동작.
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True, # 사용한 토큰은 갱신하면 블랙리스트처리됨
}

환경변수를 추가해서 환경변수 설정 여부에 따라 HS256과 RS256동적으로 적용되도록 적용.

DEBUG='1'
SQL_ENGINE='django.db.backends.mysql'
SQL_HOST="{DB_ADDRESS}"
SQL_USER="{DB_USER_NAME}"
SQL_PASSWORD="{DB_USER_PASSWORD}"
SQL_DATABASE="{DB_NAME}"
SQL_PORT='3306'
SECRET_KEY='django-insecure-#############'
ISTIO_JWT=1 # 이것 추가

settings.py 수정

암호화에 사용하는 key파일은 project/keys 폴더에 생성 및 적용하였음.

# 개인키 생성
openssl genrsa -out private.pem 2048

# 공개키 추출
openssl rsa -in private.pem -pubout -out public.pem
from datetime import timedelta
ISTIO_JWT = os.environ.get("ISTIO_JWT", "0") == "1"

if ISTIO_JWT:
    # RS256 모드 
    # 운영환경에서 key파일은 POD mount로 적용하는게 안전
    with open(BASE_DIR / "keys/private.pem", "r") as f:
        PRIVATE_KEY = f.read()
    with open(BASE_DIR / "keys/public.pem", "r") as f:
        PUBLIC_KEY = f.read()

    SIMPLE_JWT = {
        "ALGORITHM": "RS256",
        "SIGNING_KEY": PRIVATE_KEY,
        "VERIFYING_KEY": PUBLIC_KEY,
        "ISSUER": "msa-user",
        "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),  
        "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
        "ROTATE_REFRESH_TOKENS": True,
        "BLACKLIST_AFTER_ROTATION": True, # 사용한 토큰은 갱신하면 블랙리스트처리됨
    }
else:
    SIMPLE_JWT = {
        "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
        "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
        "ROTATE_REFRESH_TOKENS": True,
        "BLACKLIST_AFTER_ROTATION": True, # 사용한 토큰은 갱신하면 블랙리스트처리됨
    }

Django views 추가

jwks 공개키 경로 및 내용을 노출하기 위한 설정을 추가한다.

# users/views_jwks.py
from django.http import JsonResponse, HttpResponseNotFound
from django.conf import settings
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

def jwks_view(request):
    if settings.SIMPLE_JWT["ALGORITHM"] != "RS256":
        return HttpResponseNotFound("JWKS is only available in RS256 mode")

    public_key = settings.SIMPLE_JWT["VERIFYING_KEY"]

    key = serialization.load_pem_public_key(
        public_key.encode(), backend=default_backend()
    )
    numbers = key.public_numbers()

    e = numbers.e.to_bytes((numbers.e.bit_length() + 7) // 8, "big")
    n = numbers.n.to_bytes((numbers.n.bit_length() + 7) // 8, "big")

    jwk = {
        "kty": "RSA",
        "use": "sig",
        "alg": "RS256",
        "kid": "msa-user-key",
        "n": base64.urlsafe_b64encode(n).decode().rstrip("="),
        "e": base64.urlsafe_b64encode(e).decode().rstrip("="),
    }

    return JsonResponse({"keys": [jwk]})

Django urls.py 수정

프로젝트 urls는 이미 적용되어 있으므로 app의 urls만 수정한다. ‘django-jwks’ 부분이 추가된 내용

from django.urls import path
from .views import RegisterView, MeView, CustomTokenObtainPairView, SSHKeyUploadView, SSHKeyInfoView, SSHKeyRetrieveView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView
from .views_jwks import jwks_view # django-jwks

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    # path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('verify/', TokenVerifyView.as_view(), name='token_verify'),
    path('me/', MeView.as_view(), name='me'),
    path("ssh-key/", SSHKeyUploadView.as_view(), name="ssh_key_upload"),
    path("ssh-key/info/", SSHKeyInfoView.as_view(), name="ssh_key_info"),
    path("ssh-key/view/", SSHKeyRetrieveView.as_view(), name="ssh_key_retrieve"),
    path(".well-known/jwks.json", jwks_view, name="jwks"), # django-jwks
]

Serializers.py 수정

istio jwt token 인증에는 payload에 iss와 sub가 반드시 필요함

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # ✅ JWT payload에 커스텀 정보 추가
        token["name"] = user.name
        token["grade"] = user.grade
        token["email"] = user.email  # 선택적으로 추가 가능
        token["sub"] = user.email  # 선택적으로 추가 가능

        # Kong JWT 플러그인용 issuer 정보 추가
        token["iss"] = "msa-user"

        return token
        
        ...

분리된 서비스 구성 변경

환경변수는 동일하게 따라가고 settings.py에서 변경해주어야한다.
CRUD를 분리하여서 인증정보가 달라지면 인증되지 않아 등록되지 않는 현상 발생.

key파일들은 경로맞춰주고, SIMPLE_JWT 부분을 수정하여 적용한다.

ISTIO_JWT = os.environ.get("ISTIO_JWT", "0") == "1"

if ISTIO_JWT:
    # RS256 모드 
    # 운영환경에서 key파일은 POD mount로 적용하는게 안전
    with open(BASE_DIR / "keys/private.pem", "r") as f:
        PRIVATE_KEY = f.read()
    with open(BASE_DIR / "keys/public.pem", "r") as f:
        PUBLIC_KEY = f.read()

    SIMPLE_JWT = {
        "ALGORITHM": "RS256",
        "VERIFYING_KEY": PUBLIC_KEY,
        "ISSUER": "msa-user",
        "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),  
        "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
    }