[WIL #7] DRF 개인프로젝트

안떽왕·2023년 4월 30일
0

Weekly I Learned

목록 보기
7/16

이번 한 주는 drf를 이용한 todo리스트를 만드는 개인프로젝트를 진행했습니다. 이전 프로젝트와의 차이점이라면 로그인 방식을 토큰방식으로 진행한다는게 가장 큰 차이인 것 같습니다.

회원관리 모델

회원가입

user manager

class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError(messages.REQUIRED_EMAIL)

        user = self.model(
            email=self.normalize_email(email),
            **extra_fields
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

장고의 기본 상속클래스인 User모델을 쓰지않고 커스터마이징을 위해 usermanager를 만들었습니다.
normalize_email은 @뒤의 알파벳을 소문자로 바꿔주고
set_password는 입력받은 비밀번호를 해시 값으로 바꿔줍니다.\

user 모델

class User(AbstractBaseUser):
    email = models.EmailField(
        verbose_name="email_address",
        max_length=255,
        unique=True
    )
    name = models.CharField(max_length=50, verbose_name='name')

    GENDERS = (('M', 'Man'), ('W', 'Woman'))
    gender = models.CharField(verbose_name='gender',
                              max_length=1, choices=GENDERS)

    age = models.IntegerField(verbose_name='age', null=True)
    introduction = models.TextField(
        max_length=500, verbose_name='introduction')
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return True

    @property
    def is_staff(self):
        return self.is_admin

기본적인 모델입니다. 성별은 남과 여 둘중 하나만 고를 수 있도록 만들었고, username 표기는 email을 이용하게 했으며, user를 불러올 때 보이는 값이 처음에는 pk키로 되어있는데 보기 편하도록 email이 보일 수 있게 __str__함수를 만들었습니다.

시리얼라이저

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"

    def create(self, validated_data):
        user = super().create(validated_data)
        password = user.password
        user.set_password(password)
        user.save()
        return user

시리얼라이저를 통해 프론트에서 입력받은 데이터를 백엔드에서 처리할 수 있습니다. 주로 JSON데이터를 받아와 장고에서 읽을 수 있도록 가공한다음 데이터를 저장합니다.

views

class UserView(APIView):
    # 회원가입
    def post(self, request):
        serializer = UserSerializer(data = request.data)
        if serializer.is_valid():
            serializer.save()
            return Response({"message":messages.SUCCESS_SIGNUP}, status=status.HTTP_201_CREATED) 
        else:
            return Response({"message":f"${serializer.errors}"}, status=status.HTTP_400_BAD_REQUEST)

설정해둔 api주소에서 post요청이 들어왔을 때, 회원가입을 실행할 수 있도록 설정했습니다. 데이터를 받으면 위에 적어놓은 시리얼라이저를 거쳐 데이터를 변환하고 해당 데이터가 is_valid 유효성 검사를 통과하면 데이터를 저장하게 만들었습니다.

리턴에서 메세지의 경우 작성하다보니 동일한 메세지를 적용하는 경우가 많아 messages.py라는 파일을 만들어 별도로 관리하고 불러오는 방식으로 만들었습니다.

로그인

시리얼라이저

class LoginSerializer(serializers.ModelSerializer):
    email = serializers.CharField()
    password = serializers.CharField()

    class Meta:
        model = User
        fields = ['email', 'password']

    def validate(self, data):
        email = data.get('email')
        password = data.get('password')

        if email and password: 
            user = authenticate(email=email, password=password)
            if user:
                data['user'] = user
            else:
                raise serializers.ValidationError('유저 정보가 일치하지 않습니다.')
        else:
            raise serializers.ValidationError('이메일과 패스워드는 필수 입력사항입니다.')

        return data

로그인에 사용할 시리얼라이저를 만들었습니다. 로그인에는 이메일과 비밀번호를 사용하고 validate함수를 만들어 다수의 필드에 유효성검사를 시행했습니다.

이 유효성 검사를 통과하고 이메일과 비밀번호가 모두 입력된 상태일때 정보를 비교해 데이터가 같으면 로그인할 수 있게 만들었습니다.

views

# 로그인
    def post(self, request):
    	# 유저 인증
        user = authenticate(
            email=request.data.get("email"), password=request.data.get("password")
        )
        # 이미 회원가입 된 유저일 때
        # 정보가 일치하지 않거나 없을경우 user는 None으로 나옴
        if user is not None:
            serializer = LoginSerializer(user)
            # jwt 토큰 접근
            token = TokenObtainPairSerializer.get_token(user)
            refresh_token = str(token)
            access_token = str(token.access_token)
            res = Response(
                {
                    "user": serializer.data.get("email"),
                    "message": messages.SUCCESS_LOGIN,
                    "token": {
                        "access": access_token,
                        "refresh": refresh_token,
                    },
                },
                status=status.HTTP_200_OK,
            )
            # jwt 토큰 => 쿠키에 저장
            # 쿠키에 저장할 경우 httponly로 JS공격으로 부터는 비교적 안전적인편
            # 하지만 csrf공격에 취약하기 때문에 프론트 작성시 csrf토큰을 사용해줘야 한다
            res.set_cookie("access", access_token, httponly=True)
            res.set_cookie("refresh", refresh_token, httponly=True)
            return res
        else:
            return Response({"message":messages.FAIL_LOGIN}, status=status.HTTP_400_BAD_REQUEST)

로그인 방식은 simple jwt를 이용해 토큰 방식 로그인을 구현하고자 하였습니다. 그래서 TokenObtainPairSerializerget_token함수를 이용해 토큰을 발급 받았습니다. 토큰은 2종류가 발급되는데

하나는 자유이용권과 같은 access토큰, 그리고 access토큰을 재발급받기 위한 refresh토큰이 있습니다. 토큰을 발급받으면 string으로 형변환시켜 각 변수에 할당하고 리턴되는 딕셔너리에 기재해줬습니다.

후기

토큰방식으로 회원관리를 만들다가 로그아웃을 어떻게 구현하는지 깊이 고심했습니다. 그래서 쿠키에 집어넣어 로그아웃을 진행할 때 쿠키에 저장된 정보를 삭제하는 방법을 구현하게 되었습니다. 하지만 이 방법으로는 아직 만료되지 못한 access토큰이 살아있다는 것을 깨닫게 되었습니다.

그렇게 알아보다가 토큰을 사용하는 로그인방식을 채택하는 경우 백엔드에서는 로그아웃을 하지않고 프론트에서 처리를 한다는 사실을 알게되었습니다. 이번에는 포스트맨만을 이용한 철저히 백엔드만 개발하는 프로젝트였기에 쿠키의 저장정보를 날린다고 한들 로그아웃이 제대로 실행되는지도 제대로 확인해보지 못했습니다.

다음 프로젝트때는 분명 프론트도 같이 개발하게 될 것 같아 다음 프로젝트가 기대가 됩니다.

profile
이제 막 개발 배우는 코린이

0개의 댓글