Django 개발 시, URL 파라미터나 폼의 hidden 필드, 쿠키 등을 통해 클라이언트에게 데이터를 보냈다가 다시 받아야 할 때가 있습니다. 이때 "이 데이터가 중간에 사용자에 의해 변경되지 않았을까?"라는 고민이 생깁니다. django.core.signing은 바로 이 문제를 해결하기 위한 강력한 도구입니다.
이 모듈은 데이터의 암호화(Encryption)가 아닌 암호화 서명(Cryptographic Signing)을 제공합니다.
-
암호화 (X): 데이터의 내용을 숨깁니다.
-
서명 (O): 데이터의 내용은 노출될 수 있지만, 데이터가 위변조되지 않았음을 보장합니다.
1. 핵심 사용법: dumps()와 loads()
signing 모듈의 핵심은 dumps()와 loads()입니다. 파이썬 객체를 서명된 문자열로 만들거나, 서명된 문자열을 다시 검증하고 객체로 복원합니다.
dumps(): 객체를 서명된 문자열로 변환
dumps()는 딕셔너리, 리스트 등 JSON 직렬화가 가능한 객체를 받아, URL-safe한 서명된 문자열을 반환합니다.
from django.core.signing import dumps
# 서명할 데이터
user_data = {'user_id': 123, 'role': 'user'}
# 데이터를 서명하여 문자열로 만듭니다.
signed_data = dumps(user_data)
print(signed_data)
# 출력 예시: eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJ1c2VyIn0:1q51oH:F... (데이터:서명)
결과물은 [인코딩된 데이터]:[서명] 형태입니다. 앞부분은 원본 데이터를 Base64로 인코딩한 것이라 누구나 디코딩해서 원본을 볼 수 있습니다. 뒷부분이 바로 Django의 SECRET_KEY로 생성한 서명(해시값)입니다.
loads(): 서명된 문자열을 객체로 복원 (검증)
loads()는 dumps()로 생성된 문자열을 받아 서명을 검증합니다. 서명이 유효하면 원본 객체를 반환하고, 유효하지 않으면 BadSignature 예외를 발생시킵니다.
from django.core.signing import loads, BadSignature
try:
# 서명된 데이터를 원래 객체로 복원 (검증)
unsigned_data = loads(signed_data)
print(unsigned_data)
# 출력: {'user_id': 123, 'role': 'user'}
except BadSignature:
print("데이터가 위변조되었거나 서명이 유효하지 않습니다.")
# 만약 데이터가 1글자라도 변경되었다면?
tampered_data = signed_data.replace('user', 'admin')
try:
loads(tampered_data)
except BadSignature:
print("위조된 데이터는 BadSignature 예외를 발생시킵니다.")
항상 try...except BadSignature 구문으로 감싸서 사용해야 합니다.
2. 핵심 특징: 데이터는 어디에 저장되나요?
django.core.signing의 가장 큰 특징은 "Stateless (상태 비저장)"라는 점입니다.
dumps()로 생성된 문자열은 서버의 데이터베이스나 캐시 어디에도 저장되지 않습니다. 모든 정보(데이터와 서명)가 서명된 문자열 그 자체에 포함되어 있습니다.
서버는 이 문자열을 클라이언트에게 (URL, 쿠키, 폼 필드 등으로) 전달했다가, 나중에 클라이언트가 이 값을 다시 제출하면 SECRET_KEY를 이용해 즉석에서 유효성을 검증할 뿐입니다. 이 덕분에 서버의 저장 공간을 전혀 사용하지 않아 매우 가볍고 효율적입니다.
3. 유효 시간 설정: max_age의 비밀
"데이터가 서버에 저장되지 않는데 어떻게 유효 시간을 설정할 수 있나요?"
max_age 옵션은 dumps() 시 타임스탬프(시간 정보)를 데이터와 함께 서명에 포함시킵니다.
loads()를 호출할 때 max_age 인자(초 단위)를 전달하면, 서명 검증 후 내장된 타임스탬프를 확인합니다.
-
현재 시간과 타임스탬프의 차이를 계산합니다.
-
이 차이가
max_age를 초과하면, 서명이 위조되지 않았더라도SignatureExpired예외를 발생시킵니다.
from django.core.signing import dumps, loads, SignatureExpired, BadSignature
import time
# 1. 서명 생성 (이때의 시간이 기록됨)
signed_data = dumps({'user_id': 456})
# 2. 10초 유효기간으로 검증 (즉시) -> 성공
try:
data = loads(signed_data, max_age=10)
print(f"검증 성공: {data}")
except SignatureExpired:
print("서명이 만료되었습니다.")
except BadSignature:
print("서명이 유효하지 않습니다.")
# 3. 5초 대기
time.sleep(5)
# 4. 3초 유효기간으로 검증 (이미 5초 지남) -> 실패
try:
data = loads(signed_data, max_age=3)
print(f"검증 성공: {data}")
except SignatureExpired:
print("서명이 만료되었습니다. (max_age=3)")
except BadSignature:
print("서명이 유효하지 않습니다.")
이 역시 서버에 만료 시간을 저장하는 방식이 아니며, 서명 자체에 포함된 타임스탬프를 활용하는 stateless 방식입니다.
4. 중요! 주의사항 및 활용 팁
-
SECRET_KEY는 생명입니다.
이 모든 서명 메커니즘은 settings.py의 SECRET_KEY를 기반으로 동작합니다. 이 키가 유출되면 누구나 유효한 서명을 만들어낼 수 있으므로 절대 외부에 노출해서는 안 됩니다. (Git에 올리지 마세요!)
-
암호화가 아님을 명심하세요.
서명된 데이터의 앞부분(Base64)은 누구나 쉽게 디코딩하여 원본 내용을 볼 수 있습니다. 비밀번호, 개인정보 등 민감한 데이터를 절대 dumps()에 직접 넣지 마세요. (예: user_id는 괜찮지만, user_password는 안 됩니다.)
-
salt로 서명을 분리하세요.
서로 다른 용도로 서명을 사용할 때는 salt 인자를 사용하세요. salt가 다르면 동일한 데이터라도 서명 결과가 완전히 달라집니다.
# 용도에 따라 다른 salt 사용
pw_reset_token = dumps(user.pk, salt='password-reset')
unsubscribe_link = dumps(user.pk, salt='unsubscribe')
이렇게 하면 '탈퇴 링크'용 서명을 '비밀번호 재설정'에 재사용하는 등의 공격을 막을 수 있습니다.
5. 주요 활용 사례
-
비밀번호 재설정 URL: 사용자의 ID와 타임스탬프를 서명하여 이메일로 전송 (DB에 임시 토큰 저장 불필요)
-
이메일 인증 링크: 신규 가입 시 이메일 인증 링크
-
안전한
nextURL: 로그인 후 리디렉션될?next=/private/URL이 악의적인 사이트로 변경되는 것 방지 -
임시 다운로드 링크: 특정 시간(
max_age) 동안만 유효한 파일 접근 URL 생성 -
다단계 폼 (Form Wizard): 이전 단계의 폼 데이터를 다음 단계로 넘길 때 데이터 위변조 방지
댓글이 없습니다.