비밀번호 정책 기능 구현
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

@ -14,14 +14,37 @@ class SiteSettings(models.Model):
"""
사이트 전역 설정 (싱글톤 패턴)
- Google 로그인 활성화 여부 등 관리
- 비밀번호 정책 설정
"""
# 소셜 로그인 설정
google_login_enabled = models.BooleanField(default=True, verbose_name="Google 로그인 활성화")
# 향후 확장 가능한 설정들
# kakao_login_enabled = models.BooleanField(default=False, verbose_name="카카오 로그인 활성화")
# naver_login_enabled = models.BooleanField(default=False, verbose_name="네이버 로그인 활성화")
# registration_enabled = models.BooleanField(default=True, verbose_name="회원가입 허용")
# ========== 비밀번호 정책 설정 ==========
# 비밀번호 길이
password_min_length = models.IntegerField(default=8, verbose_name="최소 비밀번호 길이")
password_max_length = models.IntegerField(default=128, verbose_name="최대 비밀번호 길이")
# 비밀번호 복잡성 요구사항
password_require_uppercase = models.BooleanField(default=True, verbose_name="대문자 필수")
password_require_lowercase = models.BooleanField(default=True, verbose_name="소문자 필수")
password_require_digit = models.BooleanField(default=True, verbose_name="숫자 필수")
password_require_special = models.BooleanField(default=True, verbose_name="특수문자 필수")
password_special_chars = models.CharField(
max_length=100,
default="!@#$%^&*()_+-=[]{}|;:,.<>?",
verbose_name="허용 특수문자"
)
# 비밀번호 만료 정책
password_expiry_days = models.IntegerField(default=90, verbose_name="비밀번호 만료일 (0=무제한)")
password_expiry_warning_days = models.IntegerField(default=14, verbose_name="만료 경고일 (만료 전 N일)")
# 비밀번호 이력 관리
password_history_count = models.IntegerField(default=3, verbose_name="이전 비밀번호 재사용 금지 횟수 (0=제한없음)")
# 계정 잠금 정책
login_max_failures = models.IntegerField(default=5, verbose_name="최대 로그인 실패 횟수 (0=무제한)")
login_lockout_minutes = models.IntegerField(default=30, verbose_name="계정 잠금 시간(분)")
updated_at = models.DateTimeField(auto_now=True)
@ -124,6 +147,11 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
encrypted_nhn_api_password = models.BinaryField(blank=True, null=True, verbose_name="NHN Cloud API Password (암호화)")
nhn_storage_account = models.CharField(max_length=128, blank=True, null=True, verbose_name="NHN Cloud Storage Account")
# 🔒 비밀번호 보안 관련 필드
password_changed_at = models.DateTimeField(blank=True, null=True, verbose_name="비밀번호 변경일시")
login_failures = models.IntegerField(default=0, verbose_name="로그인 실패 횟수")
locked_until = models.DateTimeField(blank=True, null=True, verbose_name="계정 잠금 해제 시간")
objects = CustomUserManager()
USERNAME_FIELD = 'email'
@ -339,3 +367,29 @@ class KVMServer(models.Model):
if self.libvirt_uri:
return self.libvirt_uri
return f"qemu+ssh://{self.username}@{self.host}:{self.port}/system"
# ============================================
# 비밀번호 이력 관리
# ============================================
class PasswordHistory(models.Model):
"""
사용자 비밀번호 변경 이력 (재사용 방지용)
"""
user = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='password_history',
verbose_name="사용자"
)
password_hash = models.CharField(max_length=255, verbose_name="비밀번호 해시")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "비밀번호 이력"
verbose_name_plural = "비밀번호 이력"
ordering = ['-created_at']
def __str__(self):
return f"{self.user.email} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"