SignUp , SignIn , Jwt token // django 인스타그램 api(1)

2022. 4. 24. 20:30
  • django 인스타그램 api 시리즈
  • json통신을 상정한 인스타그램의 백엔드적 기능을 구현
    • SignUp, SignIn
    • Jwt 토큰 확인을 위한 decorator

1. User : signup

  • 유저 모델링
  • 유저와 관련된, signup 기능을 구현

 

Coreapp : TimeStampModel

  • 실제로 기능을 구현하기에 앞서서 프로젝트 전반에서 자주 사용될 부분을 따로 모아두기 위한 coreapp을 만듬
  • 생성시각 및 업데이트 시각을 기록하기 위한 TimeStampModel을 생성(이 모델을 다른 앱에서 상속받아 사용)
  • meta속성에 abstract = True를 줌으로써 해당 모델이 상속받을 수 있게 함
#core:models.py
from django.db import models

class TimeStampModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

 

User modeling

  • Timestamp 모델을 상속 > user의 생성시각 및 업데이트 시각 확인
  • 비밀번호의 경우, 암호화를 위해 max_length를 넉넉히 주는 것이 좋다
    • 참고) max_length는 허용된 byte크기를 나타내며, UTF-8방식의 유니코드 인코딩에서 알파벳은 1byte 한글은 3byte가 요구된다. 한글 열자를 집어넣고 싶으면 max_length는 최소 30이상이 필요하다
    • https://namu.wiki/w/UTF-8
from django.db import models
from core.models import TimeStampModel

# Create your models here.
class User(TimeStampModel):
    name = models.CharField(max_length=50)
    email = models.EmailField(max_length=50,unique=True)
    password = models.CharField(max_length=200)
    phone_number = models.CharField(max_length=20)

    class Meta:
        db_table='users'

 


View : Signup

  • post메서드로 email , password, name, phone_number에 대한 정보를 받을 것을 상정
  • validation파일을 만들어, 입력받은 email, password, name에 대해서 유효성 검사를 진행
  • 추가로, email의 경우 db에 중복된 값이 존재하는지 확인하여 중복되지 않도록 하였음
    • 이때 exists를 사용하였음. exists는 get을 통해 선택된 객체에는 사용할 수 없고, 오로지 queryset인 filter에만 사용할 수 있는 함수.
    • filter가 queryset을 반환한다는 것을 생각해보면, 해당 filter에 연동되는 sql문을 실행한 결과가 어떤 객체도 선택되지 않은 빈값일 수도 있다는 것을 상정하고, 해당 queryset에 포함된 객체가 있는지를 확인하는 느낌
  • bcrypt를 사용해 비밀번호 암호화(참고 : https://wannabehumblebee.tistory.com/120)
    • 비밀번호를 db에 저장할 때, 암호화된 비밀번호를 다시 decode시켜서(즉 str로 바꿔서) 저장해준다
#views.py : SignUpView
class SignUpView(View):
    def post(self,request):
        data = json.loads(request.body) 

        try:
            entered_email        = data['email']
            entered_password     = data['password']
            entered_name         = data['name']
            entered_phone_number = data['phone_number']

            validated_password = validate_password(entered_password)
            validated_email = validate_email(entered_email)
            validated_phone_number = validate_phone_number(entered_phone_number)
        
            if User.objects.filter(email=entered_email).exists():
                return JsonResponse({"message":"ALREADY_EXISTED_EMAIL"},status=409)

            encrypted_password = bcrypt.hashpw(validated_password.encode('utf-8'),bcrypt.gensalt()).decode('utf-8')

            User.objects.create(
                name         = entered_name,
                password     = encrypted_password,
                email        = validated_email,
                phone_number = validated_phone_number,
            )
            return JsonResponse({'messasge':'user_created'}, status=201)

        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"},status=400)
        except ValidationError as error:
            return JsonResponse({"message": error.messages}, status=409)

 

Validation.py

  • 비밀번호, 이메일, 전화번호의 유효성검사를 하기 위한 validation함수를 모아둠
  • re.search를 사용하면, 검사하려는 문자열에 정규표현식이 일치하는 구간이 있는지를 확인하는 것임
    • 따라서 일치하는 구간 + 앞뒤로 이상한 내용이 추가될 수 있음
    • 그러나 정규표현식의 첫 부분과 끝 부분에 문자열의 처음과 끝을 의미하는 ^와 $를 붙임으로써 , 검사하려는 문자열 전체가 정규표현식과 일치되도록 하였음
    • 정규표현식 참고 사이트 : https://yozm.wishket.com/magazine/detail/1197/
  • 입력받은 값이 유효하지 않을 경우, validationerror를 발생시키며 validationerror가 받은 문자열은 messages라는 이름으로 저장됨
    • 이렇게 저장된 값은 ~.messages 와 같은 식으로 꺼낼 수 있다.(바로 위의 SignupView의 끝 부분)
import re
from django.http            import JsonResponse
from django.core.exceptions import ValidationError

REGEX_PASSWORD = '^(?=.*[\d])(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*()])[\w\d!@#$%^&*()]{8,}$'
REGEX_EMAIL = r'^\w+[\.\w]*@([0-9a-zA-Z_-]+)(\.[0-9a-zA-Z_-]+){1,2}$'
REGEX_PHONE_NUMBER = r'^\d{3}-\d{3,4}-\d{4}$'

def validate_password(password):
    if not re.search(REGEX_PASSWORD,password):
        raise ValidationError('INVALID_PASSWORD')
    return password

def validate_email(email):
    if not re.search(REGEX_EMAIL,email):
        raise ValidationError('INVALID_EMAIL_ADDRESS')
    return email

def validate_phone_number(phone_number):
    if not re.search(REGEX_PHONE_NUMBER,phone_number):
        raise ValidationError('INVALID_PHONE_NUMBER')
    return phone_number

 


 

2. User : signin

  • signin을 위한 view 구현
  • 로그인시 jwt token의 발행을 위한 로직이 함께 들어 있음

 

Views.py

  • json통신을 통해 email과 password를 받는다고 상정
  • 비밀번호 확인 과정
    • 받은 email을 가진 유저를 식별(이메일은 고유값임)하고, 식별된 유저의 db에 저장된 비밀번호를 가져옴
    • db에서 가져온 비밀번호와 통신으로 받은 비밀번호가 일치하는지 bcrypt.checkpw를 통해 확인
    • 이때, bcrypt를 사용하기 위해 받은 비밀번호와 db에서 가져온 비밀번호 모두 encoding이 적용된다
    • 비밀번호가 일치할 경우, jwt토큰을 발행하여 응답메세지에 담아서 보냄
  • jwt 토큰 발행
    • 일부러 비밀번호 확인 로직 뒤에 배치함. 비밀번호가 불일치 할 경우, 굳이 jwt토큰을 만들 필요 없으니까
    • token의 내용으로 user.id + 'exp'라는 이름으로 token의 만료시간을 함께 보냄. 'exp'라는 키에 만료시간을 보낼 경우, 나중에 pyjwt가 자동으로 만료시간을 체크하여 만료되었을 경우 에러를 이르킴
    • 만료시간의 기준점은 datetime.utcnow()함수를 써서, UTC를 기준으로 하는 게 무조건 무조건 좋다. settings.py에서 설정할 수 있는 장고의 기준시간 설정에 영향을 받지 않기 위함
#views.py:SignInView
class SignInView(View):
    def post(self,request):
        data = json.loads(request.body) 
        try:
            email                 = data['email']
            password              = data['password']
            user                  = User.objects.get(email=email)
            user_saved_db         = user.password
			
            #jwt_token을 위한 로직
            current_time          = datetime.utcnow()
            expiration_time       = timedelta(seconds=300)
            token_expiration_time = current_time+expiration_time
            jwt_access_token = jwt.encode({'id':user.id,'exp':token_expiration_time},SECRET_KEY,algorithm=ALGORITHM)            
			
            #password확인 로직
            if bcrypt.checkpw(password.encode('utf-8'),user_saved_db.encode('utf-8')):
                return JsonResponse({'messasge':'SUCCESS','JWT_TOKEN':jwt_access_token}, status=200)
            return JsonResponse({"message":"INCORRECT_PASSWORD"},status=401)

        except KeyError:
            return JsonResponse({"message":"KEY_ERROR"},status=400)
        except User.DoesNotExist:
            return JsonResponse({"message":"NOT_REGISTERED_EMAIL"},status=401)

 


 

3. JWT Token decorator

  • 로그인이 필요한 기능에서, jwt token의 유효성을 검사하는 decorator를 구현
  • 추가적으로, jwt token에 user정보가 들어있기 때문에, user를 식별할 수 있는 수단이 되기도 한다.
  • jwt의 유효성을 검증해주는 데코레이터는 어디서든 사용 >> 프로젝트 전반에 공통적으로 사용되는 coreapp에 구현

 

decorators.py

  • jwt토큰의 생성 및 검즈에 필요한 Algorithm이나 Secret_key는 숨겨야 함
  • 데코레이터 이므로 클로저형태로 작성하였음(당연)
    • 이때 데코레이터에 원래 함수가 받는 인자가 적용되는 이유가 좀 궁금했는데(여기선 self와 request), 어찌보면 당연한 것임
    • 클래스의 메서드를 인스턴스가 사용할 때, 메서드에 데코레이터가 적용되어있다면 해당 데코레이터가 먼저 사용되는 것임
    • 따라서 원래 메서드가 인자로 받는 것들(self,request)이 데코레이터 인자로 무조건 들어오게 되어 있음
    • 따라서 반대로 데코레이터에서 func()를 호출해줄 때도 인자를 그대로 넘겨줘야 하며, 이때 넘겨주는 인자의 값을 변화시켜줄 수 있음
    • 아래의 예시에서도, jwt토큰을 해제해서 얻은 user.id를 활용해 user객체를 잡아서 self.user라는 이름에 담고, self.user값이 변화된 self를 원래의 함수에 담아줌
    • 그러면 기존에 호출하려는 함수(아래의 예시에서는 func)에서 self.user로 접근하면, 데코레이터에서 저장된 user객체가 담겨져 있음
  • 로그인 과정에서 jwt토큰에 'exp'라는 이름의 키에 만료시간을 담았다면, pyjwt에서 자동으로 현재 시간이 만료시간을 초과했는지를 확인해 준다. 만료되었을 경우 Expired_Signature 에러를 발생시킴
#coreapp:decorator.py

import jwt
from django.conf import settings
from my_settings import ALGORITHM
from users.models    import User
from django.http import JsonResponse


#wrapper가 감싸는 변수 자체가 클래스.데코레이터(메소드), 데코레이터에 self나 request등을 줄 수 있다.

def access_token_check(func):
    def wrapper(self,request,*args,**kwargs):
        try:
            access_token = request.headers.get('Authorization')
            payload      = jwt.decode(access_token, settings.SECRET_KEY,ALGORITHM)
            
            #함수(func)에 유저객체를 담아보내기 위한 것
            self.user = User.objects.get(id = payload['id'])

            return func(self,request,*args,**kwargs)

        except User.DoesNotExist:
            return JsonResponse({'message' : 'invalid_user'}, status=401)
        
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message' : 'Expired_Signature'}, status=401)
            #exp값이 만료되었을 때(exp에 적힌 값이 현재시간 이후일 때)

        except jwt.DecodeError:
            return JsonResponse({'message' : 'invalid_payload'}, status=401)
            #유효성 검사에 실패하여 토큰을 디코딩 할 수 없을 때, 즉 토큰 자체가 빈 문자열이거나 header혹은 payload가 손상되었을 때 발생, 혹은 시크릿키나 알고리즘이 다를 경우 발생

        except jwt.InvalidSignatureError:
            return JsonResponse({'message' : 'invalid_signature'}, status=401)
            #토큰의 서명이 일부로 제공된 서명과 일치하지 않을 때 발생 > 걍 조금이라도 맛탱이가면 최종적으로 발생하는 듯 함

    return wrapper

BELATED ARTICLES

more