[1차 프로젝트] Market-kurly User 로그인 회원가입 구현하기

ybear90·2020년 3월 8일
0

Projects

목록 보기
2/3

1차프로젝트(2.24 ~ 3.6)간 marke kurly 백엔드 클론을 진행하는 도중 기억에 남는 코드가 있어 여기에 기록해 본다.

sign-in/sign-up 구현하기

어떤 웹서비스던 회원가입 및 로그인 절차가 없을 수가 없다. 흔한 서비스인 만큼 항상 구현되어야 하는 부분이며 해당 서비스 구현에 익숙해 진다면 간단하게 먼저 구현할 정도로 쉬운 부분일 수 있으나, 보안문제등을 고려하게 되어 여러 조건을 고려한다면 처음에는 조금 어려울 수 있다고 생각한다.

Aquerytool로 작성한 아래 DB 모델을 참고하여 아래와 같이 DB 모델을 구성했다.

from django.db import models

# Create your models here.
class User(models.Model):
    account     = models.CharField(max_length=45, null=False, unique=True)
    grade       = models.ForeignKey('Grade', models.SET_NULL, blank=True, null=True)
    password    = models.CharField(max_length=300)
    name        = models.CharField(max_length=45)
    email       = models.EmailField(max_length=100, null=False, unique=True)
    phone       = models.CharField(max_length=15)
    gender      = models.ForeignKey('Gender', models.SET_NULL, blank=True, null=True)
    birthday    = models.CharField(max_length=15)
    created_at  = models.DateTimeField(auto_now_add=True)
    updated_at  = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'

class Gender(models.Model):
    name        = models.CharField(max_length=10)

    class Meta:
        db_table = 'genders'

class Grade(models.Model):
    name        = models.CharField(max_length=10)
    info        = models.CharField(max_length=45)
    percentage  = models.DecimalField(max_digits=3, decimal_places=1)

    class Meta:
        db_table = 'grades'

class Address(models.Model):
    user            = models.ForeignKey('User', on_delete=models.CASCADE)
    address         = models.CharField(max_length=200)
    is_capital_area = models.BooleanField(default=False)

    class Meta:
        db_table = 'addresses'

회원등급은 한 등급당 여러 회원이 해당될 수 있는 1:n 관계가 형성 되어 있으므로 users에서 grades로 참조가 일어나게 FK(ForeignKey)를 설정해 주었으며 주소 또한 한 회원이 여러 개의 주소를 가지고 있을 수 있으므로 1(회원):n(주소)참조관계를 갖도록 하였다. 성별 또한 따로 테이블을 형성하여 n(회원):1(성별) 관계를 형성하는 식으로 처리하여 데이터의 중복을 최대한 피하려고 모델을 구성하였다.

이를 바탕으로 회원가입 및 로그인에 대한 End-point구현을 다음과 같이 진행했다.

import json
import bcrypt
import jwt
import re

from .models                        import User, Gender, Grade, Address
from .utils                         import login_required

from WeketKurly_backend.settings    import SECRET_KEY, ALGORITHM
from django.views                   import View
from django.http                    import HttpResponse, JsonResponse
from django.db                      import transaction
from django.core.validators         import validate_email
from django.core.exceptions         import ValidationError

# Create your views here.
class SignInView(View):
    def post(self, request):
        user_data = json.loads(request.body)

        try:
            if User.objects.filter(account=user_data['account']).exists():
                account = User.objects.get(account=user_data['account'])

                if bcrypt.checkpw(user_data['password'].encode('utf-8'), account.password.encode('utf-8')):
                    token = jwt.encode({'account_id' : account.id}, SECRET_KEY, algorithm=ALGORITHM)
                    
                    return JsonResponse({'token': token.decode('utf-8')}, status=200)
                
                return HttpResponse(status=401)

            return HttpResponse(status=401)
        
        except KeyError:
            return HttpResponse(status=400) 


class SignUpView(View):
    def check_capital_area(self, area):
        for capital in ['서울', '경기', '인천']:
            if capital in area:
                return True
        return False

    def invalid_password(self, password):
        pattern1 = r"^(?=.*[\d])(?=.*[A-Za-z])(?=.*[!@#$%^&*()_+={}?:~\[\]])[A-Za-z\d!@#$%^&*()_+={}?:~\[\]]{10,}$"
        pattern2 = r"^(?=.*[\d])(?=.*[A-Za-z])[A-Za-z\d]{10,}$"
        pattern3 = r"^(?=.*[\d])(?=.*[!@#$%^&*()_+={}?:~\[\]])[\d!@#$%^&*()_+={}?:~\[\]]{10,}$"
        pattern4 = r"^(?=.*[A-Za-z])(?=.*[!@#$%^&*()_+={}?:~\[\]])[A-Za-z!@#$%^&*()_+={}?:~\[\]]{10,}$"

        if re.match(pattern1, password) or re.match(pattern2, password) or re.match(pattern3, password) \
           or re.match(pattern4, password):
            if re.search(r"^(\d)(\1{2})", password):
                return True
            return False
        
        return True


    def invalid_account(self, account):
        pattern1 = r"^(?=.*[\d])(?=.*[A-Za-z])[A-Za-z\d]{6,}$"
        pattern2 = r"^(?=.*[A-Za-z])[A-Za-z]{6,}$"

        if re.match(pattern1, account) or re.match(pattern2, account):
            return False
        return True

    def invalid_phone(self, phone):
        if re.match(r"^\d{3}?\d{4}?\d{4}$", phone):
            return False
        return True


    def post(self, request):
        user_data = json.loads(request.body)
        
        try:
            if User.objects.filter(account=user_data['account']).exists():
                return HttpResponse(status=400)
           
            if self.invalid_account(user_data['account']):
                return HttpResponse(status=400)

            validate_email(user_data['email'])

            if self.invalid_password(user_data['password']):
                return HttpResponse(status=400)

            if self.invalid_phone(user_data['phone']):
                return HttpResponse(status=400)
            
            if user_data['name'] is None:
                return HttpResponse(status=400)

            with transaction.atomic():
                password = bcrypt.hashpw(user_data['password'].encode('utf-8'), bcrypt.gensalt())

                user_model = User(
                    account=user_data['account'],
                    grade=Grade.objects.get(id=1),
                    password=password.decode(),
                    name=user_data['name'],
                    email=user_data['email'],
                    phone=user_data['phone'],
                    gender=Gender.objects.get(name=user_data['gender']),
                    birthday=user_data['birthday']
                )

                user_model.save()

                # capital area check
                Address(
                    user=User.objects.get(id=user_model.id),
                    address=user_data['address'],
                    is_capital_area=self.check_capital_area(user_data['address'])
                ).save()

                return HttpResponse(status=200)
        
        except KeyError:
            return HttpResponse(status=400)

        except ValidationError:
            return HttpResponse(status=400)




class CheckAccountView(View):
    def post(self, request):
        user_account = json.loads(request.body)

        if User.objects.filter(account=user_account['account']).exists():
            return JsonResponse({'message': 'INVALID_ID'}, status=400)
        else:
            return HttpResponse(status=200)

class CheckEmailView(View):
    def post(self, request):
        user_email = json.loads(request.body)

        if User.objects.filter(email=user_email['email']).exists():
            return JsonResponse({'message': 'INVALID_ID'}, status=400)
        else:
            return HttpResponse(status=200)

비록 회원가입/로그인, 그리고 계정 및 이메일에 관한 중복체크와 같이 실제로는 간단하게 서비스 되는 기능이지만 하나하나의 기능을 제대로 구현하기 위해선 기존에 연습했던 방식과는 다르게 특히 회원가입 부분에 신경써야 할 부분들이 많았다. 계정으로 유효한 조건을 고려하고 중복을 피해야 하는게 요지인데, 실제 kurly 회원가입 페이지에 나온 조건을 더해주기 위해 정규표현식을 익혀서 사용해야 했다.

또한 회원가입 폼에 유효하지 않은 input이 들어갔을 경우 함부로 DB에 저장되어선 안되기 때문에 transaction 처리를 통해 제대로 가입이 이뤄 졌을 때만 DB에 저장될 수 있게끔 처리했다. 그 외에 각각에 회원정보 입력 조건으로 고려하여 class method등으로 조건을 처리할 수 있게끔 하였다.

httpie등을 통해 간이 테스트를 진행하였고 실제 FE 부분과 테스트를 할 때도 처음에는 시행착오가 있었지만 실제 market kurly 회원가입과 거의 유사하게 작동되었다.

추가적으로 social login(naver, kakao)등도 구현하여 추가할 수 있다면 실제 서비스와 유사한 sign-up/sign-in 엔드포인트가 될 것 같다.

Reference

  1. https://stackoverflow.com/questions/10294626/regex-to-check-for-4-consecutive-numbers
  2. https://stackoverflow.com/questions/49322409/username-validation-in-python-regex
  3. https://stackoverflow.com/questions/12018245/regular-expression-to-validate-username
  4. https://stackoverflow.com/questions/3217682/checking-validity-of-email-in-django-python
profile
wanna be good developer

0개의 댓글