TIL

23.04.28 TIL

best_spear_man 2023. 5. 1. 18:54

오늘은...

1. 개인과제를 진행하였다.

2. 객체지향 특강을 들었다.

 

1. 개인과제 - 로그아웃

사용자의 회원가입과 로그인을 구현하였으니 로그아웃 및 탈퇴, 회원정보수정을 구현하기로 하였다.

class UserView(APIView):
    permission_classes = (IsExsistDeleteXorCreateOnly,)

    def post(self, request):  # 회원가입
        ...

    def delete(self, request):  # 탈퇴
        user = request.user
        user = User.objects.get(id=user.id)  # 유저 정보 삭제
        user.delete()
        return Response(
            {"message": "delete_success"}, status=status.HTTP_204_NO_CONTENT
        )

탈퇴는 위와같이 사용자 정보를 삭제하는 것으로 구현할 수 있었다. 그러나 로그아웃을 구현하는 것에서 문제가 발생하였다.

토큰은 기본적으로 프론트엔드에서 관리하는 것이므로, 백엔드에서는 해당 토큰을 블랙리스트에 넣지 않는 이상 유효한 토큰을 가진 사용자의 접속을 막을 수 없다. 그런데 찾아본 결과, 엑세스 토큰은 블랙리스트에 넣을 수 없고 오직 refresh만 블랙리스트에 넣을 수 있다. 즉 제대로된 로그아웃 기능을 만들 수 가 없다.

해결:

이를 해결 하기 위해 한참을 찾을 결과, 결론적으로 로그아웃은 프론트에서 처리하는 것이 맞는 것이었다. 즉, 브라우저가 저장한 엑세스 토큰과 리프레시 토큰을 삭제하면 그것이 바로 로그아웃 인 것이다. 단, 리프레시 토큰은 lifetime이 매우 기므로, 만약의 경우를 대비해 Black list에 등록하여 액세스 토큰을 재발급하는 것을 막을 수 있다.

# setting.py
INSTALLED_APPS = [
	...
    'rest_framework_simplejwt.token_blacklist',
]

위와같이 설정한 뒤, DB에 반영하면, 두개의 새로운 테이블이 생긴다.

위는 블랙리스트에 등록된 토큰의 id와 날짜가 저장되어있고 아래에는 상세한 토큰의 정보가 기록된다.

 

class LogoutView(APIView):
    def post(self, request):
        refresh_token = request.data["refresh_token"]
        token = RefreshToken(refresh_token)
        token.blacklist()
        return Response(status=status.HTTP_205_RESET_CONTENT)

이제 위와같이 refresh 토큰을 body로 받아와 blacklist에 등록하는 view를 작성해주면 된다.

단, 이 부분은 어디까지나 선택사항이며, 실제로 가장 중요한 부분은 브라우저가 토큰을 삭제하는 것이다.

async function logout() {
    localStorage.removeItem('payload')
    localStorage.removeItem('access')
    localStorage.removeItem('refresh')
}

 

2. 개인과제 - 회원정보 수정 중 오류 발생

회원정보 수정을 위해 다음과 같이 시리얼라이저를 작성하였다.

class UserEditSerializer(UserSerializer):
    current_password = serializers.CharField(
        style={"input_type": "password"}, write_only=True
    )
    password2 = serializers.CharField(style={"input_type": "password"}, write_only=True)

    class Meta:
        model = User
        exclude = ("email",)
    def update(self, instance, validated_data):
        validated_data.pop("password2", None)
        validated_data.pop("current_password", None)
        ...
        return user

    def validate(self, attrs):
        if not self.instance.check_password(attrs.get("current_password")):
            raise serializers.ValidationError(
                {"current password": "current password wrong."}
            )
        return super().validate(attrs)

이 시리얼라이저는 UserSerializer를 상속받는데 따라서 UserSerializer의 validate 메서드를 사용하고 있다. 오류는 여기서 발생하였다.

def validate(self, attrs):
        if attrs["password"] != attrs["password2"]:
            raise serializers.ValidationError(
                {"password": "Password fields didn't match."}
            )
        return super().validate(attrs)

원인:

해당 시리얼라이저는 view에서 다음과 같이 사용되었다.

user_serialized = UserEditSerializer(user, data=request.data, partial=True)

partial 인자는 이 serializer가 update를 할 때 유효한 값이 있는 필드만 업데이트 하도록 하는 옵션이다. 따라서 이 시리얼라이저는 password를 validated_data에 가지지 않을 수 있다. 하지만 상속받은 Userserializer는 password가 반드시 있음을 상정하므로 오류가 발생하였다.

해결:

validated_data는 dictionary 형태로 저장되어 있었으므로 다음과 같이 .get()메서드를 이용하면 오류없이 잘 동작한다.

def validate(self, attrs):
        if attrs.get("password") != attrs.get("password2"):
            raise serializers.ValidationError(
                {"password": "Password fields didn't match."}
            )
        return super().validate(attrs)

 

2. 객체지향 정리(https://spear-mans-dev.tistory.com/41)

 

배운점

세션로그인과 토큰 로그인의 차이에 대해 실질적으로 경험해 보았다.

객체지향의 세부 개념들에 대해 알게되었다.

 

레퍼런스

https://medium.com/django-rest/logout-django-rest-framework-eb1b53ac6d35

 

JWT Logout — Django Rest Framework

Build a Product Review Backend with DRF — Part 10

medium.com