Django | 클래스 101 클론 - JWT (토큰 기반 인증)

김민철·2021년 2월 13일

팀프로젝트

목록 보기
8/8
post-thumbnail

Intro

로그인한 유저가 추가적인 기능을 이용할 때, 해당 사용자가 맞는지 확인이 필요합니다. 로그인한 유저들에게 매번 재로그인을 시킬 수는 없습니다. 그렇다면 어떤 방식으로 확인할 수 있을까요?

Session 기반 인증

서버측에서 유저들의 정보를 기억하는 방식입니다. 여기서 세션이라는 용어가 나오는데, 세션은 클라이언트의 데이터를 서버 측에 저장하는 것을 말합니다.

세션을 사용한 인증과정을 보겠습니다.

유저가 로그인에 성공하면, 세션이 서버에 저장됩니다. 식별을 위해 SessionId 를 반환하고 브라우저는 쿠키에 SessionId 를 저장합니다.

서버에 보내는 요청에는 쿠키가 들어있고, 서버는 클라이언트가 보낸 SessionId와 저장하고 있는 세션을 비교하고 응답을 보내줍니다.

세션을 기반으로 인증을 진행한다면 유저가 매번 재로그인을 할 필요가 없습니다. 이렇게 좋아보이는 세션기반 인증에는 몇 가지 단점이 있습니다.

  1. 세션은 대부분 서버 메모리에 저장하는데, 로그인 중인 사용자가 늘어날 수록 부하가 걸리게 됩니다. 데이터베이스에 저장하는 경우에도 역시 무리를 줄 수 있습니다.
  2. 세션 기반 인증으로 동작하는 웹 사이트가 있습니다. 이 사이트의 네이티브 앱(모바일) 을 구현하고자 할 때 문제가 생깁니다. 네이티브 앱은 쿠키가 없기 때문입니다.

2번 경우처럼 다양한 플랫폼에서 인증을 진행하려면 어떠한 방식을 써야할까요 ?

Token 기반 인증

유저의 인증정보를 서버에 보관하지 않고, 클라이언트에 보관하면 어떨까요 ?

JSON Web Token

JWT(JSON Web Token)는 JSON 객체를 사용하여 가볍고 자가수용적인 방식으로 정보를 안전성있게 전달할 수 있는 구조입니다.

  • 자가수용적 : JWT 는 필요한 모든 정보를 자체적으로 지니고 있습니다. JWT 시스템에서 발급된 토큰은, 토큰에 대한 기본정보, 전달할 정보, 토큰이 검증됐다는 signature 를 포함하고 있습니다.

JWT 구조

JWT 는 헤더, 페이로드, 서명으로 이루어져 있습니다.

헤더에는 토큰 타입과 해시 알고리즘 정보가 들어갑니다.

페이로드에는 토큰에 담을 정보(ex user_name, user_id)가 들어갑니다.

서명부분은 헤더의 해시 알고리즘을 사용하는 곳입니다.

작성한 헤더와 페이로드는 인코딩만 이루어지고, 서명부분은 암호화가 이루어집니다. 만들어진 JWT 구조는 헤더.페이로드.서명 형태입니다.

보셨다시피 JWT 는 페이로드를 숨기지 않습니다. 그렇기에 페이로드에는 민감한 정보를 담아서는 안 됩니다. JWT 의 목적은 데이터가 인증된 소스에서 생성되었다는 것을 증명하는 것입니다.

JWT 를 이용한 인증과정입니다.

JWT 는 자체에 인증정보를 포함하고 있기에 서버에 따로 저장할 필요가 없습니다. 정보를 저장하지 않으므로 이용자 요청에 대해 리소스를 절약할 수 있습니다.

또한 토큰을 클라이언트 측에 저장되기 때문에 다양한 플랫폼에서 사용할 수 있습니다.

파이썬에서 jwt 를 적용해보겠습니다 .

>>> import jwt
>>> secret = "kmc"
>>> access_token = jwt.encode({"user":"mincheol"},secret,algorithm="HS256")
>>> access_token
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibWluY2hlb2wifQ.NRRQMekvmxsxilUc8BemQ2bCYkjq3daSnfQ6OmxAHIs'

# secret 키를 설정해주지 않으니 에러가 발생
>>> access_token2= jwt.encode({"user":"mincheol"},algorithm="HS256")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: encode() missing 1 required positional argument: 'key'

# 생성된 토큰을 디코드
>>> original = jwt.decode(access_token,secret,algorithm="HS256")
>>> original
{'user': 'mincheol'}

# 다른 secret 키를 사용하니 에러가 발생
>>> original_2 = jwt.decode(access_token,"mincheol",algorithm="HS256")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/jwt/api_jwt.py", line 91, in decode
    decoded = super(PyJWT, self).decode(
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/jwt/api_jws.py", line 155, in decode
    self._verify_signature(payload, signing_input, header, signature,
  File "/Library/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/jwt/api_jws.py", line 223, in _verify_signature
    raise InvalidSignatureError('Signature verification failed')
jwt.exceptions.InvalidSignatureError: Signature verification failed

프로젝트 적용

이번 프로젝트에서 로그인 부분을 맡았기에 JWT 를 적용했습니다 .

class SignInView(View):
				...
        if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
            access_token = jwt.encode({'id': user.id, 'exp': datetime.utcnow() + timedelta(hours=24)}, SECRET,
                                              algorithm=ALGORITHM)
            return JsonResponse({'ACCESS_TOKEN': access_token}, status=201)

bcrypt 라이브러리를 통해 db 에 저장된 암호와 입력받은 암호를 비교해줍니다. 두 암호가 같다면 유효시간을 설정한 JWT 토큰을 전달해주고 로그인을 성공시킵니다.

유저가 여러 기능을 이용할때, @id_auth 데코레이터를 이용해 JWT 토큰의 유효성을 검사했습니다.

def id_auth(func):

    def wrapper(self, request, *args, **kwargs):
        try:
            access_token = request.headers.get('Authorization', None)
            payload      = jwt.decode(access_token, SECRET, algorithms = ALGORITHM)
            user         = User.objects.get(id = payload["id"])
            request.user = user

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

        except jwt.DecodeError:
            return JsonResponse({'MESSAGE': 'INVALID_TOKEN'}, status=401)

        except jwt.InvalidTokenError:
            return JsonResponse({'MESSAGE': 'INVALID_ACCESS_TOKEN'}, status=401)

        except jwt.ExpiredSignatureError:
            return JsonResponse({'message': 'EXPIRED_TOKEN'}, status=401)

        except User.DoesNotExist:
            return JsonResponse({'MESSAGE': 'INVALID_USER'}, status=401)

    return wrapper

정리

스마트폰 이후, 여러 플랫폼에서 더 적합한 방법은 세션 인증보다 JWT 인증이라고 할 수 있습니다. 하지만 JWT 인증이 최고는 아니며, 구현방법에 따라 서버 기반 세션인증을 사용할 수도 있습니다.


참고
https://bcho.tistory.com/999
https://velopert.com/2389
https://velopert.com/2389
https://pyjwt.readthedocs.io/en/stable/index.html

0개의 댓글