지난 시간에는 게시물 등록 기능을 구현하였습니다. 이번에는 권한이 있는 유저만이 서비스를 사용할 수 있는 인가 과정을 구현하겠습니다. 구체적으로 로그인한 유저만 게시물을 등록할 수 있도록 로그인 인증 데코레이터를 만들고, 게시물 기능에 적용해보겠습니다.
웹 사이트를 이용할 때, 우리는 이 여러 기능을 매번 로그인 하지 않고 사용합니다. 사실, HTTP는 stateless한 특성이 있어서 이전의 요청과 응답을 기억하지 못합니다. 어떻게 웹사이트는 내가 로그인한 사용자인지 알 수 있을 까요?
그 비밀은 HTTP Request Headers에 있습니다. 백엔드에서 로그인에 성공한 사용자에게 토큰을 발행하면 프론트 엔드에서는 이 토큰을 Local Storage 또는 Session Storage에 담아둡니다. 그리고 토큰을 새로운 요청 때마다 헤더에 넣어서 보내주면, 로그인된 상태로 유지시켜줍니다. 이때 이 토큰이 유효한 토큰인지 확인하기 위해 로그인 인증 데코레이터를 사용합니다.
# user/utils.py [0]
import jwt
import json
from django.http import JsonResponse
from my_settings import SECRET, ALGORITHM
from user.models import User
def login_decorator(func): [1]
def wrapper(self, request, *args, **kwargs): [2]
try:
access_token = request.headers.get('Authorization', None) [3]
payload = jwt.decode(access_token, SECRET, algorithms=ALGORITHM) [4]
user = User.objects.get(id=payload['id']) [5]
request.user = user [6]
except jwt.exceptions.DecodeError: [7]
return JsonResponse({'message':'INVALID_TOKEN'}, status=400)
except User.DoesNotExist: [8]
return JsonResponse({'message':'INVALID_USER'}, status=400)
return func(self, request, *args, **kwargs)
return wrapper
[0] : 장고에서는 보통 데코레이터를 utils.py라는 파일에 별도로 저장해서, 필요할 때마다 import해서 사용합니다. login_decorator
함수는 유저 정보와 관련이 있으므로 user
디렉토리 아래에 utils.py 파일을 만들어서 함수를 저장하였습니다.
[1] : login_decorator
함수는 데코레이터가 끝나고 나서 수행될 메인함수를 매개변수로 받습니다.
[2] : 내부 함수인 wrapper
함수에는 self(instance 자신), request(http request)를 매개변수로 받고, 이후 확장성을 고려하여 *args와 **kwargs까지 매개변수로 받습니다. / 매개변수가 고정되어 있지 않을 때, wrapper
함수를 가변 인수 함수로 만듭니다.
[3] : access_token
은 HTTP Request의 헤더인 Authorization
의 값을 가져오고, 없으면 None으로 넘깁니다.
[4] : payload
는 토큰을 디코딩하면 나오게 될 사용자에 대한 정보입니다. 토큰 발행 시와 동일한 사용자라면 동일한 payload
가 반환됩니다. 디코딩에 들어가는 SECRET
와 algorithms
은 토큰 발행 시 넣었던 정보와 같아야 합니다.
[5] : 데이터베이스에서 토큰의 사용자 정보와 매칭되는 사용자 정보를 불러오고, user
라는 변수에 저장합니다. 토큰 발행 시 id
라는 키로 사용자 정보를 저장했으므로, 불러올 때도 동일하게 id
라는 키를 사용합니다.
[6] : request에 user
라는 변수를 지정해 [5]에서 불러온 유저 정보 객체를 담습니다. 이 객체를 데코레이터 다음에 나오는 함수에서 사용합니다.
토큰 정보를 확인하는 HTTP Request 에는 토큰을 제외하고는 사용자 정보가 들어오지 않기 때문에, 이 user 값을 request에 저장해서 이후 활용하는 것입니다.
여기에서 request는 가변 객체입니다. 가변 객체이기 때문에 다른 객체 또는 변수 할당이 가능한 것입니다.
[7] : 토큰이 손상되었거나 전달된 토큰 값이 없을 때 DecodeError
가 발생합니다. 즉, 해당 서버가 발급한 토큰이 아니라면(시크릿키와 알고리즘 방식이 다르다면) decode가 불가하므로 발생하는 에러입니다.
[8] : 토큰에서 디코드된 사용자 정보가 데이터베이스에 존재하지 않을 때 DoesNotExist
가 발생합니다.
# posting/views.py
import json
from json.decoder import JSONDecodeError
from django.http import JsonResponse
from django.views import View
from posting.models import Posting, Image
from user.models import User
from user.utils import login_decorator --> 추가
class PostingView(View):
@login_decorator [1]
def post(self, request):
try :
data = json.loads(request.body)
user = request.user [2]
content = data.get('content', None)
image_url_list = data.get('image_url', None)
if not image_url_list: [3]
return JsonResponse({'message':'KEY_ERROR'}, status=400)
posting = Posting.objects.create(
content = content,
user = user
)
for image_url in image_url_list:
Image.objects.create(
image_url = image_url,
posting = posting
)
return JsonResponse({'message':'SUCCESS'}, status=200)
except JSONDecodeError:
return JsonResponse({'message':'JSON_DECODE_ERROR'}, status=400)
[1] : post 함수를 실행하기 전에 로그인 토큰을 확인하도록 데코레이터를 추가하였습니다.
[2] : user
변수에는 request에 담긴 유저 정보가 담깁니다.
[3] : user
에는 데코레이터를 통과했다면 반드시 값이 담기므로 KeyError
처리할 필요가 없습니다.
참고사이트
https://velog.io/@devmin/Django-decorator-login-token-basic
https://devvvyang.tistory.com/44?category=973523
https://livetodaykono.tistory.com/49
https://phin09.tistory.com/63