HTTP통신은 각각의 통신의 독립성(stateless)때문에 지금의 통신이 과거의 통신의 정보를 가지고 있지 않다.
그래서 로그인을 했어도 다음 통신에서 유저가 로그인을 했다는 사실을 기억하지 못한다.
따라서, 로그인을 했다는 사실을 각각의 통신마다 담아야한다.
그렇다면 어떤 것이 로그인을 했다는 사실을 증명할까?
여기서 확인했던 jwt 토근이 그 사실을 증명해준다.
endpoint로 request가 들어오면 해당 view의 매서드를 실행시키기 전에 로그인여부를 확인하는 decorator를 만들어보자.
class Profile(View):
@login_required
def get(self, request):
profile = Account.objects.filter(id=request.user.id).values()
return JsonResponse({'profile' : profile[0]}, status = 200)
프로필 endpoint로 로그인 한 유저만 접근가능한 페이지이다. get매서드가 실행되기 전 login_required라는 데코레이터가 먼저 실행된다.
로그인 데코레이터는 endpoint를 추가해도 로그인후에 접근가능하다면 걸어줘야하기 때문에 account앱에 utils.py라는 파일을 따로 생성해주어 데코레이터를 관리하도록 한다.
import json, jwt
from .models import Account
from westagram.my_settings import SECRET_KEY
from django.http import JsonResponse, HttpResponse
파이썬 내장 라이브러리, 내가 만든 파일들, django내장 라이브러리를 구분해서 import해준다. settings.py에 있는 SECRET_KEY는 보안상의 이유로 my_settings.py라는 파일을 따로 만들어주어 gitignore로서 관리하도록한다.
def login_required(func):
def wrapper(self, request, *args, **kwargs):
login_required라는 데코레이터를 만들고 데코레이터가 적용된 매서드를 func로 불러온다(이 경우는 accounts.views에 있는 Profile)
그리고 parameter을 받아올 wrapper함수를 만들어준다. 결국 데코레이터도 클래스 밑에서 생성되는 함수이기 때문에 self, http request를 받는 request, 쿼리스트링이나 url parameter을 받아오기위해 *arg, **kwargs를 넣어준다.
access_token = request.headers.get('Authorization', None)
if access_token:
이제 headers에 추가되어 온 Authorization 키의 value값을 get해온다. 여기서 값이 없으면 None을 반환함으로서 값이없을 경우 발생하는 except처리 코드를 줄일 수 있다.
그리고 if access_token은 access_token(가져온 Authorization의 value값 즉, 토큰)이 있으면이라는 뜻이다.(만약 토큰이 없어 None이 access_token에 담길경우의 애러는 추후에 except처리 하도록 한다.) access_token에는 실재 값이 들어가겠지만 이 bool(access_token)의 불리언 타입을 확인하면 True가 나온다.(None의 불리언을 확인하면 False가 나온다) 그래서 if문의 조건으로 바로 사용해준다.
try:
decode = jwt.decode(access_token, SECRET_KEY, algorithm = 'HS256')
이제 들어온 토큰을 decode해서 유저의 정보를 알아낼 차례이다. import 해온 setting.py에서 지급되는 SECRET_KEY를 넣어준다. 그리고 jwt매서드를 사용해서 토큰을 decode해준다.
이과정에서 토큰이 잘못되었다면 DecodeError가 발생할 것이다.
하지만 여기서 의문점이 생길 수 있다. 처음에 로그인으로 encode된 토큰을 발행할 때는 decode해줬었다. 그렇다면 받아온 토큰을 encode해서 byte형태로 jwt매서드에 넣어야 하지 않는가?
답은 byte형태로 넣지않아도 jwt매서드가 값을 뽑아낼 수 있기 때문에 따로 encode해주지 않도록한다.
user = Account.objects.get(id=decode['user_id'])
request.user = user
except jwt.DecodeError:
return JsonResponse({'message' : 'INVALID_TOKEN'}, status = 403)
이제 decode된 토큰값의 id를 데이터베이스의 Account테이블에 있는 id가 1인 객체를 가져와 user변수에 넣어준다.(user변수에 담긴것은 특정 value가 아니라 객체임을 명심하자)
그리고 request객체에 Account테이블에 id가 decode['user_id']인 객체를 넣어준다.
#request객체는 headers, body, startline등으로 이루어져 있다. 그리고 request객체의 특성상 객체를 추가하는 것이 가능하다는 것을 명심하자. 이렇게 request객체에 추가된 user객체는 그대로 데코레이터를 빠져나오는 순간 사라지지않고 views.py에 있는 Profile클래스의 get매서드에 전해진다.
request객체에 들어간 user를 직접 프린트해보자
위의 사진은 accounts.views에서 Profile클래스의 get매서드에서 httpresponse로 reqest.user을 그대로 프린트한 것으로 request에 객체가 추가된 것을 확인할 수 있다.
return func(self, request, *args, **kwargs)
return JsonResponse({'message' : 'LOGIN_REQIRED'}, status = 401)
return wrapper
조건이 맞다면 시작될 때 받아온 parameter들을 전부 다 리턴해주고 마지막으로 wrapper함수를 리턴한다. 리턴값에 포함된 request는 user객체를 가진채로 Profile로 넘겨진다.
이제 login_required의 모든 과정이 통과되었다면 accounts.views로 넘어와서 Profile클래스의 매서드를 실행시킬 차례이다.
def get(self, request):
profile = Account.objects.filter(id=request.user.id).values()
return JsonResponse({'profile' : profile[0]}, status = 200)
login_required 데코레이터에서 request에 추가된 user객체를 이용할 차례이다. user객체의 id값을 필터링해주어 Account테이블에서 가져와 그 values를 Json으로 리턴한다.
위와같이 지급된 토큰에서 decode된 유저 data가 뿌려졌다.
이제 구현된 login_required를 댓글(comment)기능에 적용시켜보자. 로그인을 한 사람만 댓글을 달 수 있는 경우이다.
@login_required
def post(self, request):
data = json.loads(request.body)
Reply.objects.create(
comment = data['comment'],
account_id = request.user.id
)
return HttpResponse(status = 200)
토큰이 통과된 상태에서 user객체가 request에 추가되어 넘어 온 상태이다. 유저가 입력한 comment와 request.user객체에서 id값을 뽑아서 account_id에 저장시킨다.(Reply테이블이 Account테이블을 정참조 하고 있기 때문에 id로 표현된다.)
알맞은 토큰을 지급받아 댓글이 성공적으로 달렸다.
추가적으로 login_required로 제한된 영역은 아니지만 앤드포인트로 갔을 때 참조관계에있는 테이블의 value를 가져오는 것을 확인하기 위해 get요청애 대한 response에 대해 살펴보자.
def get(self, request):
return JsonResponse({'reply' : list(Reply.objects.select_related('account_id').values('account_id__name', 'comment'))}, status = 200)
우선 쿼리로 가져온 값들은 쿼리셋으로 담길 것이기 때문에 json return이 불가능하다는 점은 감안하여 list처리 해준다.
댓글 페이지를 보면 일반적으로 댓글내용과 댓글을 달았는 유저의 이름을 볼 수 있다. 현재 Reply테이블의 유저의 이름부분은 Account테이블의 id를 정참조하고 있기 때문에 Reply테이블 자체의 value를 불러온다면 유저이름 부분에서 Account테이블의 id값밖에 얻을 수 없다.
그렇다면 여기서 실재로 Reply테이블의 comment와 Account테이블의 name의 값을 한번의 쿼리로 불러올 고민을 해야한다.
위의 코드를 분석해보면 Reply테이블에서 account_id(Account테이블의 id값)값이 바라보고 있는 테이블의 값을 cach데이터로 가져와 저장시킨다. 그리고 'account_id__name'에서 뒤에 있는 _name이라는 lookup함수를 이용해 Account 테이블에 있는 name 값을 가져오고, Reply테이블에 있는 'comment' 값을 같이 리턴해준다.
실재로 select_related('account_id')를 사용하지 않아도 값을 lookup으로 값을 가져오는 것이 가능하지만 select_related()를 통해 해당 값의 caching을 통해 쿼리를 줄여줄 수 있음을 명심하자