All Articles

django - 인증, 인가 실습 심화

지난 번 인증, 인가 실습에서 조금 더 발전시켰다. 모두 views 내에서 이루어졌던 인가 과정을 분리해 데코레이터로 만들어주었고, 뭉뚱그려 출력되던 에러 메시지도 에러 케이스를 세분화해 에러를 특정할 수 있도록 수정했다.

유효한 토큰인지 검증하는 데코레이터

import jwt

from django.http import JsonResponse
from .models import Users

def auth(func):
  def wrapper(self, request, **kwargs):
    # request 헤더에서 토큰 가져오기. 
    auth_token = request.headers.get("Authorization", None)
    # 토큰 값이 아예 안 들어왔을 때 401 코드 처리 및 메시지 출력
    if auth_token == None:
      return JsonResponse({'message':'Enter the token.'}, status=401)

    try:
      # 받은 토큰 디코딩해서 user id 정보 출력하기
      payload = jwt.decode(auth_token, 'secret')
      # 위에서 디코딩한 user id 숫자만 추출해서 DB에 있는지 대조
      if Users.objects.get(id=payload['id']):
        # id 숫자를 user에 할당
        user = Users.objects.get(id=payload['id'])
        # 데이터 테이블에 열 이름과 값 동적 추가
        request.user = user
        return func(self, request, **kwargs)

    # signiture 부분이 잘못됐을 때 401 코드 처리 및 메시지 출력
    except jwt.InvalidSignatureError:
      return JsonResponse({'message':'Invalid token. Check the suffix.'}, status=401)
    # header 부분이 잘못됐을 때 401 코드 처리 및 메시지 출력
    except jwt.DecodeError:
      return JsonResponse({'message':'Invalid token. Check the prefix.'}, status=401)
  return wrapper

코멘트 views.py 파일에 데코레이터 붙이기

import json
from django.views import View
from django.http import JsonResponse, HttpResponse
from .models import Comments
# 데코레이터를 다른 파일에 작성했으므로 임포트 해야함
from users.utils import auth 

class CommentView(View):
  # auth 데코레이터 장식
  @auth
  def post(self, request):
    try:
      # 데이터 테이블에 동적으로 생성해준 데이터를 끌어와서 user에 할당해줌
      # print 찍어보면 {'username_id' : 1 } 같은 형식임
      user = request.user
      # request body로부터 받아올 데이터 (유저 입력값)
      data = json.loads(request.body)
      # 프론트에서 코멘트가 공백일 때 게시 버튼이 활성화되지 않도록 1차로 막고 백에서 2차로 막아줌
      if data['comment'] == '':
        return HttpResponse(status=400)
      
      # FK로 끌어온 username에(즉, 데이터 테이블 열 이름은 username_id) 유저 id 숫자만 뽑아서 할당
      # comment는 유저가 입력한 값을 request body에서 받아옴
      Comments(
        username_id = user.id,
        comment = data['comment'],
      ).save()
      return JsonResponse({'comment': data['comment']}, status=200)

    # 프론트에서 key 이름을 잘못 지정해서 보냈을 때 출력할 에러 코드와 메시지
    # as e로 조건 걸어주면 e에 올바른 key 값이 리턴됨   
    except KeyError as e:
      return JsonResponse({'message': e + 'Invalid key. The key name is comment.'})

    except:
      return JsonResponse({'message':'Something wrong.'}, status=401)


# get 생략

users views.py 예외 조건 세분화

import json
import bcrypt
import jwt

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

class MainView(View):
  def get(self, request):
    return JsonResponse({'Welcome to':'Westagram', 'Sign-up':'/users/sign-up', 'Log-in':'/users/log-in'}, status=200)

class SignUpView(View):
  def post(self, request):
    data = json.loads(request.body)

    try:
      hased_pw = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt())
      decoded_hashed_pw = hased_pw.decode('utf-8')

      if data['username'] == '':
        return JsonResponse({'message':'username is required.'}, status=401)
            
      elif data['password'] == '':
        return JsonResponse({'message':'password is required.'}, status=401)
            
      elif Users.objects.filter(username=data['username']).exists():
        return JsonResponse({'message':'username already exists.'}, status=409)
            
      if not Users.objects.filter(username=data['username']).exists():
        Users(
          username = data['username'],
          password = decoded_hashed_pw
        ).save()
        return JsonResponse({'message':'WELCOME, ' + data['username']}, status=200)

    except IntegrityError:
      return JsonResponse({'message':'username already exists.'}, status=409)

    # 프론트에서 key 이름을 잘못 지정해서 보냈을 때 출력할 에러 코드와 메시지
    # as e로 조건 걸어주면 e에 올바른 key 값이 리턴됨
    except KeyError as e:
      return JsonResponse({'message': str(e) + ' is right key name. The key names are username and password.'}, status=400)

    except:
      return JsonResponse({'message':'Something wrong.'}, status=401)

# get 생략

class LogInView(View):
  def post(self, request):
    data = json.loads(request.body)

    try:
      if Users.objects.filter(username=data['username']).exists():   
        user_id = Users.objects.get(username=data['username'])
                
        if bcrypt.checkpw(data['password'].encode('utf-8'), user_id.password.encode('utf-8')) == True:
          access_token = jwt.encode({'id':user_id.id}, 'secret', algorithm='HS256')
          # 토큰 같은 값 리턴 시 부가 메시지 없이 value란에는 value만 넣어서 리턴해야 함
          # 그래야 프론트에서 깔끔하게 처리 가능
          return JsonResponse({'message':'WELCOME BACK, ' + data['username'], 'token' : access_token.decode('utf-8')}, status=200)
        else:
          return JsonResponse({'message':'Wrong password.'}, status=401)

      else:
        return JsonResponse({'message':'Wrong username.'}, status=401)

    # 프론트에서 key 이름을 잘못 지정해서 보냈을 때 출력할 에러 코드와 메시지
    # as e로 조건 걸어주면 e에 올바른 key 값이 리턴됨
    except KeyError as e:
      return JsonResponse({'message': str(e) + ' is right key name. The key names are username and password.'}, status=400)

# get 생략

어려웠던 점

에러 케이스를 세분화하기 위해 정확히 어떤 에러가 뜨는지 알아야 했다. 이때 except Exceptions as e를 아주 유용하게 잘 써먹었다. 하지만 유독 토큰 값이 아예 입력되지 않거나, 틀린 토큰이 입력됐을 때 뜨는 에러를 잡아내기가 어려웠는데, 알고보니 except를 데코레이터에서 걸어줬어야 했다. 데코레이터 단에서 이미 토큰 검증 등의 토큰과 관련한 작업을 마치기 때문이다. 데코레이터를 왜 만드는지 잘 생각해보면 어떤 기능이 들어가야할지 알 수 있을 것이다.

그리고 데이터 테이블에 동적으로 값을 추가해주는 개념이 잘 안 잡힌다. 코드를 보면 읽을 수는 있는데, 내가 혼자 적으려면 아직 컨닝이 필요한 부분…