23.04.28 TIL
오늘은...
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