Django - 데코레이터로 로그인 토큰 확인하기

jomminii_before·2020년 2월 16일
10

기존에 제작 중이던 인스타 클론 기초편에 댓글 api를 추가했다. 말이 댓글이지 지금은 이메일과 댓글 내용을 보내면 별다른 확인 과정없이 데이터를 저장하고, GET 요청이 들어오면 DB에 있는 댓글 리스트를 보여주는 정도의 기능을 가졌다.

그래도 댓글인데 아무나 달거나 볼 수 있게 하고 싶지가 않아져서 로그인한 회원만 댓글을 관리할 수 있게 하려고 한다.

그런데 여기서 잠깐. 내 서비스에서는 댓글을 작성하려는 사람이 로그인한 사용자인지 어떻게 알고 권한을 줬다 뺐었다 하는걸까?

그 비밀은 HTTP 리퀘스트 헤더에 있다. 아래 두 글에서 살펴봤듯이 로그인에 성공한 사용자에게 토큰을 발행하고, 이 토큰을 새로운 요청 때마다 HTTP 리퀘스트 헤더에 넣어서 보내주면, 로그인된 상태로 유지시켜준다. 이때 이 토큰이 유효한 토큰인지 확인하기 위해 로그인 인증 데코레이터를 사용하려한다.


로그인 인증 데코레이터 만들기

이제 데코레이터를 만들어보자. 데코레이터라 하면 특정 함수의 바로 위에 붙어, 특정 함수가 실행되기 전에 먼저 실행되어, 사전에 하고 싶은 작업을 처리하는 함수라고 할 수 있다. 우리는 이 데코레이터를 만들어 사용자가 특정 기능을 수행하기 전에 사용자가 그 기능을 수행할 권한이 있는지 살펴 볼 예정이다. 토큰으로 권한이 확인되지 않는다면 특정 기능을 담은 함수는 실행되지 않는다.

참고로 로그인 인증 데코레이터는 core라는 앱을 따로 만들어서 utils.py라는 파일을 만들어 작성했다. 하나의 앱에만 쓸게 아니라 데코레이터가 사용될 모든 함수에 공통으로 적용시켜야하기 때문이다. 필수는 아니다.


먼저 필요한 모듈들을 임포트하자.

import jwt                                                
import json                 
import requests

from django.http import JsonResponse
from django.core.exceptions import ObjectDoesNotExist          [1]

from my_settings import SECRET_KEY                             [2]
from account.models import Account

[ 1 ] : DB에 값이 없을 경우 내보낼 에러 처리
[ 2 ] : 토큰 디코딩에 사용할 시크릿키. 보안을 위해 이전 글과는 경로가 달라졌으니 참고하자.


이제 로그인 데코레이터 함수를 짜보자. 데코레이터의 인자로는 이후에 나올 함수가 들어올 것이고, wrapper에는 HTTP 요청을 읽어오기 위해 request가 들어간다.

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        try:
            access_token = request.headers.get('Authorization', None)          [1]
            payload = jwt.decode(access_token, SECRET_KEY, algorithm='HS256')  [2]
            user = Account.objects.get(email=payload['email'])                 [3]
            request.user = user                                                [4]

        except jwt.exceptions.DecodeError:                                     [5]
            return JsonResponse({'message' : 'INVALID_TOKEN' }, status=400)

        except Account.DoesNotExist:                                           [6]
            return JsonResponse({'message' : 'INVALID_USER'}, status=400)

        return func(self, request, *args, **kwargs)

    return wrapper

[ 1 ] : access_token은 HTTP Request의 헤더인 Authorization의 값을 가져오고, 없으면 None으로 넘긴다.
[ 2 ] : payload는 토큰을 디코딩하면 나오게 될 사용자에 대한 정보. 토큰 발행 시와 동일한 사용자라면 동일한 payload가 반환된다. 디코딩에 들어가는 SECRET_KEYalgorithm은 토큰 발행 시 넣었던 정보와 같아야한다.
[ 3 ] : DB의 Account 테이블에서 [토큰을 디코딩 해서 나온 사용자 정보]와 매칭되는 사용자 정보를 불러오고, user라는 변수에 저장한다.
[ 4 ] : HTTP에서 받은 Request에 user변수를 저장해 데코레이터 다음에 나오는 함수에서 사용한다. 토큰 정보를 확인하는 HTTP Request 에는 토큰을 제외하고는 사용자 정보가 들어오지 않기 때문에, 이 user 값을 저장해서 이후 활용한다.
[ 5 ] : 없는 토큰 값이 들어왔을 경우 DecodeError를 처리한다.
[ 6 ] : Account 테이블에 매칭되는 값이 없을 경우 DoesNotExist를 처리한다.
-> import 한건 ObjectDoesNotExist 지만 이렇게 쓰면 Attribute Error 가 난다.


댓글에 적용하기

댓글에는 사용자 이메일과 댓글 내용이 담기는 기본적인 기능으로 구성되어 있다. 기존에 작성된 코드에 GETPOST를 실행하기 전에 로그인 토큰을 확인하도록 데코레이터를 추가하였다.

import json

from .models import Comment
from core.utils import login_decorator                       [1]

from django.views import View
from django.http import JsonResponse, HttpResponse

class CommentView(View):
    @login_decorator                                         [2]
    def get(self, request):
        return JsonResponse({'comment_list':list( Comment.objects.values())},status=200)

    @login_decorator                                         [3]
    def post(self, request):
        data = json.loads(request.body)

        Comment(
            email = request.user.email,                      [4]
            comment = data['comment'],
        ).save()

        return HttpResponse(status=200)

[ 1 ] : 데코레이터가 만들어진 곳에서 데코레이터를 임포트 해와야 사용할 수 있다.
[ 2 ] : get 메서드가 실행되기 전에 로그인 인증 여부를 확인한다.
[ 3 ] : post 메서드가 실행되기 전에 로그인 인증 여부를 확인한다.
[ 4 ] : 토큰을 디버깅해서 얻은 유저 이메일 정보를 활용한다.


실행결과

## 회원가입

$ http -v http://localhost:8000/account/sign-up email=abc@abdc.com password=1234
POST /account/sign-up HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 45
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "email": "abc@abdc.com",
    "password": "1234"
}

HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sun, 16 Feb 2020 09:13:04 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY


## 로그인

$ http -v http://localhost:8000/account/sign-in email=abc@abdc.com password=1234
POST /account/sign-in HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 45
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "email": "abc@abdc.com",
    "password": "1234"
}

HTTP/1.1 200 OK
Content-Length: 126
Content-Type: application/json
Date: Sun, 16 Feb 2020 09:13:14 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImFiY0BhYmRjLmNvbSJ9.CL0JtbheR3-iiOEoFRAtPaFe7L9p-EJYcxxUc-1Beqc"
}
-> 로그인되서 토큰 발행됨


## 정확한 로그인 토큰과 작성할 댓글 요청 보냄

$ http -v http://localhost:8000/comment/input "Authorization:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImFiY0BhYmRjLmNvbSJ9.CL0JtbheR3-iiOEoFRAtPaFe7L9p-EJYcxxUc-1Beqc" comment="로그인되서 댓글 남김"
POST /comment/input HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImFiY0BhYmRjLmNvbSJ9.CL0JtbheR3-iiOEoFRAtPaFe7L9p-EJYcxxUc-1Beqc
Connection: keep-alive
Content-Length: 71
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "comment": "로그인되서 댓글 남김"
}
-> 토큰이 확인되어 댓글 작성 성공


## 잘못된 로그인 토큰(맨 뒤 한 글자 바꿈)과 작성할 댓글 요청 보냄
HTTP/1.1 200 OK
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Sun, 16 Feb 2020 09:14:56 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

$ http -v http://localhost:8000/comment/input "Authorization:
 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImFiY0BhYmRjLmNvbSJ9.CL0JtbheR3-iiOEoFRAtPaFe7L9p-EJYcxxUc-1Beqa" comment="로그인되서 댓글 남김"
POST /comment/input HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImFiY0BhYmRjLmNvbSJ9.CL0JtbheR3-iiOEoFRAtPaFe7L9p-EJYcxxUc-1Beqa
Connection: keep-alive
Content-Length: 71
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/2.0.0

{
    "comment": "로그인되서 댓글 남김"
}

HTTP/1.1 400 Bad Request
Content-Length: 28
Content-Type: application/json
Date: Sun, 16 Feb 2020 09:15:35 GMT
Server: WSGIServer/0.2 CPython/3.8.1
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "message": "INVALID_TOKEN"
}
-> 토큰이 통과하지못해 댓글 작성 실패
profile
https://velog.io/@jomminii 로 이동했습니다.

0개의 댓글