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
'django > 프로젝트' 카테고리의 다른 글
게시물 기능(게시물 등록, 댓글, 좋아요) // django 인스타그램 api(3) (0) | 2022.04.25 |
---|---|
Follow기능 구현 // django 인스타그램 api(2) (0) | 2022.04.24 |
instagram만들기(3)로그인 / 로그아웃 구현 (0) | 2022.04.07 |
수정필요!)instagram 만들기 (2)회원가입 환영 이메일 보내기 (0) | 2022.04.06 |
instagram 만들기 (1)유저 지정 및 회원가입 (0) | 2022.04.04 |