Westagram with Django 최종일(후반부 총정리)

이번 일차는 약 3~4일 간 토큰, 데코레이터와의 기나긴 싸움이었다고 볼 수 있었다. 정확히 끝난 시점은 오늘이고 모든 것이 해결되면 한 번에 정리해서 블로깅하기 위해 지금껏 미뤄왔다.

목차

0. 왜 토큰을 알아야 하는가? 눈으로 토큰을 봤는가?

1. http를 왜 알아야 하는가?

2. 인증 및 인가를 정확히 알아야 하는 이유는 무엇인가?

3. 데코레이터란?

4. 코드

5. 총정리 및 참고 블로그


0. 왜 토큰을 알아야 하는가? 눈으로 토큰을 봤는가?

4주차 첫 날 오후 세션은 프론트엔드 분들과 신호를 맞추는 시간을 가졌다. 즉 프론트 엔드 측에서 알려준 서버 ip에 회원 가입 및 로그인을 백엔드가 설정한 엔드포인트로 보내주면, 가입이 제대로 되었는지, 또 로그인이 제대로 되었는지를 눈으로 직접 확인할 수 있었다. 로그인 과정에서 백엔드는 프론트엔드에게 인가를 위한 '토큰'을 발행해줘야 하는데 여기서 '토큰'이라는 것을 왜 알아야 하는지를 체감할 수 있었다.


1. http는 왜 알아야 하는가?

그렇다면 토큰은 왜 쓸까? 그 답을 알기 위해서는 http가 어떤 구조를 가지고 어떻게 움직이는지부터 알아야 한다. 그냥 인터넷 검색하고, 접속만 되는지 알고, s가 붙는지 안 붙는지만 알면 되는거 아닌가? 아니다. http 통신이 어떻게 되는지 전혀 이해하지 못 하고 토큰을 알기 위해 공부한다면 아마 이렇게 될 것이다.

  • 데이터베이스의 암호화된 비밀번호랑 토큰이 같다.
  • 토큰은 어디에 담겨서 왔다갔다 하지?
  • 다 필요 없고 토큰이 대체 뭐지?...
  • 인증 인가를 한다는데 과정을 모르겠네?

(먼저 http 용어의 개념을 모르는 것이 아니므로 패스.)

여기에 내가 정리하고 싶은 내용은 http가 통신될 때 어떤 구조로 어떤 자료를 어떻게 담아서 왔다 갔다 하냐는 것이다. 근데 그 전에 http가 어떤 성질을 지녔는지 이해하고 넘어가야 하는데 다음과 같다.

  1. 요청과 응답의 형태
    말 그대로 http라는 컴퓨터 간 약속된 조항을 통해 요청하고 응답 받는다는 뜻이다. 월요일에 진행한 세션을 뜯어 보면
  • 프론트엔드에서 아이디와 비밀번호를 로그인으로 보내준다.(요청)
  • 백엔드는 서버에 있는 자료를 바탕으로 로그인된 회원을 확인해서 어떤 신호를 보내준다.(응답)
    로 볼 수 있다.
  1. stateless
    요청과 응답을 서로 알아서 잘 하고 보낸걸 알아서 기억을 잘 하고 있으면 이걸 알 필요가 없다. 문제는 요청과 응답을 하고 나면 이 페이지들은 서로 독립적으로 모른척을 한다는거다.
    예를 들어 사용자가 로그인을 했고 마이페이지에 들어가려고 한다. 근데 그 중간 과정에서 뭔가 정해진 규약을 실행시켜주지 않으면(뒤에 볼 토큰이 서로 왔다 갔다 하지 않으면) 사용자를 거부할 것이다. 이런 특성을 stateless라고 한다.

이제 형태에 대해서는 알았다. 그럼 정보를 어떻게 담아주는지 확인해봐야 할 차례다. 요청과 응답의 형태임을 안다 하더라도 그 상세 과정을 모르면 쓸 수가 없다. 이번 기회에 정리하자면 다음과 같다.

  1. 요청과 응답의 메세지 구조 : 이걸 제대로 정리 안 하고 넘어 갔어서 토큰이 어디에 위치해 있는지 전혀 몰랐다.

3-1. 요청 : 프론트가 보낸다 생각하는 과정
< start line > GET/login HTTP/1.1

  • GET, POST, DELETE 등 요청 액션
  • Request target이라 불리는 목표 url
  • HTTP version
    < Headers > : (중요)
    제일 중요한 부분이다.
  • key, value 형태로 되어 있으며
  • "Authorization"에 회원의 인증인가 처리를 위한 로그인 토큰을 담는다.
    < body >
  • 실제로 보내는 내용으로 POST 메소드를 쓴다.
Body: {
	"user_email": "jun.choi@gmail.com"
	"user_password": "wecode"
}

3-2. 응답
< status line >

  • 응답 상태 표기 줄 : version,Code,Text(HTTP/1.1 404 Not Found
    ) "이거 잘못 보낸거 같은데? 라고 답장 주는 느낌"
    < headers >
  • 요청의 헤더와 동일.
  • 응답에서만 사용되는 헤더 정보들(서버 헤더)

< body >

  • 요청 바디와 동일
  • 응답도 형태에 따라 필요 없는 경우도 있음
ex) 로그인 요청에 대해 성공했을 때 응답의 내용
Body: {
	"message": "SUCCESS"
	"token": "kldiduajsadm@9df0asmzm" (암호화된 유저의 정보)
}
  • JSON타입

    자바스크립트와 소통해야 하니까 중간 다리로 보내는게 JSON

error message 등 자세한 사항도 있지만 토큰을 위해 알아야하는 구조는 이 정도다.


2. 인증 및 인가를 정확히 알아야 하는 이유는 무엇인가?

http가 어떻게 전달되고 받는지 인증 인가를 모르는 상태에서, 백엔드가 보면 굉장히 별 내용이 없는 것 같다.(나만 그런가?) 하지만 인증 인가를 공부하고 토큰의 개념이 들어가기 시작하면 http의 정보 전달 과정이 얼마나 중요한지 알게 된다.

인증 인가에 대한 개념은 알고 있으니 회원 가입 및 로그인 플로우를 통해 인증인가가 어떻게 적용되는지 서술해보도록 하겠다.

<인증 및 인가>

  • 회원가입을 한다->서버에 유저의 데이터가 저장된다->로그인을 한다->데이터베이스 상의 유저 정보가 동일하다면 '토큰'을 발행해준다(인증 끝)->페이지를 옮길 때마다 클라이언트는 받은 토큰을 보낸다->그 토큰을 확인하고 보내준 토큰과 일치하다면 권한을 준다.(인가 인가 인가....)

플로우만 알면 별게 없다. 근데 이렇게 끝나면 참 쉬울 것이다. 인증 및 인가에서 중요한 포인트가 있는데 다음과 같다.

  1. 인증에서 중요한 포인트1 : "암호화"
    인증 및 인가 정리
    암호화는 자료 1~4 부분을 통해 다행히 어렵지 않게 이해할 수 있다.(??)

  2. 인증에서 중요한 포인트2 : "토큰", JWT
    토큰은 흔히 버스 토큰 등 동전을 생각하면 되는데 웹 상에서 토큰은 말 그대로 사용자가 어떤 상황에 접근하기 위해 건내주어 권한을 받기 위한(access token) 어떤 수단이다.(돈 내는 것?)

그렇다면 JWT는 무엇일까? JSON Web Tokens의 준말로 엑세스 토큰 생성의 여러 방법 중 하나다.

  1. 그럼 왜 이 내용을 정리할까?
    몰랐던 부분은 이거다. 과연 토큰은 어떤 정보를 담고 있는가? 그리고 토큰은 어디에 담겨서 왔다 갔다 할까?
    정답은, 로그인 회원가입을 기준으로 보면, 토큰은 유저의 id값을 디코드 시 딕셔너리 형태로 가지고 있고(딕셔너리라는 말이 아님), 담긴 위치는 '헤더'다.

< 추가로 알았어야 할 부분 >
플로어도 알고, 토큰이 뭔지도 알았고, 암호화가 어떻게 진행되어 서버에 저장되는지도 알았다. 문제는 토큰을 혼자서 어떻게 받아서 실습하고, 그게 제대로 암호화가 되었는지 아는 것이었다.

주로 HTTPIE를 이용했으므로 이를 기준으로 하면

  • 토큰은 헤더에 담기므로 헤더를 볼 줄 아는 걸 알았어야 했다.
  • 그리고 다시 보낼 때도 헤더에 담아서 보내는 것을 알아야 한다.
  • 바디에서 꺼내오는 것이 아니다.
  • 토큰 안에는 이메일이나 비밀번호가 들어가 있으면 안 된다.

정도다.

실제 실습 시 토큰은 암호화 인코드 때 bcrypt 양식을 통해 만들었고 인코드 디코드 하는 로직을 이해한 다음 httpie에 직접 토큰을 알아내어 GET, POST 해보니 바로 답이 나왔다. 정확한 내용은 너무 많아 코드를 모두 옮길 수 없어 뒤에 전체 코드 확인만 참고할 수 있게 하겠다.

3. 데코레이터란?

무엇을 데코하다. 꾸미다라는 말처럼 데코레이터는 함수를 꾸며주는 애다. 깊게 들어가면 한도 끝도 없으니 예제 하나만 보고 넘어가도록 하겠다.

def trace(func):
  def wrapper():
    print(func.__name__,1)
    print(func.__name__,2)
    print(func.__name__,3)
  return wrapper

@trace
def hello():
  print('hello')

@trace
def world():
  print('world')

hello()
world()

위에 만들어진 함수 trace는 wrapper라는 내용물 안의 것으로 다른 함수들의 내용을 건들지 않고 꾸며줄 것인데 위의 예시를 아래 hello, world 함수를 각각 꾸미면 프린트가 3줄씩 나온다. 대략 이런 기능을 하는 함수다.

만일 위의 래퍼말고 아래처럼 인자가 주어진 함수면 인자를 맞춰주면 된다.

def trace(func):
    def wrapper(a, b):
        r = func(a, b)
        print('{0}(a={1}, b={2}) -> {3}'.format(func.__name__, a, b, r))
        return r
    return wrapper
 
@trace
def add(a, b):
    return a + b 
 
print(add(10, 20))

꾸며주는 함수의 양식은 다양하므로 상황에 맞게 사용하면 되는데 여기서는 로그인 후 클라이언트가 토큰을 건내줄 때마다
1. 토큰이 제대로 보내졌는지?
2. 맞다면 입장시켜줄 수 있게 views.py의 내용을 수정
하는 일을 할 것이다.


4. 코드

지금껏 이론 정리로 살펴본 내용을 직접 코드화 시켰을 때 어떻게 나오는지 정리해봤다. 자세한 내용보다는 핵심 부분 위주로만 추렸다.

  1. 회원가입 과정에서의 암호화 과정
...
(중략)
import jwt
....
            byted_password = data['password'].encode('utf-8')
            # 먼저 패스워드 정보를 인코드 해주고
            hash_password = bcrypt.hashpw(byted_password, bcrypt.gensalt()).decode()
            # 비크립트로 해쉬화 해준 뒤 디코드 해준다. 디코드 안하면 나중에 못 찾는다.
            password = hash_password
            # 이후 패스워드를 변수에 넣어주면 끝
  1. 로그인 과정에서 패스워드 체크 및 토큰 발행
  • 선행 될 부분 : 토큰에 넣어야 할 내용은 user의 id다. 그러므로 객체에서 id를 가져올 방법을 생각해야한다.
                user    = User.objects.get(email=data['email'])
                # user는 객체다
                user_id = user.id
                # 객체에 .~하면 바로 내용을 꺼낼 수 있다.
            except User.DoesNotExist:
                return JsonResponse({"message":"USER_DOES_NOT_EXIST"}, status=400)

            if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
            # 만일 패스워드 체크 시 입력한 인코딩 비밀번호와 서버 인코딩 비밀번호가 같다면
                token = jwt.encode({'user_id' : user_id}, SECRET_KEY, algorithm="HS256")
                # 토큰 발행. jwt 공식 문서 등 참고
               	# 유저의 id임을 꼭 명심해야 한다.(여기 바꾸는데 한참 걸렸다.)
  1. 이후 토큰을 상시 확인할 수 있는 로직을 짜줘야 한다. 물론 그렇다고 이 로직이 자동으로 모든 뷰를 체크해주는 것이 아니다. 데코레이터가 있기 전에 로직이므로 참고만 하자.(데코레이터를 만든 이상 아마 안 써도 될 것 같으나 공부용으로 남겨두도록 하자.)
class TokenCheckView(View):
    def post(self,request):
        data = json.loads(request.body)
        # 값을 json 바디로 받아오고
        
        user_token_info = jwt.decode(data['token'], SECRET_KEY, algorithms='HS256')
        # 유저 토큰 정보를 디코드해서 받아온다.
        
        if User.objects.filter(email=user_token_info['email']).exists():
        # 그래서 유저 객체가 있으면 ~
            return JsonResponse({"message": "SUCCESS"}, status=200)
        return JsonResponse({"message":"INVALID_USER"}, status=401)
  1. (제일 중요한) 위스타그램 데코레이터
    답 안 보고 만드느라 너무 고생했지만 뿌듯하다. 여하튼 데코레이터에 대한 이해가 잘 없었을 때는 쓰던 함수를 가져와서 만드는 것인 줄 알았는데 그냥 독립적인 장식품이었다.
def TokenCheck(func):
    def wrapper(self, request, *args, **kwargs):
    # 리퀘스트해서 토큰 까는 애
        # 토큰의 핵심은 공개가 되어도 된다 -> 그래서 예제가 user_id였던 것.
        try:
            token = request.headers.get('Authorization')
            # 토큰을 Authorization (위에서 본 그거)으로 가져오는데 위치를 잘 봐야한다. 헤더다.
            if token:
                payload = jwt.decode(token, SECRET_KEY, algorithms="HS256")
                user_id = payload['user_id']
                user = User.objects.get(id=user_id)
                # user_id 가져온 것을 user라는 변수에 담고
                # 로직 잘 보기. if 한 번으로 모든 것이 해결.(elif 지양)
                # 페이로드에서 디코드해서 값을 변수에 저장하고 유저는 그 아이디값으로 가져온다.
                
                
                request.user = user
                # 변수에 담은 것을 뒤에서 부를 request.user에 또 담아준다.
                # 이 과정이 데코레이터를 사용하기 위한 결정적인 부분이다.(위에서 정의한 내용을 이렇게 담고 리퀘스트를 통해서 다른 뷰를 통제할 수 있게 되니 다른 뷰에서도 사용자가 접근할 때마다 토큰을 제시하고 까고 할 수 있게 되는 것.
                return func(self, request, *args, **kwargs)
                # 데코레이터 펑션은 어떤 것을 받을지 모르니 이렇게 하기.
            return JsonResponse({"message":"GIVE_ME_TOKEN"}, status=400)
        except jwt.InvalidTokenError:
            return JsonResponse({"message":"YOUR_TOKEN_ERROR"}, status=400)
  1. 데코레이터 테이블에 적용한 예시
class PostingView(View):
    @TokenCheck    
    # 데코레이터로 유저의 토큰을 깐 상태
    def post(self, request):
        #딕셔너리 모양으로 받아들여진 정보(httpie에 입력한 정보)
        data    = json.loads(request.body)
        user    = request.user
        # 위에서 살펴본 request.user로 토큰 깐 상태로 받아들여진 상태.
        posting = Posting.objects.create(
            img_url = data["img_url"],
            user    = user
        )
        return JsonResponse({"message" : "SUCCESS"}, status=200)

5. 총정리 및 참고 블로그

많은 과정이었다. 다 기억나지 않을 정도로 수많은 에러가 발생했고 위에 있는 내용만 이번주 내내 진행했다. 한 줄 한 줄 생각하고, print해보고, shell에 쳐보며 디버깅 하는 것에 대해 배웠고 어떤 인자가 왜 이렇게 사용되는지도 많이 이해하게 되었다. 완성되었으니 지속적으로 복습하고(특히 인증인가부분) 다음주 프로젝트 잘 진행해야겠다.

참고 블로그

profile
커피 내리고 향 맡는거 좋아해요. 이것 저것 공부합니다.

0개의 댓글