Trouble Shooting

비밀번호 해시화

kinggoddino 2024. 9. 9.

 

신나는 장고 DRF 과제

회원가입 기능과 로그인 기능을 만들었다

 

포스트맨에서 테스트해보기

 

회원가입

성공

 

 

 

로그인

아니 누가봐도 위에 있는 아이디 비밀번호랑 똑같은데

"비밀번호가 일치하지 않습니다"라고 어거지를 쓴다

 

내 눈이 잘못된건가 해서 수많은 시도를 하였으나

계속해서 400에러를 건네주는 친절한 장고

 

 

원인찾기

 

 

선생님이 문제가 발생했을때는 소거법으로 해결하라그랬어.

 

(내가 생각했을 때)제일 의심스러운 부분부터 확인했는데 순서는 이런식이었음

 

1. 비밀번호 맞는지 대조하는 로직 확인하기

2. 데이터베이스에 저장된 비밀번호 확인하기

3. 입력된 비밀번호 처리하는 로직 확인하기 

 

여기서 끝났기 때문에 그 다음은 생각 안해봤다

 

 

처음 한 생각

비밀번호가 맞는지 확인하는 로직이 작성된 곳을 찾아보면 머라도 나오겠지?

 

일단 나는 뷰에서는 

# 회원가입 View
class SignupView(generics.CreateAPIView):
    serializer_class = SignupSerializer

# 로그인 View
class LoginView(TokenObtainPairView):
    serializer_class = LoginSerializer

이렇게 전부 시리얼라이저에게 떠넘겼기 때문에

 

시리얼라이저를 조져본다

# 회원가입 Serializer
class SignupSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomUser
        fields = ["username", "password", "email", "name", "nickname", "birthday", "gender", "bio"]

그냥 평범한데.

회원가입할 때 필요한 필드들을 나열하는 역할을 하는 것 뿐이라 얜 잘못 없을듯.

 

다음 시리얼라이저.

# 로그인 Serializer
class LoginSerializer(TokenObtainPairSerializer):
    def validate(self, attrs):                                               # attrs: 입력받은 데이터 (username, password)

        try:
            user = CustomUser.objects.get(username=attrs["username"])        # 입력받은 username과 db의 username이 일치하는 사용자를 찾아냄
       
        except CustomUser.DoesNotExist:
            raise serializers.ValidationError("그런 아이디는 없습니다.")       # 일치하는 사용자가 없으면 None이 아니라 DoesNotExist가 반환되므로 예외처리

        if not user.check_password(attrs["password"]):                        # 입력받은 password와 db에 저장된 사용자의 password와 일치하는지 확인
            raise serializers.ValidationError("비밀번호가 일치하지 않습니다.")  # 일치 안하면 예외 발생!!!

        data = super().validate(attrs)                                        # validate 메서드 호출해서 원래 검증 진행(JWT 토큰 생성됨)
        return data

이거네.

완전 의심스럽게 생겼다.

"비밀번호가 일치하지 않습니다." 이거 장고 기능 사용해서 말했던 게 아니라 내가 발생시킨 예외였구나. 나를 화나게 했던 게 과거의 나였어

 

어쨌든 저 문구가 출력된다는 건, 예외 발생까지는 제대로 실행되면서 내려오고 있는거니까 

if not user.check_password(attrs["password"]):                        
# 입력받은 password와 db에 저장된 사용자의 password와 일치하는지 확인

여기까지 동작은 제대로 하고 있다는 건데

 

아무리 봐도 로직이 잘못 작성된 것이 아니라

진짜로 입력받은 password와 db에 저장된 password가 다르다는 결론이 난다.

 

    def check_password(self, raw_password):
        """
        Return a boolean of whether the raw_password was correct. Handles
        hashing formats behind the scenes.
        """

        def setter(raw_password):
            self.set_password(raw_password)
            # Password hash upgrades shouldn't be considered password changes.
            self._password = None
            self.save(update_fields=["password"])

        return check_password(raw_password, self.password, setter)

 

 

데이터베이스 accounts_customuser 테이블 확인해보기

잘 저장되고 있는데?

 

입력된 password와 db에 저장된 password를 비교하는 로직에 문제가 있어 보이는지?  x

db에 저장된 password 에 문제가 있어 보이는지? x

 

그럼 이제 입력된 password가 어떻게 처리되고 있는지 확인해보기

 

 

내가 만든 CustomUser 모델

class CustomUser(AbstractUser):
    GENDER_CHOICES = [("M", "남자"), ("F", "여자"), ("O", "다른거"),]
    email = models.EmailField(unique=True)
    name = models.CharField(max_length=10)
    nickname = models.CharField(max_length=10)
    birthday = models.DateField()
    gender = models.CharField(max_length=1, choices=GENDER_CHOICES, blank=True)
    bio = models.TextField(blank=True)

    def __str__(self):
        return self.username

CustomUser 클래스는 AbstractUser 클래스를 상속받아서 만들었고

class AbstractUser(AbstractBaseUser, PermissionsMixin):

AbstractUser 클래스는 AbstractBaseUser 클래스를 상속받아서 만들었고

class AbstractBaseUser(models.Model):
    password = models.CharField(_("password"), max_length=128)
    
    def set_password(self, raw_password):            # raw_password == 평문 비밀번호
        self.password = make_password(raw_password)  # 평문 비밀번호를 해싱해서 저장
        self._password = raw_password                # 얘는 메모리에 임시 저장. 나중에 password_validation.password_changed 에서 사용됨

AbstractBaseUser 클래스는 models.Model 클래스를 상속받아서 만들었는데…

오 다행히 여기서 password에 관련된 메서드가 등장

 

make_password 함수가 raw_password(평문 비밀번호)를 전달 받아서

뭔가를 한 다음에 self.password에 저장하고 있나보네

 

make_password 함수는 머하는 애일까

def make_password(password, salt=None, hasher="default"):
    """
    Turn a plain-text password into a hash for database storage

    Same as encode() but generate a new random salt. If password is None then
    return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
    which disallows logins. Additional random string reduces chances of gaining
    access to staff or superuser accounts. See ticket #20079 for more info.
    """
    if password is None:
        return UNUSABLE_PASSWORD_PREFIX + get_random_string(
            UNUSABLE_PASSWORD_SUFFIX_LENGTH
        )
    if not isinstance(password, (bytes, str)):
        raise TypeError(
            "Password must be a string or bytes, got %s." % type(password).__qualname__
        )
    hasher = get_hasher(hasher)
    salt = salt or hasher.salt()
    return hasher.encode(password, salt)