Feat: 강력한 비밀번호 정책 및 로그인 보안 강화
- 비밀번호 최소 10자 이상, 특수문자 1개 이상 필수 - 20회 로그인 실패 시 비밀번호 재설정 필요 - 로그인 페이지에 남은 시도 횟수 경고 표시 - 계정 잠금 시 비밀번호 재설정 링크 제공 - 회원가입 페이지에 비밀번호 요구사항 체크리스트 UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,41 @@ def login(
|
||||
):
|
||||
"""로그인"""
|
||||
user = db.query(User).filter(User.email == form_data.username).first()
|
||||
if not user or not verify_password(form_data.password, user.password_hash):
|
||||
|
||||
# 사용자가 존재하는 경우 로그인 실패 처리
|
||||
if user:
|
||||
# 비밀번호 재설정 필요 여부 체크
|
||||
if getattr(user, 'password_reset_required', False):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Password reset required. Too many failed login attempts.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 비밀번호 검증
|
||||
if not verify_password(form_data.password, user.password_hash):
|
||||
# 실패 횟수 증가
|
||||
failed_attempts = getattr(user, 'failed_login_attempts', 0) or 0
|
||||
user.failed_login_attempts = failed_attempts + 1
|
||||
|
||||
# 20회 이상 실패 시 비밀번호 재설정 필요
|
||||
if user.failed_login_attempts >= 20:
|
||||
user.password_reset_required = True
|
||||
db.commit()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account locked. Password reset required due to too many failed attempts.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
db.commit()
|
||||
remaining = 20 - user.failed_login_attempts
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Incorrect email or password. {remaining} attempts remaining before account lock.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
@@ -184,6 +218,10 @@ def login(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 로그인 성공 - 실패 횟수 초기화
|
||||
user.failed_login_attempts = 0
|
||||
db.commit()
|
||||
|
||||
access_token = create_access_token(data={"sub": user.email})
|
||||
return Token(access_token=access_token)
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ class User(Base):
|
||||
withdrawal_reason = Column(String(500), nullable=True) # 탈퇴 사유
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True) # 실제 삭제 시각 (soft delete)
|
||||
|
||||
# Login security
|
||||
failed_login_attempts = Column(Integer, default=0) # 로그인 실패 횟수
|
||||
locked_until = Column(DateTime(timezone=True), nullable=True) # 계정 잠금 해제 시간
|
||||
password_reset_required = Column(Boolean, default=False) # 비밀번호 재설정 필요 여부
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Note: foreign_keys specified as string to avoid circular import
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
@@ -11,6 +12,15 @@ class UserCreate(BaseModel):
|
||||
country: str = "Mongolia"
|
||||
referred_by: Optional[str] = None # Referral code of the user who referred
|
||||
|
||||
@field_validator('password')
|
||||
@classmethod
|
||||
def validate_password(cls, v):
|
||||
if len(v) < 10:
|
||||
raise ValueError('Password must be at least 10 characters long')
|
||||
if not re.search(r'[!@#$%^&*(),.?":{}|<>_\-+=\[\]\\\/`~]', v):
|
||||
raise ValueError('Password must contain at least one special character')
|
||||
return v
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""Schema for updating user profile"""
|
||||
|
||||
Reference in New Issue
Block a user