개발기록장

[Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용하기 본문

TIL/Django

[Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용하기

yangahh 2021. 2. 8. 19:37

 

[이전 글]

[Django] 인스타그램 클론 코딩(2) - 로그인 기능 구현

 

[Django] 인스타그램 클론 코딩(2) - 로그인 기능 구현

인스타그램은 전화번호, 사용자 이름, 이메일 중 하나와 패스워드로 로그인을 할 수 있다. 이를 views.py에 LoginView에 작성하였다. views.py - LoginView 클래스에서 로그인 기능 처리 import jwt import json i..

devvvyang.tistory.com

[다음 글]

[Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API에서 Update에는 무슨 메소드를 써야하는가...)

 

[Django] 인스타그램 클론 코딩(4) - 게시물과 댓글 C.R.U.D (+ RESTful API에서 Update에는 무슨 메소드를

[이전 글] [Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용하기 [Django] 인스타그램 클론 코딩(3) - Authorization Decorator 만들고 활용 [이전 글] devvvyang.tistory.com/41 [Django..

devvvyang.tistory.com

 

서비스에는 로그인을 해야지만 이용할 수 있는 서비스가 있다.

인스타그램에서는 게시물 올리기, 댓글 쓰기 등이 있다.

그런데 이런 서비스에 request를 날릴 때마다 로그인을 했는지 검증하는 구문이 있다면, 굉장히 비효율적인 코드가 될 것이다.

그래서 이런 경우에는 decorator를 이용하여 효율적으로 코드를 작성할 수 있다.

 

** decorator에 대한 설명 보러 가기

** 인증(Authentication)에 대한 설명 보러 가기

** 인가(Authorization)에 대한 설명 보러 가기

 

1. Authorization Decorator 만들기

import jwt
import json

from django.http import JsonResponse

from .models     import User
from my_settings import SECRET

def login_decorator(func):
    def wrapper(self, request, *args, **kwargs):
        
        # login check
        if "Authorization" not in request.headers:
            return JsonResponse({"message": "NEED_LOGIN"}, status=401)
        
        try:
            access_token = request.headers['Authorization']
            payload      = jwt.decode(access_token, SECRET['secret'], algorithms=['HS256'])
            login_user   = User.objects.get(id=payload['user_id'])
            request.user = login_user
            return func(self, request, *args, **kwargs)
        
        except jwt.DecodeError:
            return JsonResponse({'message': 'INVALID_TOKEN'}, status=401)
        
        except User.DoesNotExist:
            return JsonResponse({'message': 'INVALID_USER'}, status=401)

    return wrapper

 

  • login_decorator라는 이름의 함수에서 유저의 권한을 확인하는 로직을 처리하도록 구현하였다.

  • Django에서는 decorator 함수들을 보통 utils.py라는 파일에 별도로 저장해서, 다른 views.py에서 필요할 때마다 이 utils.py를 import 해서 decorator로 사용한다.

    나는 login_decorator는 user와 관련 있는 decorator이므로 user app 디렉토리 아래 utils.py라는 파일을 만들어서 여기에 login_decorator 함수를 저장하였다.

  • login_decorator의 형태는 일반적인 decorator 함수와 비슷하다. 이 데코레이터를 실행하고 나서 수행할 API function을 파라미터로 받고, 중첩 함수인 wrapper를 return 한다.

  • 실제로 로직을 처리하는 wrapper라는 내부 함수는 데코레이터가 실행되고 나서 실행되는 API function을 고려하여 self(instance 자신), request(http request)를 파라미터로 받고 더불어 확장성을 고려하여 *args와 **kwarges까지 파라미터로 받게 구현하였다.

  • 첫 번째로 http request의 header에 'Authorization'이라는 key가 없다면, 토큰이 없다는 뜻이므로 로그인이 필요하다는 에러 메시지(NEED_LOGIN)를 반환하도록 했다.

  • 토큰이 있는 request라면 jwt.decode() 메소드를 이용하여 토큰 정보를 복호화하여 payload라는 변수에 저장하였다.

  • 복호화하는 과정에서 이 서버가 발급한 토큰이 아니라면 jwt.DecodeError가 발생하므로 try-except로 에러 처리를 해준다.

  • 로그인 시 발급했던 jwt에서 payload에 user_id를 저장했다 (로그인 코드 참고). 따라서 payload 변수에서 user_id에 접근하여 login_user라는 변수에 user_id를 저장하였다.

  • 이때 토큰에서 전달받은 user_id가 DB에 없는 user id 라면 User.DoesNotExist 에러가 발생하므로 try-except로 그에 따른 에러 처리도 해준다.

  • 그리고 request.user에 이 login_user 즉, user의 id를 저장했다.

 

 

참고)

- Authorization이라는 키워드는 어디서 나온 걸까?
  : request headers에 담긴 정보도 request body처럼 딕셔너리 형태로(정확히는 JSON타입) 받아오는데,
토큰에 대한 정보는 통상적으로 request headers에 Authorization이라는 키로 정보를 넘겨준다.
꼭 Authorization으로 할 필요는 없지만 통상적으로 쓰이기 때문에 기본 값도 Authorization이다.

 

 

- request.user는 무엇일까?

: request는 가변 객체이다. 가변 객체이기 때문에 다른 객체(또는 변수) 할당이 가능하다. 그래서 request에 user라는 객체를 새로 할당하여 여기에 user 정보를 넣어서 request로 정보를 return 하는 것이다.

 

- 왜 이렇게 할까?

: 데코레이터가 실행되고 나서 실행되는 API function가 인자를 어떻게 받을지 모르기 때문에 어차피 인자로 받아야 하는 request에 user정보를 담아서 주는 것이다.
이렇게 하지 않는다면, user라는 인자를 전달하기 위해 이 데코레이터 다음에 실행될 모든 API function에 추가 인자를 받을 수 있도록 구현해야 한다. 하지만 이렇게 되면 데코레이터를 쓰는 의미가 퇴색된다. 

 

 

 

2. Authorization Decorator 활용

로그인을 해야만 사용 가능한 서비스를 처리할 때 그 서비스를 처리하는 함수 위에 login_decorator를 붙인다.

예시) 인스타그램에 게시글을 등록하는 메소드에는 반드시 로그인이 필요하므로 이 함수 위에 @login_decorator를 붙인다.

import json

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

from .models      import Post
from user.utils   import login_decorator


class PostView(View):
    @login_decorator
    def post(self, request):
        data      = json.loads(request.body)
        user      = request.user
        image_url = data.get('image_url', None)
        content   = data.get('content', None)
        
        # KEY_ERROR check
        if not image_url:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)
            
        post = Post.objects.create(
            user      = user,
            image_url = image_url,
            content   = content
        )
        return JsonResponse({'message': 'SUCCESS'}, status=200)

 

 

 

3. http request에 토큰 보내기

위에 예시로 적은 PostView의 post메소드를 테스트하려고 보니 막상 http request에 토큰을 어떻게 보내지..? 싶었다.

구글링 해보고 몇 가지 테스트를 해본 결과, JWT 토큰은 아래와 같이 보내면 된다는 것을 알게 되었다.

http -v POST http://127.0.0.1:8000/url입력 "Authorization: 토큰입력" image_url='https://test.jpg' content="test"

request body 부분인 image_url나 content 처럼 작성하면 Authorization도 request body로 인식을 해버린다.

반드시 따옴표 부분 안에 Authorization이라는 키워드를 넣고 ':' 뒤에 토큰을 입력해서 함께 전달해주면 된다.