이번 일차는 약 3~4일 간 토큰, 데코레이터와의 기나긴 싸움이었다고 볼 수 있었다. 정확히 끝난 시점은 오늘이고 모든 것이 해결되면 한 번에 정리해서 블로깅하기 위해 지금껏 미뤄왔다.
4주차 첫 날 오후 세션은 프론트엔드 분들과 신호를 맞추는 시간을 가졌다. 즉 프론트 엔드 측에서 알려준 서버 ip에 회원 가입 및 로그인을 백엔드가 설정한 엔드포인트로 보내주면, 가입이 제대로 되었는지, 또 로그인이 제대로 되었는지를 눈으로 직접 확인할 수 있었다. 로그인 과정에서 백엔드는 프론트엔드에게 인가를 위한 '토큰'을 발행해줘야 하는데 여기서 '토큰'이라는 것을 왜 알아야 하는지를 체감할 수 있었다.
그렇다면 토큰은 왜 쓸까? 그 답을 알기 위해서는 http가 어떤 구조를 가지고 어떻게 움직이는지부터 알아야 한다. 그냥 인터넷 검색하고, 접속만 되는지 알고, s가 붙는지 안 붙는지만 알면 되는거 아닌가? 아니다. http 통신이 어떻게 되는지 전혀 이해하지 못 하고 토큰을 알기 위해 공부한다면 아마 이렇게 될 것이다.
(먼저 http 용어의 개념을 모르는 것이 아니므로 패스.)
여기에 내가 정리하고 싶은 내용은 http가 통신될 때 어떤 구조로 어떤 자료를 어떻게 담아서 왔다 갔다 하냐는 것이다. 근데 그 전에 http가 어떤 성질을 지녔는지 이해하고 넘어가야 하는데 다음과 같다.
이제 형태에 대해서는 알았다. 그럼 정보를 어떻게 담아주는지 확인해봐야 할 차례다. 요청과 응답의 형태임을 안다 하더라도 그 상세 과정을 모르면 쓸 수가 없다. 이번 기회에 정리하자면 다음과 같다.
3-1. 요청 : 프론트가 보낸다 생각하는 과정
< start line > GET/login HTTP/1.1
Body: {
"user_email": "jun.choi@gmail.com"
"user_password": "wecode"
}
3-2. 응답
< status line >
< body >
ex) 로그인 요청에 대해 성공했을 때 응답의 내용
Body: {
"message": "SUCCESS"
"token": "kldiduajsadm@9df0asmzm" (암호화된 유저의 정보)
}
자바스크립트와 소통해야 하니까 중간 다리로 보내는게 JSON
error message 등 자세한 사항도 있지만 토큰을 위해 알아야하는 구조는 이 정도다.
http가 어떻게 전달되고 받는지 인증 인가를 모르는 상태에서, 백엔드가 보면 굉장히 별 내용이 없는 것 같다.(나만 그런가?) 하지만 인증 인가를 공부하고 토큰의 개념이 들어가기 시작하면 http의 정보 전달 과정이 얼마나 중요한지 알게 된다.
인증 인가에 대한 개념은 알고 있으니 회원 가입 및 로그인 플로우를 통해 인증인가가 어떻게 적용되는지 서술해보도록 하겠다.
<인증 및 인가>
플로우만 알면 별게 없다. 근데 이렇게 끝나면 참 쉬울 것이다. 인증 및 인가에서 중요한 포인트가 있는데 다음과 같다.
인증에서 중요한 포인트1 : "암호화"
인증 및 인가 정리
암호화는 자료 1~4 부분을 통해 다행히 어렵지 않게 이해할 수 있다.(??)
인증에서 중요한 포인트2 : "토큰", JWT
토큰은 흔히 버스 토큰 등 동전을 생각하면 되는데 웹 상에서 토큰은 말 그대로 사용자가 어떤 상황에 접근하기 위해 건내주어 권한을 받기 위한(access token) 어떤 수단이다.(돈 내는 것?)
그렇다면 JWT는 무엇일까? JSON Web Tokens의 준말로 엑세스 토큰 생성의 여러 방법 중 하나다.
< 추가로 알았어야 할 부분 >
플로어도 알고, 토큰이 뭔지도 알았고, 암호화가 어떻게 진행되어 서버에 저장되는지도 알았다. 문제는 토큰을 혼자서 어떻게 받아서 실습하고, 그게 제대로 암호화가 되었는지 아는 것이었다.
주로 HTTPIE를 이용했으므로 이를 기준으로 하면
정도다.
실제 실습 시 토큰은 암호화 인코드 때 bcrypt 양식을 통해 만들었고 인코드 디코드 하는 로직을 이해한 다음 httpie에 직접 토큰을 알아내어 GET, POST 해보니 바로 답이 나왔다. 정확한 내용은 너무 많아 코드를 모두 옮길 수 없어 뒤에 전체 코드 확인만 참고할 수 있게 하겠다.
무엇을 데코하다. 꾸미다라는 말처럼 데코레이터는 함수를 꾸며주는 애다. 깊게 들어가면 한도 끝도 없으니 예제 하나만 보고 넘어가도록 하겠다.
def trace(func):
def wrapper():
print(func.__name__,1)
print(func.__name__,2)
print(func.__name__,3)
return wrapper
@trace
def hello():
print('hello')
@trace
def world():
print('world')
hello()
world()
위에 만들어진 함수 trace는 wrapper라는 내용물 안의 것으로 다른 함수들의 내용을 건들지 않고 꾸며줄 것인데 위의 예시를 아래 hello, world 함수를 각각 꾸미면 프린트가 3줄씩 나온다. 대략 이런 기능을 하는 함수다.
만일 위의 래퍼말고 아래처럼 인자가 주어진 함수면 인자를 맞춰주면 된다.
def trace(func):
def wrapper(a, b):
r = func(a, b)
print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))
return r
return wrapper
@trace
def add(a, b):
return a + b
print(add(10, 20))
꾸며주는 함수의 양식은 다양하므로 상황에 맞게 사용하면 되는데 여기서는 로그인 후 클라이언트가 토큰을 건내줄 때마다
1. 토큰이 제대로 보내졌는지?
2. 맞다면 입장시켜줄 수 있게 views.py의 내용을 수정
하는 일을 할 것이다.
지금껏 이론 정리로 살펴본 내용을 직접 코드화 시켰을 때 어떻게 나오는지 정리해봤다. 자세한 내용보다는 핵심 부분 위주로만 추렸다.
...
(중략)
import jwt
....
byted_password = data['password'].encode('utf-8')
# 먼저 패스워드 정보를 인코드 해주고
hash_password = bcrypt.hashpw(byted_password, bcrypt.gensalt()).decode()
# 비크립트로 해쉬화 해준 뒤 디코드 해준다. 디코드 안하면 나중에 못 찾는다.
password = hash_password
# 이후 패스워드를 변수에 넣어주면 끝
user = User.objects.get(email=data['email'])
# user는 객체다
user_id = user.id
# 객체에 .~하면 바로 내용을 꺼낼 수 있다.
except User.DoesNotExist:
return JsonResponse({"message":"USER_DOES_NOT_EXIST"}, status=400)
if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
# 만일 패스워드 체크 시 입력한 인코딩 비밀번호와 서버 인코딩 비밀번호가 같다면
token = jwt.encode({'user_id' : user_id}, SECRET_KEY, algorithm="HS256")
# 토큰 발행. jwt 공식 문서 등 참고
# 유저의 id임을 꼭 명심해야 한다.(여기 바꾸는데 한참 걸렸다.)
class TokenCheckView(View):
def post(self,request):
data = json.loads(request.body)
# 값을 json 바디로 받아오고
user_token_info = jwt.decode(data['token'], SECRET_KEY, algorithms='HS256')
# 유저 토큰 정보를 디코드해서 받아온다.
if User.objects.filter(email=user_token_info['email']).exists():
# 그래서 유저 객체가 있으면 ~
return JsonResponse({"message": "SUCCESS"}, status=200)
return JsonResponse({"message":"INVALID_USER"}, status=401)
def TokenCheck(func):
def wrapper(self, request, *args, **kwargs):
# 리퀘스트해서 토큰 까는 애
# 토큰의 핵심은 공개가 되어도 된다 -> 그래서 예제가 user_id였던 것.
try:
token = request.headers.get('Authorization')
# 토큰을 Authorization (위에서 본 그거)으로 가져오는데 위치를 잘 봐야한다. 헤더다.
if token:
payload = jwt.decode(token, SECRET_KEY, algorithms="HS256")
user_id = payload['user_id']
user = User.objects.get(id=user_id)
# user_id 가져온 것을 user라는 변수에 담고
# 로직 잘 보기. if 한 번으로 모든 것이 해결.(elif 지양)
# 페이로드에서 디코드해서 값을 변수에 저장하고 유저는 그 아이디값으로 가져온다.
request.user = user
# 변수에 담은 것을 뒤에서 부를 request.user에 또 담아준다.
# 이 과정이 데코레이터를 사용하기 위한 결정적인 부분이다.(위에서 정의한 내용을 이렇게 담고 리퀘스트를 통해서 다른 뷰를 통제할 수 있게 되니 다른 뷰에서도 사용자가 접근할 때마다 토큰을 제시하고 까고 할 수 있게 되는 것.
return func(self, request, *args, **kwargs)
# 데코레이터 펑션은 어떤 것을 받을지 모르니 이렇게 하기.
return JsonResponse({"message":"GIVE_ME_TOKEN"}, status=400)
except jwt.InvalidTokenError:
return JsonResponse({"message":"YOUR_TOKEN_ERROR"}, status=400)
class PostingView(View):
@TokenCheck
# 데코레이터로 유저의 토큰을 깐 상태
def post(self, request):
#딕셔너리 모양으로 받아들여진 정보(httpie에 입력한 정보)
data = json.loads(request.body)
user = request.user
# 위에서 살펴본 request.user로 토큰 깐 상태로 받아들여진 상태.
posting = Posting.objects.create(
img_url = data["img_url"],
user = user
)
return JsonResponse({"message" : "SUCCESS"}, status=200)
많은 과정이었다. 다 기억나지 않을 정도로 수많은 에러가 발생했고 위에 있는 내용만 이번주 내내 진행했다. 한 줄 한 줄 생각하고, print해보고, shell에 쳐보며 디버깅 하는 것에 대해 배웠고 어떤 인자가 왜 이렇게 사용되는지도 많이 이해하게 되었다. 완성되었으니 지속적으로 복습하고(특히 인증인가부분) 다음주 프로젝트 잘 진행해야겠다.
참고 블로그