로그인 구현 - Django(JWT)

박정호·2022년 8월 18일
8

Portfolio

목록 보기
2/6
post-thumbnail

🚀 Start

  • 현재 내가 진행하고 있는 프로젝트에 로그인 기능을 구현해보자. 클라이언트는 react를, 서버는 Django를 사용하고 있고, 우선 Django에서 JWT를 세팅해보자.

🔐 인증 & 인가

인증

  • 입력한 ID와 Password를 통해 사용자의 신원을 검증하는 절차
  • 인증을 위해, 사전에 사용자의 정보들을 생성하고 저장할 수 있는 기능 필요
  • ex) 입력한 ID와 Password를 DB에 저장된 내용과 비교

인가

  • 로그인 후 사용자가 요청한 request를 실행할 권한을 부여하는 과정
  • ex) 로그인 성공 시 access token을 클라이언트에게 전송 로그인 성공 후 다음 request부터는 access token을 첨부해서 서버에 요청 전송 서버는 access token을 통해 사용자의 권한을 확인 해당 권한을 가지고 있으면 요청을 처리 해당 권한을 가지고 있지 않으면 Unauthorized Response(401) 혹은 다른 에러 코드를 보냄

참조
인증,인가
https://www.incodom.kr/DJango/DRF-JWT

⭐️ JWT(Json Web Token)

  • '토큰 자체에 유저의 정보가 담겨있다'는 특징
  • 클라이언트와 서버, 서비스와 서비스 사이 통신 시 권한인가(Authorization)를 위해 사용하는 토큰
  • URL에 대해 안전한 문자열로 구성되어 있음

💡중요!

  • session 인증은 모든 유저의 정보와 session 정보를 서버에서 관리
  • JWT 인증에서는 토큰에 유저의 정보를 담는다.(서버에 인증정보 저장x)

1️⃣ JWT 탄생

Session기반 인증

  • JWT을 사용하기 전까지는 주로 session을 이용하여 인증이 이루어졌다.

Session 인증 동작과정

  1. 클라이언트에서 사용자의 인증 정보를 서버에 전달.
  2. 서버는 인증을 처리한 뒤 해당 user에 대해 session을 생성.
  3. session 정보는 서버에 저장되고, 클라이언트는 session id를 받아 브라우저에 저장.
  4. 클라이언트는 이후 이루어지는 요청에 session id를 이용.
  5. 서버는 전달 받은 session id를 이용하여 저장 중인 session 정보로 인증을 처리.
  6. 만약 session id가 만료되었을 경우에는 1번 과정부터 다시 이루어짐.

Django에서 제공하는 session 인증

: Django에서 기본적으로 제공하는 session 인증을 사용해보면 django_session 테이블이 생성되고 그 안에 session_key, session_data, expire_date와 같은 필드로 session 정보가 저장됩니다.

💡중요! session 정보를 서버에서 관리
: 클라이언트도 브라우저에 session id를 저장하여 사용하지만 session id 자체는 중요한 정보가 담겨있지 않은 일종의 임시 비밀번호이며 실제 session의 정보를 관리하는 것은 전적으로 서버의 역할

session인증 단점

  • 매 요청마다 인증을 위해 데이터베이스를 탐색해야 한다는 점
  • 캐시 등을 이용하여 탐색 과정을 최적화 할 수 있지만 기본적으로 session 정보가 어딘가에 저장되는 구조
  • session 정보를 저장하는 데이터베이스가 분산되어 있을 경우에는 각각의 데이터베이스를 탐색해야 하고, MSA로 설계된 경우 각 어플리케이션 마다의 session 정보를 통합으로 관리하는 것도 쉽지 않다.

2️⃣ JWT 동작과정

  1. (session 인증과 마찬가지로) 클라이언트에서 사용자의 인증 정보를 서버에 전달.
  2. 서버는 인증 정보로 인증을 처리하고 (session 대신) JWT를 생성하여 클라이언트에 전달.
  3. 클라이언트는 JWT를 브라우저에 저장.
  4. 클라이언트는 이후 이루어지는 요청에 JWT를 이용.
  5. 서버는 JWT를 검증하여 인증을 처리.
  6. JWT가 만료되면 토큰을 refresh.

💡중요! session 인증의 동작과정과의 차이점

  • 3번 과정에서 session 정보가 서버에 저장되는 반면 JWT 인증에서는 서버에 아무것도 저장되지 않는다.
  • 5번 과정에서 session은 확인을 위해 데이터 베이스를 탐색하지만, JWT 인증에서는 토큰을 바로 검증.
    (DB 탐색과정 필요없으므로 -> 별도의 테이블 생성하지 않아도 됨
    Why? 토큰 안에 유저정보가 담겨있어서, 별도의 데이터 및 저장 공간이 필요하지 않음.)

3️⃣ JWT 구성

  • JWT는 .을 이용하여 크게 세 개의 영역(Header.Payload.Signature) 구성

👉 Header

  • JWT의 메타정보를 나타낸다.
  • token의 타입을 정의하고(typ), 어떤 signing 알고리즘이 쓰였는지(alg)를 나타냄.
    -> typ: 토큰 유형 (JWT)
    -> alg : 어떤 해싱 함수를 사용할 것인지
    (이 알고리즘은 토큰을 검증할 시 사용되는 signature 부분에서 사용됨)
  • 해싱 함수: 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하여 암호화하는 함수

ex) JSON 정보는 Base64Url로 인코딩 되어 JWT의 첫 번째 영역이 된다.

{
      "typ": "JWT",
      "alg": "HS256"        
    }

👉 Payload

  • 토큰이 만료되는 시간, 유저의 정보와 같은 실질적인 데이터를 담는 영역(토큰에 담을 정보)
  • 하나의 정보를 클레임(claim)이라 부름 (따라서 Payload에 있는 속성들은 클레임 셋이라 부름)
  • 클레임은 name: value의 한 쌍으로 이루어짐
  • 클레임 셋은 토큰 생성자(클라이언트)의 정보, 생성 일시 등이나 클라이언트와 서버 간 전송되는 데이터가 들어있음

ex) payload 역시 Base64Url로 인코딩 되어 JWT의 두 번째 영역이 된다.

{
  "token_type": "access",
  "exp": 1649145719,
  "jti": "1foo2jwt3id4",
  "user_id": 123
}

👉 Signature

  • signature가 만들어지기 위해서는 인코딩 된 header, 인코딩 된 payload, secret이 필요
  • 인코딩 된 header, payload, secret을 합친 뒤 이를 header에 지정한 알고리즘으로 해싱

잠깐)
Payload영역에 담긴 유저 정보는 인코딩만 되어 있고 별도의 암호화 처리가 되어있지 않다. (= 쉽게 디코딩 될 수 있고 변조 될 수 있다.)
but, payload에는 유저를 특정할 수 있는 user id만 담았기 때문에 토큰이 디코딩 되어 user id가 노출되는 것은 보안상 큰 문제가 X
So, 변조의 문제만 해결하면 되고, 그 문제를 signature 영역에서 해결하는 것!!

  • Simple JWT에서는 secret으로 Django 프로젝트마다 사용하는 secret_key를 기본으로 이용.
    -> setting.py에서 확인 가능!

4️⃣ JWT 전달 & 검증

  • JWT는 클라이언트에 전달 되었다가 이후 요청에 HTTP Header에 담겨서 서버로 전달.
  • 서버가 JWT를 검증하는 과정은 JWT가 생성될 때와 마찬가지로 header, payload 그리고 secret을 이용하여 signature를 해싱한 뒤 전달받은 JWT의 signature와 같은지 확인
  • payload가 변조 되었다면 클라이언트에서 받은 signature와 서버에서 해싱한 signature가 다를 것

마침내 토큰이 검증되면 서버는 payload에 담긴 유저의 정보를 통해 유저를 특정하고 인증된 유저로 처리합니다.
그렇기 때문에 JWT 인증은 별도의 데이터베이스 탐색 없이도 빠르게 유저 인증을 수행할 수 있습니다

5️⃣ Django에서 JWT 사용 방법

  • djangorestframework-simplejwt(djangorestframework-jwt 는 업데이트 중단)
  • djangorestframework–jwt: django의 Restful API에서 JWT를 쉽게 구현할 수 있게 도와주는 모듈

👉 JWT(Json Web Token)인증 설치

python -m pip install djangorestframework-simplejwt

👉 setting.py에 코드 설정

from datetime import timedelta

INSTALLED_APPS = [
    ...
    # simple-jwt 추가해주기 
    'rest_framework_simplejwt',
]


REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        ...
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
    ...
}


# 추가적인 JWT_AUTH 설정
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

SIMPLE_JWT 기타설정
https://velog.io/@auddwd19/DRF-simpleJWT-%EA%B8%B0%ED%83%80%EC%84%A4%EC%A0%95

👉 view.py에 custom token을 설정할 수 있는 코드를 작성

-> 토큰에 담긴 사용자의 정보를 의미하는 claim 을 커스터마이징
-> Serializer 를 활용하여 simplejwt 에서 제공하는 기본 정보 이외에 우리가 포함하고 싶은 정보를 토큰에 추가적으로 넣기 가능.(우리가 클라이언트단에서 입력할 ID/PW)

  • simple JWT 공식 사이트에 나와 있는 Customizing token claims 내용은 다음과 같다.
    -> TokenObtainPairView 및 TokenObtainSlidingView 보기에 의해 생성된 웹 토큰에 포함된 클레임을 사용자 지정하려면 원하는 보기에 대한 하위 클래스와 해당 직렬 변환기에 대한 하위 클래스를 만듭니다. 보기에서 생성된 새로 고침 토큰과 액세스 토큰 모두에 사용자 지정된 클레임이 표시됩니다. 이는 위의 get_token 메서드가 보기에 대한 새로 고침 토큰을 생성하고 다시 보기의 액세스 토큰을 생성하는 데 사용된다는 사실에서 비롯됩니다.
    표준 토큰 보기와 마찬가지로 하위 클래스 보기에 대한 URL 경로도 포함해야 합니다.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView


class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # Add custom claims
        token['username'] = user.username
        # ...

        return token

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer
  • 커스터마이징된 token의 username을 입력하면 payload에 해당값이 출력되는 것을 확인 가능

참조

👉 urls.py에 MyTokenObtainPairView 등록, TokenRefreshView 등록

  • TokenRefreshView: simplejwt 라이브러리에서 제공하는 refresh token으로 access token 재발급하는 뷰
from .views import MyTokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
 	...
    path('token/',  MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]   
  • http://127.0.0.1:8000/api/token/ 접속 시 다음과 같은 화면 출력

  • createsuperuser로 아이디,비번 생성 후 post하면 refresh token과 access token을 확인할 수 있다.

  • refresh token을 http://127.0.0.1:8000/api/token/refresh/로 이동하여 토큰을 입력하면 새로운 access token을 받게 된다.

  • refresh된 토큰 이전의 토큰을 붙여넣게 되면 블랙리스트의 보안으로 다음과 같이 토큰이 출력되지 않는다.

👉JWT 장점 & 단점

장점

JWT 의 주요한 이점은 사용자 인증에 필요한 모든 정보는 토큰 자체에 포함하기 때문에 별도의 인증 저장소가 필요없다는 것이다. 분산 마이크로 서비스 환경에서 중앙 집중식 인증 서버와 데이터베이스에 의존하지 않는 쉬운 인증 및 인가 방법을 제공한다. 개별 마이크로 서비스에는 토큰 검증과 검증에 필요한 비밀 키를 처리하기위한 미들웨어가 필요하다. 검증은 서명 및 클레임과 같은 몇 가지 매개 변수를 검사하는 것과 토큰이 만료되는 경우로 구성된다.토큰이 올바르게 서명되었는지 확인하는 것은 CPU 사이클을 필요로하며 IO 또는 네트워크 액세스가 필요하지 않으며 최신 웹 서버 하드웨어에서 확장하기가 쉽다.

JWT 사용을 권장하는 몇 가지 이유는 다음과 같다.

  • URL 파라미터와 헤더로 사용
  • 수평 스케일이 용이
  • 디버깅 및 관리가 용이
  • 트래픽 대한 부담이 낮음
  • EST 서비스로 제공 가능
  • 내장된 만료
  • 독립적인 JWT

단점

  • 토큰은 클라이언트에 저장되어 데이터베이스에서 사용자 정보를 조작하더라도 토큰에 직접 적용할 수 없다.
  • 더 많은 필드가 추가되면 토큰이 커질 수 있다.
  • 비상태 애플리케이션에서 토큰은 거의 모든 요청에 대해 전송되므로 데이터 트래픽 크기에 영향을 미칠 수 있다.

참조: http://www.opennaru.com/opennaru-blog/jwt-json-web-token/

🔑 Access Token & Refresh Token

👉 Access Token

  • 인증을 위한 JWT(접근에 관여)
  • 유효기간이 짧다
  • Access Token만을 통한 인증 방식은 제 3자에게 탈취당할 경우 보안에 취약하다.

💡중요!
Access Token은 발급 이후에 서버에 저장되지 않고 토큰 자체로 검증을 하며 사용자 권한을 인증하기 때문에 탈취 당한다면 토큰이 만료되기 전까지는 누구나 권한 접근이 가능해져버리는 것이다.
So, JWT를 발급하고 삭제하는 것은 불가능하기 때문에, 대신 토큰 유효시간을 짧게하여 토큰 남용을 방지하는 것이 해결책이 될 수 있다.
But, 유효기간이 짧은 토큰의 경우 사용자가 그만큼 로그인을 자주해서 새롭게 토큰을 발급받아야한다.
Finally, 유효기간을 짧게 하면서 보안을 강화할 수 있는 방법은 없을까? -> Refresh Token

👉 Refresh Token

  • Access Token을 보완하기 위한 JWT(재발급에 관여)
  • 유효기간이 Access Token에 비해 길다.
  • Access Token과 똑같은 형태의 JWT

중요!
1. 서버는 로그인 성공 시 클라이언트에게 Access Token과 Refresh Token을 동시에 발급
2. 서버는 DB에 Refresh Token을 저장, 클라이언트는 Access Token과 Refresh Token을 쿠키,또는 로컬스토리지에 저장하고 요청이 있을 때마다 헤더에 담아서 보낸다.
3. 만료된 Access Token을 서버에 보내면, 서버는 같이 보내진 Refresh Token을  DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 원리
4. 사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장

잠깐) 토큰 유효시간 설정

  • setting.py에서 설정 가능!
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=90),
	...

참조: https://inpa.tistory.com/entry/WEB-📚-Access-Token-Refresh-Token-원리-feat-JWT [👨‍💻 Dev Scroll:티스토리]

👉 access token & refesh token 동작과정

👉 BlackList 기법

앞서 살펴본 refresh token으로 보안적인 문제를 해결할 수 있었지만, refresh token이 탈취될 수도 있는 보안적인 이슈가 존재한다.
-> 정해져 있는 유효기간이 끝나지 않는 이상 토큰방식은 토큰을 강제로 만료시킬 수 없는 방식

So, 유저의 로그아웃을 통해 더이상 필요없는 토큰이나 악의적으로 탈취된 token을 서버에서 사용할 수 없도록 관리를 해주는 방법(= refresh token이 blacklist 됨)

setting

<settings.py>
INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt.token_blacklist',
]

SIMPLE_JWT = {
    ...
    'BLACKLIST_AFTER_ROTATION': True,
    ...
}

참조
블랙리스트에 대한 설명

참고: 진짜 잘 정리된 사이트
JWT란?https://inpa.tistory.com/559#JWT%EC%9D%98_Access_Token_/_Refresh_Token_%EB%B0%A9%EC%8B%9D
Access& Refresh Token
https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Access-Token-Refresh-Token-%EC%9B%90%EB%A6%AC-feat-JWT

참조
코드 구현
https://www.youtube.com/watch?v=xjMP0hspNLE
JWT
https://www.qu3vipon.com/django-jwt

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

2개의 댓글

comment-user-thumbnail
2023년 1월 17일

Django BE로 사이드프로젝트 백엔드 구성중이었는데 jwt 구현에 대해서 잘 읽고갑니다!!

답글 달기
comment-user-thumbnail
2024년 2월 6일

정말 잘 읽고 갑니다. 감사합니다!

답글 달기