비밀번호 정책 기능 구현
Some checks failed
Build And Test / build-and-push (push) Failing after 2m49s

- RegisterSerializer에 비밀번호 정책 검증 추가
- ExtendPasswordExpiryView: 비밀번호 유효기간 연장 API
- CustomTokenObtainPairSerializer: 로그인 시 만료/잠금 검증
- password_utils.py: 정책 검증, 계정 잠금, 만료 체크 유틸리티
- SiteSettings 모델에 비밀번호 정책 필드 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 21:35:46 +09:00
parent 1c7f241b37
commit 6b4d38ad5f
6 changed files with 624 additions and 36 deletions

View File

@ -19,19 +19,52 @@ class RegisterSerializer(serializers.ModelSerializer):
return obj.has_usable_password()
def validate_email(self, value):
if CustomUser.objects.filter(email=value).exists():
# 업데이트 시에는 자기 자신 제외
instance = getattr(self, 'instance', None)
queryset = CustomUser.objects.filter(email=value)
if instance:
queryset = queryset.exclude(pk=instance.pk)
if queryset.exists():
raise ValidationError("이미 사용 중인 이메일입니다.")
return value
def validate_name(self, value):
if CustomUser.objects.filter(name=value).exists():
# 업데이트 시에는 자기 자신 제외
instance = getattr(self, 'instance', None)
queryset = CustomUser.objects.filter(name=value)
if instance:
queryset = queryset.exclude(pk=instance.pk)
if queryset.exists():
raise ValidationError("이미 사용 중인 이름입니다.")
return value
def validate_password(self, value):
"""비밀번호 정책 검증 (회원가입 시)"""
from .models import SiteSettings
from .password_utils import validate_password_policy
# 비밀번호가 제공된 경우에만 검증 (업데이트 시 비밀번호 변경 안 할 수 있음)
if value:
site_settings = SiteSettings.get_settings()
is_valid, errors = validate_password_policy(value, site_settings)
if not is_valid:
raise ValidationError(errors)
return value
def create(self, validated_data):
password = validated_data.pop("password")
from .models import SiteSettings
from .password_utils import validate_password_policy
from django.utils import timezone
password = validated_data.pop("password", None)
# 회원가입 시 비밀번호 필수
if not password:
raise ValidationError({"password": "비밀번호는 필수입니다."})
user = CustomUser(**validated_data)
user.set_password(password)
user.password_changed_at = timezone.now() # 비밀번호 변경일 설정
user.save()
return user
@ -70,6 +103,12 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
return token
def validate(self, attrs):
from .models import SiteSettings
from .password_utils import (
check_account_lockout, record_login_failure,
reset_login_failures, check_password_expiry
)
identifier = attrs.get("email") # 이메일 또는 이름
password = attrs.get("password")
@ -80,11 +119,27 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
if user is None:
raise ValidationError("계정 또는 비밀번호가 올바르지 않습니다.")
# 사이트 설정 가져오기
site_settings = SiteSettings.get_settings()
# 계정 잠금 상태 확인
lockout_status = check_account_lockout(user, site_settings)
if lockout_status['is_locked']:
raise ValidationError(lockout_status['message'])
if not user.is_active:
raise ValidationError("계정이 비활성화되어 있습니다. 관리자에게 문의하세요.")
# 비밀번호 검증
if not user.check_password(password):
# 로그인 실패 기록
record_login_failure(user, site_settings)
raise ValidationError("계정 또는 비밀번호가 올바르지 않습니다.")
# 로그인 성공 - 실패 횟수 초기화
reset_login_failures(user)
# 부모 클래스의 validate를 위해 attrs에 실제 email 설정
attrs["email"] = user.email
self.user = user # ✅ 수동 설정 필요
@ -92,6 +147,17 @@ class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
data["email"] = user.email
data["grade"] = user.grade
# 비밀번호 만료 상태 확인
expiry_status = check_password_expiry(user, site_settings)
if expiry_status['is_expired']:
data["password_expired"] = True
data["password_message"] = expiry_status['message']
elif expiry_status['is_warning']:
data["password_warning"] = True
data["password_message"] = expiry_status['message']
data["password_days_until_expiry"] = expiry_status['days_until_expiry']
return data