'll Hacker

[WIL] 6주차, Django Authentication 본문

Dev/GDSC

[WIL] 6주차, Django Authentication

씨이오가 되자 2024. 11. 23. 20:11
Contents
728x90

Django Authentication이란?

HTTP 헤더에 사용자 이름과 비밀번호를 포함하여 인증하는 방식, 주로 테스트 용도로 사용 

설정이 간단하고 빠르게 구현이 가능하지만, 보안이 낮아서 HTTPS와 함께 사용해야 안전하다.
매 요청마다 자격 증명을 포함하므로, 인증 정보가 노출될 수 있다. 


Session-Based Authentication

Django의 기본 인증 방식,

사용자가 로그인하면 서버에 세션을 생성하고 세션ID를 클라이언트에 쿠키로 저장함.

💡쿠키?
웹 서버가 생성하여 웹브라우저로 전송하는 작은 정보 파일
웹 브라우저는 수신한 쿠키를 미리 정해진 기간 동안 또는 웹 사이트에서의 사용자 세션 기간 동안 저장함.
웹 브라우저는 향후 사용자가 웹 서버에 요청할 때 관련 쿠키를 첨부함.
사용자 기기의 지정된 파일("Cookies")에 쿠키를 저장

 

이후 요청마다 세션ID를 검증하여 사용자 인증을 처리함

서버 측에서 세션 관리를 통해 보안성이 높고, 세션을 사용해 다양한 사용자 데이터를 저장하고 처리하기 쉬운 반면에,

서버에서 세션 정보를 저장해야 하므로 서버에 부하가 증가할 수 있고, 확장성이 제한적이며, 클라이언트와 서버가 같은 출처에 있어야 하는 경우가 많다.


Token-Based Authentication

사용자가 로그인하면 서버에서 토큰을 발행하고

클라이언트는 이를 저장해 이후 요청할 때마다 헤더에 포함하여 인증

대표적으로 Django Rest Framework에서 사용된다.

RESTful API에 적합하며, 서버가 상태를 저장하지 않아 확장성이 좋고, 서버와 클라이언트가 다른 출처여도 사용이 가능해서 모바일, 웹 등 다양한 클라이언트에서 사용하기 좋다, 하지만, 토큰 유효 기간 설정 및 관리가 필요하며, 토큰 유출시 보안 이슈가 발생할 수 있다. 그리고 서버에 저장된 세션보다 덜 안전할 수 있다.


JWT(JSON Web Token) Authentication

토큰 기반 인증의 한 종류로, JSON 형태의 토큰을 사용해 사용자를 인증.

토큰은 서명되어 있어(토큰의 내용이 변조되지 않았음을 확인할 수 있도록 암호화된 서명이 포함) 정보의 무결성을 보장함. RESTful API에 매우 적합하며, 토큰이 상태를 갖지 않으므로 서버 확장성이 좋고, 클라이언트와 서버가 다른 출처에 있어도 사용이 가능하고, 사용자 정보를 토큰에 포함해 서버 조회를 줄일 수 있다. 하지만, 토큰이 서명되어 있으나, 암호화는 아니므로 민감한 정보를 포함해서는 안되고, 토큰이 만료되기 전까지 취소할 수 없기 때문에 관리가 어려울 수 있다.


JWT(Json Web Token)?

Json 객체에 인증에 필요한 정보들을 담은 후 비밀키로 서명한 토큰으로, 인터넷 표준 인증 방식이다.

공식적으로 인증 & 권한허가 방식으로 사용된다.

필요한 정보를 한 객체에 담아서 전달하기 때문에 세션처럼 따로 사용자 정보를 저장해둘 필요가 없으며, 한 가지 방법으로 인증할 수 있다. 인터넷 표준 인증 방식이기 때문에 대부분의 언어에서 지원하는 방식이다. 

로그인 전 - jwt 토큰 발급

  1. 사용자가 서버에 로그인 요청을 보낸다
  2. 서버는 비밀키를 사용해 json 객체를 암호화한 jwt 토큰을 보낸다
  3. jwt를 헤더에 담아 클라이언트에 보낸다.

로그인 후 - jwt 토큰 발급 후

  1. 클라이언트는 jwt 토큰을 로컬에 저장해놓는다
  2. API 호출을 할 때마다 토큰을 헤더에 실어 보낸다
  3. 서버는 토큰을 매번 확인하여 사용자가 신뢰성이 있는지 판단하고, 인증이 완료될 시 response를 보낸다.
🤔참고
  http 헤더는 기본적으로 stateless를 유지하기 때문에 이전의 request 헤더에 토큰을 넣은 것을 기억하지 못하기 때문에
  요청을 보낼 때마다 매번 헤더에 토큰을 넣어 보내야함!!

 

jwt 구조 - header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDI5IiwibmFtZSI6Imh5ZWxlZSIsImlhdCI6MTczMDkzOTAyMn0._cZsg2ZLmPGLAWikoGIX2RPkgQRgcdNhq8WYqgsweJk

이와 같이 내용을 ' . '으로 구분하여 하나의 객체로 나타낸다.

 

Header : 알고리즘, 토큰 타입

  • 해싱 알고리즘과 토큰 타입을 지정할 수 있음
  • 알고리즘은 HS256(공개키/개인키)과 RS256(대칭키)
{
  "alg": "HS256",
  "typ": "JWT"
}

 

이 정보를 base64로 인코딩 = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

참고: JSON 형태의 객체가 base64 로 인코딩 되는 과정에서 공백 / 엔터들이 사라짐.
따라서, 다음과 같은 문자열을 인코딩을 하게 됨:
{"alg":"HS256","typ":"JWT"}

 

Payload : 데이터

  • 전달하려는 데이터를 포함
  • 데이터 각각의 key를 claim이라고 부르며, 사용자가 원하는 대로 claim을 구성
  • claim에는 registered, public, private 3가지 종류가 있음 
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

이 정보를 base64로 인코딩 = eyJzdWIiOiIxMDI5IiwibmFtZSI6Imh5ZWxlZSIsImlhdCI6MTczMDkzOTAyMn0

주의: base64로 인코딩을 할 때 dA== 처럼 뒤에 = 문자가 한두개 붙을 때가 있음. 이 문자는 base64 인코딩의 padding 문자라고 부른다. JWT 토큰은 가끔 URL 의 파라미터로 전달 될 때도 있는데, 이 = 문자는, url-safe 하지 않으므로, 제거되어야 한다. 패딩이 한개 생길 때도 있고, 두개 생길 때도 있는데, 전부 지워(제거해줘도 디코딩 할 때 전혀 문제가 되지 않습니다)야한다.

 

Signature : 비밀키로 해싱된 서명 

  • 헤더와 페이로드의 문자열을 합친 후에, 헤더에서 선언한 알고리즘과 key를 이용해 암호화한 값
  • 헤더와 페이로드 단순히 base64url로 인코딩되어 있어 복호화할 수 있지만, signature는 key가 없으면 복호화 불가능 
  • 서버와 이 signature을 사용해 사용자의 유효성 검사를 함

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDI5IiwibmFtZSI6Imh5ZWxlZSIsImlhdCI6MTczMDkzOTAyMn0 <- 이거를 합쳐서 이 값은 비밀키의 값을 secret으로 해싱하고,

base64로 인코딩= _cZsg2ZLmPGLAWikoGIX2RPkgQRgcdNhq8WYqgsweJk

Django에서의 jwt token

1. jwt library 설치

pip install djangorestframework-simplejwt

 

2. settings.py에 drf jwt 관련 설정 추가

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

JWT_AUTH = {
    'JWT_SECRET_KEY': SECRET_KEY,
    'JWT_ALGORITHM': 'HS256',
    'JWT_ALLOW_REFRESH': True,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28),
}

3. 이전 앱 post 요청에 데코레이터나 코드를 이용해서 접근 제한 걸기

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('signup/', SignUpView.as_view(), name='sign_up'),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

여기서.... 막힘...............;;;;;

4. https://127.0.0.1:8000/api/token/으로 post 요청을 보내 access token과 refrest token을 받아오기

💡Access Token?
- 인증을 위한 JWT(접근에 관여)
- 유효기간이 짧다.
- Access Token만을 통한 인증 방식은 제 3자에게 탈취당할 경우 보안에 취약함
왜냐하면 토큰이 만료되기 전까지는 누구나 권한 접근이 가능해져버린다.
하지만, JWT를 발급하고 삭제하는 것은 불가능하기 때문에, 대신 토큰 유효시간을 짧게 하여 토큰 남용을 방지
하지만, 유효기간이 짧은 토큰의 경우 사용자가 그만큼 로그인을 자주해서 새롭게 토큰을 받아야하는 번거로움이 있음
그래서 유효기간을 짧게 하면서 보안강화 토큰 -> Refresh Token
💡Refresh Token?
- Access Token을 보완하기 위한 JWT(재발급에 관여)
- 유효기간이 Access Token에 비해 길다
- Access Token과 똑같은 형태의 JWT
- 서버는 로그인 성공 시 클라이언트에게 Access Token과 Refresh Token을 동시에 발급
- 서버는 DB에 Refresh Token을 저장, 클라이언트는 Access Token과 Refresh Token을 쿠키 또는 로컬스토리지에 저장하고 요청이 있을 때마다 헤더에 담아서 보냄
- 만료된 Access Token을 서버에 보내면, 서버는 같이 보냈던 Refresh Token을 DB에 있는 것과 비교해서 일치하면 다시 Access Token을 재발급하는 원리
- 사용자가 로그아웃을 하면 저장소에서 Refresh Token을 삭제하여 사용이 불가능하도록 하고 새로 로그인하면 서버에서 다시 재발급해서 DB에 저장 

 

5. 해당 access token을 헤더 넣고, 빼고 본인의 앱 view에 post 요청 보내기


Test code

장고에서는 번거로운 과정을 거치지 않고 테스트를 할 수 있도록 unittest라는 모듈을 제공하고 있다.

 

<테스트 코드가 필요한 이유>

  • 코드의 신뢰성을 높여준다
  • 테스트 코드를 통해 코드가 어떤 방식으로, 어떤 목적으로 만들어졌는지 알 수 있다
  • 한 번의 테스트로 여러가지 기능들이 동작하는지 한꺼번에 확인가능!! - 비용(시간, 돈) 절약

하지만, 파이썬이나 장고의 내장 패키지 or 모듈은 불가능 ㅠㅠ🥹🥹

장고에서 사용할 수 있는 테스트 툴은 drf test tool(https://www.django-rest-framework.org/api-guide/testing/)이 있음

이 툴은 파이썬의 unittest와 pytest를 기반으로 만든 장고 rest framework의 테스트 툴이다.

💡unittest?
python에 내장되어 있는 표준 라이브러리.
import해서 바로 사용가능 
💡pytest?
설치하고 import해서 사용

 

테스트 용도에 맞게 함수를 선언해준 뒤 url name을 통해 url 설정을 해주고,

serializer에 들어가야 하는 데이터를 self.client에 담아 post 매소드로 전달한다.

그리고 나서, 응답의 상태 코드가 view.py에 설정해둔 상태코드와 같으면 테스트 성공

 

회원가입 테스트

def testSignUp(self):
        url = reverse("sign_up")
        response = self.client.post(url, self.user_data)
        self.assertEqual(response.status_code, 200)

 

로그인 테스트 

def testSignIn(self):
        self.user = User.objects.create_user(
            username=self.user_data["username"], 
            email=self.user_data["email"],
            password=self.user_data["password"]) 
        
        url = reverse("token_obtain_pair")
        
        response = self.client.post(url, self.user_data)
        self.assertEqual(response.status_code, 200)

새로 user을 생성해준 뒤 해당 정보로 로그인 테스트를 해준다. 장고에서 setup이라는 기능이 있는데,

이 기능은 모든 테스트 메서드 이전에 실행시켜서 테스트를 하기 위한 기본 요건을 갖춰주는 아주 고마운 기능이다.

Faker 라이브러리는 파이썬 더미 데이터를 생성하기 위해 가장 많이 사용하는 라이브러리로

username, password, email 등 많은 속성들을 특성에 맞게 임의로 생성해준다.

def setUp(self):
        username = fake.user_name()
        email = fake.email()
        password = fake.password()

        self.user_data = {
            "username": username,
            "email": email,
            "password": password,
        }

 

from faker import Faker

>>> fake = Faker()
>>> fake
>>> fake.name()
'Jamie Wood'
>>> fake.name()
'Brenda Smith'
>>> fake.name()
'Susan Davis'

# 언어 설정도 가능
>>> fake = Faker("ko_KR")
>>> fake.name()
'한미경'
>>> fake.name()
'차명숙'
>>> fake.name()
'서진호'

 


실습

요구사항

  • 사이트를 이용하기 위해서는 회원가입이 필요합니다.
  • 로그인 시도 시 가입되지 않은 회원은 status code 400 과 함께 “회원가입이 필요합니다.” 메세지를 띄워주세요.
  • 로그인이 필요한 서비스와 필요하지 않은 서비스를 설정하고, 필요한 서비스에 로그인되지 않은 상태로 접근 시 경고 메세지를 보내주세요.

ㅜㅜㅠㅜㅠㅜㅠㅜㅠㅜㅠㅜㅠㅜㅠㅜ


 

참고 블로그

https://www.cloudflare.com/ko-kr/learning/privacy/what-are-cookies/

https://velopert.com/2389

https://velog.io/@pjh1011409/%EB%A1%9C%EA%B7%B8%EC%9D%B8

장고에 JWT 사용하기 : https://deku.posstree.com/ko/django/jwt/

 

728x90