27. TIL Westagram (로그인 구현, 비밀번호 암호화, JWT 적용)

dream.log·2021년 7월 28일
3

TIL

목록 보기
26/42
post-thumbnail

오늘은 westagram 관련 두번째 포스팅이다.
이번 과제부터는 수행하는데 조금 힘이 들기도 했고, 이해가 명확하게 되지 않은 부분도 있었다.
그래서 한 번 완성한 후, 초기셋팅부터 다시 만들어보았고 이제야 비로소 조금 더 이해가 된 듯해
포스팅으로 정리하며 복습해보자 !🙂
(TMI : 이 과정에서 westagram2 migration이 잘못돼 westagram3가 되었다^3^)

1. 로그인 구현

로그인을 위해서는 view를 작성해야 한다. 아이디와 비밀번호가 필요하며, 필수 조건은 아래와 같다.

- 계정이나 패스워드 키가 전달되지 않았을 경우, {"message": "KEY_ERROR"}, status code 400 을 반환합니다.
- 계정을 잘 못 입력한 경우 {"message": "INVALID_USER"}, status code 401을 반환합니다.
- 비밀번호를 잘 못 입력한 경우 {"message": "INVALID_USER"}, status code 401을 반환합니다.
- 로그인이 성공하면 {"message": "SUCCESS"}, status code 200을 반환합니다.

- 클라이언트의 요청을 받아서 회원가입 뷰를 호출할 수 있도록 urls.py 를 작성해야합니다. view를 잘 작성하셨다면, 이에 걸맞는 좋은 url을 붙여주는 것 까지가 좋은 엔드포인트를 생성하는 과정입니다. 작성한 함수에 적절하며, 그 의미에 맞는 url을 생성하셔야 합니다.

✅ 작성한 코드 (users/views.py)

import json, re 
# re도 빼먹지 말고 추가해주기

from django.views 		  import View
from django.http  		  import JsonResponse
from users.models 		  import User 

# 정규식 대입 이전 re.compile 넣어주기
password_regular = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$")

class UserView(View): #UserView (model)으로 썼었다. view니까 당연히.. view로 적어주자
	def post(self,request): #self 빼먹지 마세요
		try: #try-except 구문 사용하기. 
			data = json.loads(request.body)
           # 프론트가 보내온 데이터를 json을 통해 {key:value} 형태로 읽어온다

# 두가지 조건을 동시에 적어줄 때는 적절하게 괄호처리를 해주어야 한다.
			if ('@' not in data['email']) or ('.' not in data['email']):
				return JsonResponse ({'MESSAGE':'INVALID_ERROR'}, status= 400)

# if data['password'] != password_regular(Match) 처음 적은 답
			if not password_regular.match(data['password']) : # 수정답.
				return JsonResponse ({'MESSAGE':'INVALID_PASSWORD'})
#이 문장에서의 User는 대문자. filter로 원하는 데이터를 불러오려면 값을 먼저 정의해줘야지!
			if User.objects.filter(email=data['email']).exists():	
				return JsonResponse ({'MESSAGE':'EMAIL_ALREADY_EXISTS'}, status =400)

			User.objects.create (
				name  		 = data['name'],
				email	     = data['email'],
				password 	 = data['password'],
				phone_number = data['phone_number'],
				age 		 = data['age'],
			)

			return JsonResponse ({'MESSAGE':'SUCCESS'}, status = 201)

		except KeyError:
			return JsonResponse ({'MESSAGE':'KEY_ERROR'}, status=400)	

class LoginView(View):
	def post (self,request):
		try:  # data = json.loads 구문 써야 오류가 안난다. 빼먹지 말자
			data = json.loads(request.body)

			if data['email'] == "" or data['password'] == "":
				return JsonResponse ({'MESSAGE':'WRONG_REQUST'}, status=400)

			if not User.objects.filter(email= data['email']).exists() :
				return JsonResponse ({'MESSAGE':'INVALID_USER'}, status=401)
			# 4번단계까지 살아있는 코드 중 get.password (password 꼭 적어줘야한다)
		   if (data['password']) != User.objects.get(password=data['password']).password: 
				return JsonResponse ({'MESSAGE':'WRONG_PASSWORD'}, status=401)

			return JsonResponse ({'MESSAGE':'SUCCESS'}, status = 201)

		except KeyError:
			return JsonResponse ({'MESSAGE':'KEY_ERROR'}, status=400)		

🔥 작성 주안점

1) 이메일 조건 정의해주기 -@나 .이 없는 경우 에러 발생
: 해당 내용을 어떻게 확인하면 좋을까 고민했는데 in을 활용해서 확인할 수 있다는 것을 찾아내고 in을 사용했다. 멘토님 컨펌을 통해 같은 조건은 or을 통해 하나로 합쳐 작성하라고 말씀해주셔서 맞춰 수정했다.
or을 사용할 경우, 괄호를 적절히 사용해 오류가 발생하지 않도록 유의해주어야 한다!

2) 정규식 사용하기
: 이메일을 이미 적었기 때문에 비밀번호에 정규식을 사용하기로 했다. 8자 이상, 문자, 숫자, 특수문자 조건에 해당하는 정규식을 찾아 변수명을 만들어주고, 상단에 기입해주었다. 데이터로 받아온 비밀번호와 정규식의 비밀번호가 일치한지 확인해주는 식을 작성해야 한다. password.regular.match 로 적어야 하는 부분을 괄호로 적어 오류가 발생했었는데 앞 부분과 이어져 값을 받아와야 하는 곳은 .으로 이어서 적어주고, match를 활용했으니 != 가 아닌 괄호 안에 비교할 값을 넣어주면 된다.

3) 로그인 시 값이 입력되지 않으면 에러 출력
: "" 빈 스트링을 활용해 이메일이나 password가 빈 스트링일 경우를 적어주었다. 비교 할 때는 == 인 것을 잊지말고, 정의해둔 데이터값을 바로 불러와 비교식을 작성해주면 된다.

4) 아이디를 잘못 입력한 경우
: 아이디가 존재하지 않는 경우로 조건을 정의해주었다. 비밀번호가 일치하지 않는 경우는 정규식 부분에 이미 정의해주었기 때문에, 아이디만 정의를 해주면 된다. filter-exists 를 사용해 존재 여부를 확인했고, 이메일의 데이터를 불러와야 하기 때문에 email=data['email'] 의 형태로 적어주었다. get은 exists를 사용할 수 없기에 filter로 조건을 잘 맞춰 사용해주는 것이 중요하다!

5) User 객체 생성
6) 성공 JsonResponse 반환

✔️ 피드백 수정내역

  • 같은 조건이거나, 같은 에러메시지를 반환하는 경우 하나로 정리해준다.
  • 올바른 에러 메시지를 걸어준다.

5. 회원가입 로그인 암호화

  • bcrypt를 활용해 작성한 비밀번호가 암호화 될 수 있게끔 한다.

    ✅ 작성한 코드

  import json, re , bcrypt

from django.views 		  import View
from django.http  		  import JsonResponse
from users.models 		  import User 

password_regular = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$")

class UserView(View): 
	def post(self,request):
		try: 
			data = json.loads(request.body) 
			bcrypt_password = bcrypt.hashpw(data['password'].encode('utf-8'),bcrypt.gensalt()).decode('utf-8')
# (password=data['password']) 가 아님 encode 괄호 - decode도 진행해주어야 한다.

			if ('@' not in data['email']) or ('.' not in data['email']):
				return JsonResponse ({'MESSAGE':'INVALID_ERROR'}, status= 400)

			if not bcrypt_password.match(data['password']):
				return JsonResponse ({'MESSAGE':'INVALID_PASSWORD'})

			if User.objects.filter(email=data['email']).exists():	
				return JsonResponse ({'MESSAGE':'EMAIL_ALREADY_EXISTS'}, status =400)

			User.objects.create (
				name  		 = data['name'],
				email	     = data['email'],
				password 	 = bcrypt_password,
				phone_number = data['phone_number'],
				age 		 = data['age'],
			)

			return JsonResponse ({'MESSAGE':'SUCCESS'}, status = 201)

		except KeyError:
			return JsonResponse ({'MESSAGE':'KEY_ERROR'}, status=400)	

class LoginView(View):
	def post (self,request):
		try:  
			data = json.loads(request.body)

			if data['email'] == "" or data['password'] == "":
				return JsonResponse ({'MESSAGE':'WRONG_REQUST'}, status=400)

			if not User.objects.filter(email= data['email']).exists() :
				return JsonResponse ({'MESSAGE':'INVALID_USER'}, status=401)
			
			# user를 정의한다. 불러온 user 객체 중 이메일 데이터로. 비밀번호는 유저에게 받아 인코딩해야함.(비교하기 위해)
			user	 = User.objects.get(email=data['email'])
			password = user.password.encode('utf-8')
			
           			# 잘못썼던 구문: if not bcrypt.checkpw (data['password'], User.objects.get(password=data['password'])): 
			if not bcrypt.checkpw (data['password'].encode('utf-8'), password) :
				return JsonResponse ({'MESSAGE':'WRONG_PASSWORD'}, status=401)

			return JsonResponse ({'MESSAGE':'SUCCESS'}, status = 201)

		except KeyError:
			return JsonResponse ({'MESSAGE':'KEY_ERROR'}, status=400)		

🔥 작성 주안점

1) 상단 bcrypt import

2) Userview try 구문 내부 bcrypt_password 정의
비밀번호를 hash화 하고 encode한다. salting 후 decode 한다. decode까지 해야하나 고민했는데, bcrypt 과정에서 같이 진행해주어야 한다. 대입하는 값은 data['password'] 이다.

3) 암호화 한 비밀번호와 기존 비밀번호 비교
not- match 구문을 활용해 작성한다.괄호 안 비교할 데이터를 넣어준다!

4) user와 password를 login view에서 재정의한다.
: 두번째 적을 때 아예 이 부분을 적지 않았다.. user를 정의하기 위해 상단의 User 객체에서 이메일 정보를 받아온다. password는 user의 password를 받아와 encode해준다. 이 단계를 정의해주어야 비밀번호가 맞는지 검증하는 5단계가 가능하니 까먹지 말자.

5) 암호화된 비밀번호와 유저가 입력한 비밀번호가 일치하지 않는다면?
: not bcrypt.checkpw (data['password'].encode('utf-8'),password)
not을 사용해 에러 메시지를 반환할 준비를 해준다. checkpw 사용하고, 입력받은 비밀번호를 encode 한 값과 기존의 비밀번호를 비교해 일치하지 않는다면 에러메시지를 반환한다.

✔️ 피드백 수정내역

  • user,password 부분 작성내역 수정 및 변수 재정의
  • bcrypt.checkpw 부분 : 비밀번호만 비교하는 것이 아니라, 기존 data['password']의 encoding이 필요하다!

6. JWT 적용하기

✅ 작성한 코드

import json, re , bcrypt, jwt

from django.views 		  import View
from django.http  		  import JsonResponse
from users.models 		  import User 
from westagram3.settings  import SECRET_KEY

password_regular = re.compile("^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$")

... UserView 생략

class LoginView(View):
	def post (self,request):
		try:
			data = json.loads(request.body)

			if data['email'] == "" or data['password'] == "":
				return JsonResponse ({'MESSAGE':'WRONG_REQUST'}, status=400)

			if not User.objects.filter(email= data['email']).exists() :
				return JsonResponse ({'MESSAGE':'INVALID_USER'}, status=401)
		
			user	 = User.objects.get(email=data['email'])
			password = user.password.encode('utf-8')
			
			if not bcrypt.checkpw (data['password'].encode('utf-8'), password) :
				return JsonResponse ({'MESSAGE':'WRONG_PASSWORD'}, status=401)

			# access_token의 key값은 id 이다. 'email'이라 적음. 토큰값 반환하며 성공 메시지도 같이 돌려줘야함
			access_token = jwt.encode({'id': user.id}, SECRET_KEY, algorithm='HS256')
			return JsonResponse ({'access_token':'token'},{'MESSAGE':'SUCCESS'} , status=201)	
		#	return JsonResponse ({'MESSAGE':'SUCCESS'}, status = 201)

		except KeyError:
			return JsonResponse ({'MESSAGE':'KEY_ERROR'}, status=400)		

🔥 작성 주안점

1) jwt import
settings.py에서 SECRET_KEY를 받아와야 하기 때문에 from-import로 파일 연결

2) access_token 발급 내역 적기
: jwt.encode({'id':user.id} , SECRET_KEY, algorithm='HS256')

불러올 id값을 적어주어야 하는데, 유저가 id로 입력한 값이기 때문에 user.id를 불러준다. SECRET_KEY를 불러온 후 알고리즘을 정의해준다. id값을 불러오는 과정에서 email을 입력했었는데, 안된다는 것을 확실히 배웠다. email에 대한 검증은 완료되었기 때문에 id로 값을 불러오는 것이 데이터적인 면에서도 효율적이며, 무엇보다 토큰 탈취 우려가 있기 때문에! ⚠️ 탈취되어도 안전한 pk값을 불러오는 것이 좋다!
(해당 부분에 대해서는 개념이 헷갈렸는데, 코드카타 짝꿍 병주님이 친절하게 설명해주셔서 이해하고! 멘토 연우님께 한번 더 여쭤보고 확실히 알게 되었다 🙂)

3) 성공 메시지에 토큰 반환하기
: 기존 SUCCESS_MESSAGE에 딕셔너리 형태로 token을 반환한다.
{'access_token' : 'token'}


★ users/urls.py 작성하기

from django.urls import path
from .views import UserView, LoginView

urlpatterns = [ 
	path ('', UserView.as_view()),
	path ('/login',LoginView.as_view()),
]

★ westagram/urls.py 작성하기

from django.urls import path, include


urlpatterns = [
    path('users', include('users.urls'))
]

✔️ urls를 작성할 때는 해당 주소가 프론트와 소통할 수 있는 직접적 창구이므로
직관적으로 이름을 지어 헷갈리지 않게 해주는 것이 중요하다!
urls.py 파일을 작성하는 것을 까먹거나 올바르지 않게 작성하는 경우가 간혹 있었는데,
해당 부분도 익숙해질 필요가 있다!


westagram 필수 과제가 드디어 끝났다 !
한 번 완성하고, 다시 반복해서 만들어보니 확실히 더 많이 이해가 된 것 같아 뿌듯하다.
아직 bcrypt와 jwt의 개념 및 view를 문법에 맞추어 짜는게 익숙치 않지만,
반복을 거듭하다보면 실력도 늘고, 실수도 줄어들겠지...!

오늘 프론트랑 연결해보고, 지금 헷갈리는 개념들과 접목해서 공부해봐야겠다.

profile
한 걸음, 한 걸음 포기하지 않고 발전하는 Backend-developer 👩🏻‍💻 노션 페이지를 통한 취업 준비 기록과 회고를 진행하고 있습니다. 계획과 기록의 힘을 믿고, 실천하고자 합니다.

2개의 댓글

comment-user-thumbnail
2021년 7월 29일

🔥🔥🔥🔥🔥

1개의 답글