Siner's Blog


장고의 패스워드 유효성 검사 코드분석

Django Password Validator

2021/08/16


django의 startproject로 기본적으로 생성되는 AUTH_PASSWORD_VALIDATORS에 대해 분석해보려 합니다

# Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ]

1. UserAttributeSimilarityValidator

유저의 attributes(username, first_name, last_name, email)를 정규표현식으로 split한 후 각각의 value_part에 대해 유사도를 측정하고, 유사도가 max_similarity(default: 0.7)이상인 경우 validation error를 발생시킵니다. 유사도 측정에는 SequenceMatcher를 사용합니다.

class UserAttributeSimilarityValidator: """ Validate whether the password is sufficiently different from the user's attributes. If no specific attributes are provided, look at a sensible list of defaults. Attributes that don't exist are ignored. Comparison is made to not only the full attribute value, but also its components, so that, for example, a password is validated against either part of an email address, as well as the full address. """ DEFAULT_USER_ATTRIBUTES = ('username', 'first_name', 'last_name', 'email') def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7): self.user_attributes = user_attributes self.max_similarity = max_similarity def validate(self, password, user=None): if not user: return for attribute_name in self.user_attributes: value = getattr(user, attribute_name, None) if not value or not isinstance(value, str): continue value_parts = re.split(r'\W+', value) + [value] for value_part in value_parts: if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity: try: verbose_name = str(user._meta.get_field(attribute_name).verbose_name) except FieldDoesNotExist: verbose_name = attribute_name raise ValidationError( _("The password is too similar to the %(verbose_name)s."), code='password_too_similar', params={'verbose_name': verbose_name}, ) def get_help_text(self): return _('Your password can’t be too similar to your other personal information.')

2. MinimumLengthValidator

password의 길이가 min_length(default: 8)보다 작다면 validatoion error를 발생시킵니다.

class MinimumLengthValidator: """ Validate whether the password is of a minimum length. """ def __init__(self, min_length=8): self.min_length = min_length def validate(self, password, user=None): if len(password) < self.min_length: raise ValidationError( ngettext( "This password is too short. It must contain at least %(min_length)d character.", "This password is too short. It must contain at least %(min_length)d characters.", self.min_length ), code='password_too_short', params={'min_length': self.min_length}, ) def get_help_text(self): return ngettext( "Your password must contain at least %(min_length)d character.", "Your password must contain at least %(min_length)d characters.", self.min_length ) % {'min_length': self.min_length}

3. CommonPasswordValidator

사람들이 가장 많이 사용하는 패스워드 20000개에 해당하는 경우 validator error를 발생시킵니다. 2000개의 패스워드는 gzip으로 압축되어 django프로젝트에 내장되어있습니다. (django/contrib/auth/common-passwords.txt.gz)

class CommonPasswordValidator: """ Validate whether the password is a common password. The password is rejected if it occurs in a provided list of passwords, which may be gzipped. The list Django ships with contains 20000 common passwords (lowercased and deduplicated), created by Royce Williams: https://gist.github.com/roycewilliams/281ce539915a947a23db17137d91aeb7 The password list must be lowercased to match the comparison in validate(). """ DEFAULT_PASSWORD_LIST_PATH = Path(__file__).resolve().parent / 'common-passwords.txt.gz' def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH): try: with gzip.open(password_list_path, 'rt', encoding='utf-8') as f: self.passwords = {x.strip() for x in f} except OSError: with open(password_list_path) as f: self.passwords = {x.strip() for x in f} def validate(self, password, user=None): if password.lower().strip() in self.passwords: raise ValidationError( _("This password is too common."), code='password_too_common', ) def get_help_text(self): return _('Your password can’t be a commonly used password.')

4. NumericPasswordValidator

password가 숫자로만 이루어진 경우 validation error를 발생시킵니다.

class NumericPasswordValidator: """ Validate whether the password is alphanumeric. """ def validate(self, password, user=None): if password.isdigit(): raise ValidationError( _("This password is entirely numeric."), code='password_entirely_numeric', ) def get_help_text(self): return _('Your password can’t be entirely numeric.')