DRF JWT Token #1

Error Coder·2023년 1월 7일
0

JWT?

JWT는 Json Web Token의 약자로 모바일이나 웹의 사용자 인증을 위해 사용하는 암호화된 토큰을 의미한다.

JWT 정보(사용자 id, 토큰 생성시간, 만료시간)를 request에 담아 사용자의 정보 열람, 수정 등 개인적인 작업들을 수행할 수 있다.

JWT를 사용해 인증시스템을 구축한 이유

  1. 프론트엔드와 백엔드의 완전한 분리
    Django의 세션기반 로그인을 사용하기 위해서는 Django Template을 사용해야한다.

이는 프론트엔드 개발인원들이 반드시 Django의 문법을 어느 정도 이해해야만 페이지를 구성할 수 있다는 뜻이다.

하지만 JWT를 사용한다면 클라이언트와 서버의 완전한 분리에 더불어 유저의 로그인 상태를 브라우저에서 간단하게 저장할 수 있어 서버의 부담이 적다는 장점이 있기에 JWT를JWT 사용하기로 하였다.

  1. 보안 문제 예방
    발생할 수 있는 보안 문제를 JWT를 도입함으로써 예방하는 작업을 하였다.

대표적인 문제로 XSS, CSRF 공격이 있다.

먼저 XSS는 Cross Site Scripting의 약자로 Code Injection Attack이라고도 한다.

이는 공격자가 의도하는 악의적인 js 코드를 목표 웹 브라우저에서 실행시키는 것으로 요약할 수 있다.

두 번째로 CSRF는 Cross Site Request Forgery의 약자로 정상적인 request를 가로채 피해자인 척하고 백엔드 서버에 변조된 requesst를 보내 악의적인 동작을 수행하는 공격을 뜻한다.

예를 들어 피해자의 정보 수정, 정보 삭제, 무단 열람 등의 공격이 있을 수 있다. 특히 대표적인 예로, 내가 작성하지 않은 해로운 글이 특정 사이트에 게시되는 경우가 있다.

위와 같은 문제를 예방하기 위해 JWT 토큰의 만료 시간을 5분으로 설정하고, refresh 토큰을 추가적으로 생성하는 방법을 사용하였다. Refresh 토큰을 httpOnly 쿠키로 설정하고, url이 새로고침 될 때마다 기존의 refresh 토큰을 가지고 새로운 JWT 토큰을 요청한다.

그리고 발급받은JWT 토큰을 js내의 private 변수에 저장한다.

이러한 방식은 refresh 토큰이 CSRF에 의해 사용된다 하더라도, 공격자는 실제로 인증에 사용되는 access_token을 알 수 없기 때문에 안전하다. 그리고 refresh 토큰이 httpOnly=True인 쿠키에 저장되기 때문에 XSS 공격으로부터 안전하게 된다.

JWT 인증 비즈니스 로직

## authenticate.py



from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication, CSRFCheck

from django.conf import settings
from django.contrib.auth import get_user_model


User = get_user_model()


class SafeJWTAuthentication(BaseAuthentication):
    """
    JWT Authentication
    헤더의 jwt 값을 디코딩해 얻은 user_id 값을 통해서 유저 인증 여부를 판단한다.
    """
    
    def authenticate(self, request):
        authorization_header = request.headers.get('Authorization')
        
        if not authorization_header:
            return None
            
        try:
            prefix = authorization_header.split(' ')[0]
            if prefix.lower() != 'jwt':
                raise exceptions.AuthenticationFailed('Token is not jwt')

            access_token = authorization_header.split(' ')[1]
            payload = jwt.decode(
                access_token, settings.SECRET_KEY, algorithms=['HS256']
            )
        except jwt.ExpiredSignatureError:
            raise exceptions.AuthenticationFailed('access_token expired')
        except IndexError:
            raise exceptions.AuthenticationFailed('Token prefix missing')
        
        return self.authenticate_credentials(request, payload['user_id'])
    
    def authenticate_credentials(self, request, key):
        user = User.objects.filter(id=key).first()
        
        if user is None:
            raise exceptions.AuthenticationFailed('User not found')
        
        if not user.is_active:
            raise exceptions.AuthenticationFailed('User is inactive')
        
        self.enforce_csrf(request)
        return (user, None)

    def enforce_csrf(self, request):
        check = CSRFCheck()
        
        check.process_request(request)
        reason = check.process_view(request, None, (), {})
        if reason:
            raise exceptions.PermissionDenied(f'CSRF Failed: {reason}')

위 코드를 풀어서 설명하기 전에 Django의 기본 인증 시스템 설정을 알아야 한다.

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'auth.authenticate.SafeJWTAuthentication',
    ),
}

DRF를 사용하면 기본 인가 클래스, 기본 인증 클래스를 위와 같이 설정해줄 수 있다.

위의 DEAULT_AUTHENTICATION_CLASSES를 보면

'auth.authenticate.SafeJWTAuthentication' 이라고 적혀있다.

auth앱에 있는 authenticate.py 에 있는 SafeJWTAuthentication 클래스를 기본 인증 클래스로 설정한다는 뜻이다.

클라이언트로부터 HTTP 요청이 들어오면 Django view에서는 user = request.user로 현재 로그인 된 유저 객체를 가져올 수 있다.

이때 request.user에 알맞은 user를 적재해 주는 역할이 DEFAULT_AUTHENTICATION_CLASSES의 역할이라고 볼 수 있다.

덕분에 인증과 인가의 개념도 확실하게 챙길 수 있다.

인증(Authentication) : 현재 유저가 누구인지 확인한다.

인가(Authorization;(Permission)) : 현재 접속된 유저에 따라 차등적인 권한을 부여할 수 있다.

인증 순서

  1. 우선 클라이언트로부터 HTTP 요청을 받는다.
  2. HTTP 요청의 헤더에 포함된 Authorization 값을 읽는다.
  3. 만약 클라이언트가 헤더에 Authorization 값을 넣지 않았다면, None을 return 한다. (request.user = None이 된다.) Authorization 값이 존재한다면 값을 저장하고 다음으로 넘어간다.
  4. Authorization 값을 decode한다. 만약 토큰의 만료시간이 지났거나 Authorization 토큰이 jwt가 아닌 경우에 예외를 발생시킨다.
  5. 토큰을 decode하면 현재 접속된 user의 id를 얻을 수 있다. id를 통해서 DB에서 알맞은 user를 찾고, CSRF를 확인한 다음 user를 return한다.
  6. request.user에 현재 요청을 보낸 유저가 담기고, view에서 사용할 수 있다.
profile
개발자 지망생

0개의 댓글