[Web] Token 기반 인증 + JWT

이수진·2021년 12월 20일
1

Web

목록 보기
2/2
post-thumbnail

먼저, 지난번에 알아보았던 세션 기반 인증에 대해 짧게 복습 후, 토큰(Token)기반의 인증 방식에 대해 알아보겠습니다.

서버 기반의 인증 시스템 - Session

기존의 인증 시스템은 서버 기반의 인증 방식으로, 서버 측에서 사용자들의 정보를 기억하고 있어야 합니다. 사용자들의 정보를 기억하기 위해서는 세션을 유지해야 하는데, 메모리나 디스크 또는 데이터베이스 등을 통해 관리합니다. 서버 기반의 인증 시스템은 클라이언트로부터 요청을 받으면, 클라이언트의 상태를 계속해서 유지하고 이 정보를 서비스에 이용하는데, 이러한 서버를 Sateful 서버라고 합니다. 예를 들어 사용자가 로그인을 하면, 세션에 사용자 정보를 저장해두고 서비스를 제공할 때 사용하곤 합니다. 이러한 서버 기반의 시스템은 다음과 같은 흐름을 갖습니다.

이러한 인증 장식은 소규모 시스템에서는 아직 많이 사용되고 있지만, 웹/앱 어플리케이션이 발달하게 되면서 서버를 확장하기가 어렵다는 등 여러 문제점이 있습니다.

이런 여러 문제들 때문에 토큰 기반의 인증 시스템을 사용하게 되었습니다.


Token(토큰) 기반의 인증 시스템?

토큰 기반의 인증 시스템은 인증받은 사용자들에게 토큰을 발급하고, 클라이언트가 서버에 요청을 할 때 헤더에 토큰을 함께 보내도록 하여 유효성 검사를 합니다. 이러한 시스템에서는 더이상 사용자의 인증 정보를 서버나 세션에 유지하지 않고 클라이언트 측에서 들어오는 요청만으로 작업을 처리합니다. 즉, 서버 기반의 인증 시스템과는 달리 상태를 유지하지 않으므로 Stateless한 구조를 갖습니다.

토큰 기반 시스템의 동작은 다음과 같습니다.

  1. 사용자 로그인 요청
  2. 서버에서 회원인지 확인
  3. 계정정보가 정확하다면 유저에게 access 토큰(signed token이라고도 함)을 발급
    : 토큰에는 유효기간을 설정해야하며, 인증에 필요한 정보를 암호화하여 토큰을 발급
  4. 로그인 요청에 대한 응답과 함께 암호화된 토큰을 클라이언트에게 보냄
  5. 클라이언트 측에서는 토큰을 저장해두고 서버에게 요청할 때 마다 함께 보냄
  6. 서버는 해당 토큰을 복호화한후 유효한 토큰인지 검증
  7. 검증에 성공하면, 해당 사용자에 맞는 데이터를 응답함

-> 즉, 토큰은 클라이언트가 저장하고, 서버는 토큰의 유효성검사만 하면 되는 것이다!

<토큰 기반 인증의 장점>

1. Stateless 서버

세션/쿠키 인증방식은 서버에서 별도의 세션 저장소에 클라이언트 상태를 계속 유지하고 이 정보를 서비스에 이용하는 Stateful 서버였습니다. 하지만 토큰에는 인증에 필요한 모든 정보를 담고 있으므로, 서버에 별도의 세션저장소가 필요하지 않으며, 서버는 클라이언트 측에서 들어오는 요청으로만 작업을 처리합니다.

2. 높은 서버 확장성

토큰 기반 인증은 stateless 서버, 즉 클라이언트와 서버와의 연결고리가 없기 때문에 서버의 확장성이 좋아집니다. 여기서 말하는 서버의 확장성이란 많은 트래픽을 감당하기 위하여 여러 개의 프로세스를 돌리거나 여러 대의 서버 컴퓨터를 추가하는 것을 의미합니다.

3. 확장성

여기서의 확장성은 로그인 분야의 확장을 의미합니다. 토큰을 사용하여 다른 서비스에서도 권한을 공유할 수 있습니다. 대표적인 예제로, 페이스북/구글/네이버 계정을 이용하여 다른 웹서비스에 로그인할 수 있습니다.

<토큰 기반 인증의 단점>

1. 이미 발급된 토큰에 대해서는 돌이킬 수 없습니다.

세션/쿠키 방식에서는 서버가 클라이언트의 세션을 삭제시켜 강제 로그아웃이 가능했지만, 토큰을 한 번 발급받게되면 유효기간이 지날때까지 계속 사용이 가능합니다.

2. 서버에서 토큰을 제어할 수 없습니다.



토큰 기반 인증, JWT에 대해서

JWT?

JWT(Json Web Token)란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token입니다. JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달합니다.

애플리케이션이 실행될 때, JWT를 static 변수와 로컬 스토리지에 저장하게 됩니다. static 변수에 저장되는 이유는 HTTP 통신을 할 때마다 JWT를 HTTP 헤더에 담아서 보내야 하는데, 이를 로컬 스토리지에서 계속 불러오면 오버헤드가 발생하기 때문이다. 클라이언트에서 JWT를 포함해 요청을 보내면 서버는 허가된 JWT인지를 검사합니다. 또한 로그아웃을 할 경우 로컬 스토리지에 저장된 JWT 데이터를 제거합니다. (실제 서비스의 경우에는 로그아웃 시, 사용했던 토큰을 blacklist라는 DB 테이블에 넣어 해당 토큰의 접근을 막는 작업을 해주어야 합니다.)

<JWT의 구조>

JWT는 Header, Payload, Signature로 이루어져있습니다.

  • Header에는 토큰의 타입이 JWT라는 것과 무슨 해싱알고리즘(HMAC/RSA)을 사용하였는지 정보가 담겨있습니다.
  • Payload에는 사용자의 인증정보가 담겨져있습니다.
  • Signature에는 전자서명이 들어가게됩니다. 이 서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 서명(Signature)은 위에서 만든 헤더(Header)와 페이로드(Payload)의 값을 각각 BASE64로 인코딩하고, 인코딩한 값을 비밀 키를 이용해 헤더(Header)에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64로 인코딩하여 생성한다.

< JWT 단점 및 고려사항>

  • 토큰 길이: 토큰의 페이로드(Payload)에 3종류의 클레임을 저장하기 때문에, 정보가 많아질수록 토큰의 길이가 늘어나 네트워크에 부하를 줄 수 있습니다.
  • Payload 인코딩: 페이로드(Payload) 자체는 암호화 된 것이 아니라, BASE64로 인코딩 된 것입니다. 중간에 Payload를 탈취하여 디코딩하면 데이터를 볼 수 있으므로, JWE로 암호화하거나 Payload에 중요 데이터를 넣지 않아야 합니다.
  • Stateless: JWT는 상태를 저장하지 않기 때문에 한번 만들어지면 제어가 불가능합니다. 즉, 토큰을 임의로 삭제하는 것이 불가능하므로 토큰 만료 시간을 꼭 넣어주어야 합니다.
  • Tore Token: 토큰은 클라이언트 측에서 관리해야 하기 때문에, 토큰을 클라이언트 측에서 저장해야 합니다.

Djangod에서 JWT 기반의 로그인 기능 구현하기

먼저 클라이언트측에서 아이디와 패스워드를 입력해 로그인을 하였을 때(POST) 서버측에서 JWT 토큰을 생성하여 클라이언트에 전달하는 코드는 다음과 같습니다.

class LoginView(APIView):
    def post(self, request):
        username = request.data['username']
        password = request.data['password']

        user = User.objects.filter(username=username).first()

        if user is None:
            raise AuthenticationFailed('존재하지 않는 username입니다.')

        if check_password(password, user.password):
            raise AuthenticationFailed('비밀번호가 틀렸습니다.')

        payload = {
            'id': user.id,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
            'iat': datetime.datetime.utcnow()
        }

        token = jwt.encode(payload, env('DJANGO_SECRET_KEY'), algorithm='HS256')

        response = Response()

        response.set_cookie(key='jwt', value=token, httponly=True)
        response.data = {
            'jwt': token
        }
        return response

클라이언트가 로그인에 대한 post요청을 보냈을 때, postman으로 이에 대한 결과를 확인하면 다음과 같습니다.

그리고 이제는 클라이언트에서 해당 토큰과 함께 요청을 서버로 보냈을 때, 서버가 토큰을 복호화하고 유효성 검사를 통해, 해당 유저가 맞다면 유저에 관한 정보를 응답 데이터로 보내는 코드를 작성해보겠습니다.

class LoginView(APIView):
    def get(self, request):
        token = request.COOKIES.get('jwt')

        if not token:
            raise AuthenticationFailed('Unauthenticated')

        try:
            payload = jwt.decode(token, env('DJANGO_SECRET_KEY'), algorithms=['HS256'])
        except jwt.ExpiredSignatureError:
            raise AuthenticationFailed('Unauthenticated')

        user = User.objects.get(id=payload['id'])
        serializer = UserSerializer(user).data
        return Response(serializer)

postman으로 확인하면 다음과 같습니다.

마지막으로, 로그아웃까지 해보겠습니다.

class LogoutView(APIView):
    def post(self, request):
        response = Response()
        response.delete_cookie('jwt')
        response.data = {
            'message': 'success'
        }
        return response

로그아웃까지 잘 완료된 것을 볼 수 있습니다.

추가로, 토큰의 저장 위치에 대해 언급하겠습니다.
위에서 저는 토큰을 쿠키에 저장하였는데, 이에대해 알아보겠습니다.

토큰 저장 위치?

토큰 저장 위치

서버가 토큰을 발급해주면, 브라우저에서 사용자/서버 간에 토큰이 전달되는 방식은 크게 두 가지로 나뉩니다.
첫 번째는 로그인 성공시 서버가 응답 정보에 토큰을 넣어서 전달하도록 하고, 해당 값을 웹 스토리지(localStorage 혹은 sessionStorage)에 넣고 다음부터 웹 요청을 할 때마다 HTTP 헤더 값에 넣어서 요청하는 방법입니다.
이 방법은 구현하기 쉽고 하나의 도메인에 제한되어있지 않다는 장점이 있지만, XSS 해킹 공격을 통하여 해커의 악성 스크립트에 노출이 되는 경우 매우 쉽게 토큰이 탈취될 수 있습니다. 그냥 localStorage에 접근하면 바로 토큰에 접근할 수 있기 때문입니다.

이에 대한 대안으로 두 번째 방식은 토큰을 쿠키에 넣는 것입니다.
쿠키를 사용한다고해서 세션을 관리하는 것은 아니고, 그저 쿠키를 정보 전송수단으로 사용할 뿐입니다.
이 과정에서 서버측에서 응답을 하면서 쿠키를 설정해 줄 때 httpOnly 값을 활성화를 해주면, 네트워크 통신 상에서만 해당 쿠키가 붙게 됩니다. 따라서 브라우저상에서는 자바스크립트로 토큰 값에 접근하는 것이 불가능해집니다.

이 방법의 단점은 쿠키가 한정된 도메인에서만 사용이 된다는 점입니다. 이 문제는 토큰이 필요해질 때 현재 쿠키에 있는 토큰을 사용하여 새 토큰을 문자열로 받아올 수 있게 하는 API를 구현하여 해결하면 됩니다.
또 다른 단점은 XSS의 위험에서 완벽히 해방되는 대신 CSRF 공격의 위험성이 생긴다는 점입니다. CSRF는 계정 정보를 탈취하는 것은 아니지만 스크립트를 통해 사이트의 외부에서 사이트의 API를 사용하는 것처럼 모방하는 것입니다. 또는, 사이트 내부에서 스크립트가 실행되어 원하지 않는 작업이 수행되게 할 수도 있습니다.
이러한 CSRF는 HTTP 요청 레퍼러 체크, 그리고 CSRF 토큰의 사용을 통하여 방지할 수 있습니다.

[참고]

profile
꾸준히, 열심히, 그리고 잘하자

0개의 댓글