JWT with COOKIE ๐Ÿช

oswaldeffยท2021๋…„ 12์›” 11์ผ
1
post-thumbnail

๐Ÿ” JWT


JSON Web Token์˜ ๊ตฌ์กฐ๋Š” HEADER, PAYLOAD, SIGNATURE๋กœ ๊ตฌ์„ฑ๋˜์–ด์žˆ๋‹ค.


์œ„์™€๊ฐ™์ด https://jwt.io ์—์„œ ๋ฐœํ–‰๋œ JWT์˜ payload๋ฅผ ๊บผ๋‚ด์–ด๋ณผ ์ˆ˜ ์žˆ๊ธดํ•˜์ง€๋งŒ
signature์˜ key๊ฐ’์„ ๋ชจ๋ฅธ๋‹ค๋ฉด ์ž„์˜๋กœ ํ† ํฐ์„ ๋ฐœํ–‰ํ•˜์—ฌ ์„œ๋ฒ„๋‹จ์—์„œ ํ•ด๋‹น์œ ์ €์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ๋ถ€์—ฌ๋ฐ›์„ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ด ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ์ด๋‹ค.

payload์— ๊ณ ์œ ์‹๋ณ„์ •๋ณด๋ฅผ ๋‹ด์„ ๊ฒฝ์šฐ,
user_id or ํŠน์ • pk๊ฐ’์— ๋Œ€ํ•ด front๋กœ๋ถ€ํ„ฐ ๋‹จ๋ฐฉํ–ฅ ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์ ์šฉ(ํ‚ค ์ŠคํŠธ๋ ˆ์นญ)ํ•˜์—ฌ ๋„˜๊ฒจ๋ฐ›๋Š”๋‹ค.

๐Ÿ”’ ํ† ํฐ๋ฐœํ–‰

from django.http import JsonResponse
from .models import User
import os
import jwt
import datetime

def jwt_publish(login_id):
    jwt_key = os.environ.get('JWT_KEY')
    jwt_expiration = datetime.datetime.utcnow() + datetime.timedelta(seconds=60*60*6)
    access_jwt = jwt.encode({'exp': jwt_expiration, 'login id': login_id}, key=jwt_key['SECRET KEY'], algorithm=jwt_key['ALGORITHM'])
    return access_jwt

def jwt_authorization(func):
    def wrapper(self, request, *args, **kwargs):
        jwt_key = os.environ.get('JWT_KEY')
        try:
            try:
                access_jwt = request.COOKIES.get('_utk')
            except:
                return JsonResponse({'message': 'GET JWT COOKIE ERROR'}, status=400)
            # decode
            payload = jwt.decode(access_jwt, key=jwt_key['SECRET KEY'], algorithms=jwt_key['ALGORITHM'])
            login_id = payload['login id']
            try:
                login_user = User.objects.get(login_id=login_id)
            except:
                return JsonResponse({'message': 'GET USER ERROR'}, status=400)
            
            request.user = login_user
            return func(self, request, *args, **kwargs)
        except jwt.ExpiredSignatureError:
            return JsonResponse({'message': 'JWTOKEN EXPIRED'}, status=401)
        except jwt.InvalidTokenError:
            return JsonResponse({'message': 'INVALID JWTOKEN'}, status=401)
    return wrapper

jwt_publish ํ•จ์ˆ˜์—์„œ๋Š” jwt๋ฅผ ๋ฐœํ–‰ํ•˜๊ธฐ ์œ„ํ•ด secret key๊ฐ’์ธ jwt_key๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๋ฐ›์•„์˜ค๊ณ  ๋งŒ๋ฃŒ์‹œ๊ฐ„(jwt_expiration)์„ ์ง€์ •ํ•ด์ค€๋‹ค.
payload๋ถ€๋ถ„์—๋Š”, dictํ˜•์‹์˜ ๊ฐ์ฒด๋กœ ์ง€์ •ํ•ด์ค€ ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ default key๊ฐ’์ธ exp๋ฅผ ํ†ตํ•ด key-value๋กœ ํ• ๋‹น์‹œ์ผœ์ฃผ๊ณ 
์ „๋‹ฌํ•˜๊ณ ์ž ํ•˜๋Š” ๊ณ ์œ ์‹๋ณ„๊ฐ’์— ๋Œ€ํ•ด(๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…์‹œencrypted๋œ ๋ฐ์ดํ„ฐ๋ฅผ)์„œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ key-value('login_id': login_id)๋กœ ๋„ฃ์–ด์ค€๋‹ค.

jwt_authorization ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ํ•จ์ˆ˜์—์„œ๋Š” ๋ฐœํ–‰๋œ jwt์— ๋Œ€ํ•ด, ํŠน์ • ๊ถŒํ•œ์ด ํ•„์š”ํ•œ API๊ฐ€ ์š”์ฒญ๋˜๋ฉด ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์— ์˜ํ•ด ๊ถŒํ•œ์ด ์กด์žฌํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„ํ•œ๋‹ค.
์ฟ ํ‚ค์— jwt๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ์ฑ„ํƒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์š”์ฒญ๋˜๋Š” API์—์„œ ์ฟ ํ‚ค๋ฅผ ์ฝ์€๋‹ค์Œ jwt_key๊ฐ’์„ ํ†ตํ•ด decodeํ•ด์ค€๋‹ค.
๊ทธ๋‹ค์Œ ์š”์ฒญํ•œ ์œ ์ €์— ๋Œ€ํ•ด ๊ณ ์œ ์‹๋ณ„๊ฐ’์„ ํ™•์ธ/์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด request.user๋กœ ๊ณ ์œ ์‹๋ณ„๊ฐ’์„ ํ• ๋‹น์‹œ์ผœ์ค€๋‹ค.

์•„๋ž˜๋Š” JWT๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์ ์šฉํ•œ ์˜ˆ์‹œ์ฝ”๋“œ์ด๋‹ค.

class item:
    '''
    '''
    @jwt_authorization
    def get(self, request, *args, **kwargs):
        order_pk = order.object.filter(user_pk=request.user)._pk
        '''
        '''
        return JsonResponse({'message': 'GET ORDER LIST SUCCESS', 'data': orders}, status=200)

์˜ˆ์‹œ์ฝ”๋“œ์™€ ๊ฐ™์ด ํŠน์ • ์œ ์ €๊ฐ€ ์ฃผ๋ฌธํ•œ ๋ฌผํ’ˆ๋“ค์— ๋Œ€ํ•œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ›์•„์˜ฌ๋•Œ,
@jwt_authorization์— ์˜ํ•ด ๋‚ด๋ถ€ payload๊ฐ’์ด ๊บผ๋‚ด์ง€๊ฒŒ ๋˜๊ณ 
์š”์ฒญ๋œ ํŠน์ • ์œ ์ €์™€ ์—ฐ๊ฒฐ๋œ DB์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ“™ SESSION๊ณผ COOKIE

์ฟ ํ‚ค์„ธ์…˜
์ €์žฅ ์œ„์น˜ํด๋ผ์ด์–ธํŠธ์„œ๋ฒ„
์ €์žฅ ํ˜•์‹ํ…์ŠคํŠธ์˜ค๋ธŒ์ ํŠธ
๋งŒ๋ฃŒ์‹œ์ ์ฟ ํ‚ค ์ €์žฅ์‹œ ์„ค์ •*๋ธŒ๋ผ์šฐ์ € ์ข…๋ฃŒ์‹œ ์‚ญ์ œ*
๋ฆฌ์†Œ์Šคํด๋ผ์ด์–ธํŠธ ๋ฆฌ์†Œ์Šค์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค
์šฉ๋Ÿ‰์ด 300๊ฐœ, ํ•˜๋‚˜์˜ ๋„๋ฉ”์ธ ๋‹น 20๊ฐœ, ํ•˜๋‚˜์˜ ์ฟ ํ‚ค ๋‹น 4kb(4096byte)์„œ๋ฒ„์šฉ๋Ÿ‰๋งŒํผ ๊ฐ€๋Šฅ
์†๋„์„ธ์…˜๋ณด๋‹ค ๋น„๊ต์  ๋น ๋ฆ„์ฟ ํ‚ค๋ณด๋‹ค ๋น„๊ต์  ๋Š๋ฆผ
๋ณด์•ˆ์„ธ์…˜๋ณด๋‹ค ๋น„๊ต์  ์•ˆ์ข‹์Œ์ฟ ํ‚ค๋ณด๋‹ค ๋น„๊ต์  ์ข‹์Œ

์œ ์ €๊ฐ€ ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด ํŠน์ •๊ถŒํ•œ ๋ฐ ๋ณธ์ธ์ธ์ฆ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์›น์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•  ๋•Œ,
์ฟ ํ‚ค์˜ ์†์„ฑ๋“ค์„ ํ†ตํ•ด ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•œ๋‹ค๋ฉด ์„ธ์…˜์— ๋น„ํ•ด ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์ด์ ๋“ค์ด ๋งŽ๋‹ค.

๊ทธ ์˜ˆ๋กœ, ์‡ผํ•‘๋ชฐ์˜ ๊ฒฝ์šฐ ์žฅ๊ธฐ๊ฐ„ ๋กœ๊ทธ์ธํ•˜๋Š” ๊ฒฝ์šฐ๋ณด๋‹ค ์—ฌ๋Ÿฌ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ ‘์†ํ•˜์—ฌ ์ƒํ’ˆ์„ ๋ณด๊ฑฐ๋‚˜ ๊ตฌ๋งค๋‚ด์—ญ ๋ฐ ๊ฒฐ์ œ๊ธฐ๋Šฅ์— ๋Œ€ํ•˜์—ฌ ๊ถŒํ•œ์ธ์ฆ์— ๋Œ€ํ•ด ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ฃผ๋ฅผ ์ด๋ฃจ๋‹ค๋ณด๋‹ˆ ์ฟ ํ‚ค ์‚ฌ์šฉ์—์„œ ๊ทธ ์ด์ ์ด ์žˆ๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

  1. expire
    ์ฟ ํ‚ค ๋งŒ๋ฃŒ๊ธฐํ•œ์„ ์„ค์ •ํ•˜์—ฌ ํŠน์ • ์œ ์ €์— ์˜ํ•œ ๊ถŒํ•œ์ธ์ฆ๊ธฐํ•œ์„ ๋ถ€์—ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
    JWT๋ฅผ ์‚ฌ์šฉํ• ๋•Œ ๋กœ๊ทธ์•„์›ƒํ•  ๊ฒฝ์šฐ ์ฟ ํ‚ค ๋งŒ๋ฃŒ๊ธฐํ•œ๊ณผ ์ƒ๊ด€์—†์ด blacklist๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ด๋ฏธ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ๋œ ํ† ํฐ๊ฐ’์— ๋Œ€ํ•ด ๊ถŒํ•œ์ด ์—†๋Š” ์ƒํƒœ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
  1. domain
    ํ•˜๋‚˜์˜ ๋„๋ฉ”์ธ์•ˆ์—์„œ ์—ฌ๋Ÿฌ ์„œ๋ฒ„๋ฅผ(for example: Apple.com, api.Apple.com, admin.Apple.com)๋‘๋Š” ๊ฒฝ์šฐ ์ฟ ํ‚ค๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•œ๋‹ค.
  1. secure
    https์™ธ์˜ ํ™˜๊ฒฝ์—์„œ ์ฟ ํ‚ค๊ฐ’์„ ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š”๋‹ค
  1. httponly
    httponly = False์ผ ๋•Œ๋Š” Javascript์—์„œ ์ฟ ํ‚ค๊ฐ’์— ๋Œ€ํ•œ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ
    httponly = True์ผ ๋•Œ๋Š” XSS(Cross-Site Scripting)๋กœ ์ธํ•ด ์ทจ์•ฝํ•œ ๋ถ€๋ถ„์ด ์ƒ๊ธธ์ง€๋ผ๋„ ์ฟ ํ‚ค๊ฐ’์˜ ํƒˆ์ทจ๋Š” ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.
    (XSS๋ž€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ผ์–ด๋‚˜๋Š” ์ทจ์•ฝ์ ์œผ๋กœ ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹Œ ๊ถŒํ•œ์ด ์—†๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›น ์‚ฌ์ดํŠธ์— ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๊ณต๊ฒฉ ๊ธฐ๋ฒ•์ด๋‹ค, XSS์™€ CSRF๊ณต๊ฒฉ์˜ ์ฐจ์ด์ ๋„ ๊ณง...)
  1. samesite
    ํ”„๋ก ํŠธ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๊ฐ๊ฐ์˜ ์„œ๋ฒ„๋ฅผ ์šด์˜ํ• ๋•Œ, ๊ฐ๊ฐ์˜ domain์— ๋Œ€ํ•ด์„œ ์ œํ•œ์‚ฌํ•ญ์„ ์„ค์ •ํ•˜๋Š” ์†์„ฑ์ด๋‹ค.
    samesite์— ๋Œ€ํ•ด์„œ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ช…์‹œ๋œ๋ฐ” ์—†์ด default๊ฐ’์€ None๊ฐ’์ด์—ˆ์ง€๋งŒ,
    20๋…„ 2์›” 4์ผ ๋ฐฐํฌ๋œ ํฌ๋กฌ80์—์„œ๋Š” samesite์˜ default๊ฐ’์ด lax๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค.
  • if samesite = None:
    ์ด์ „์—๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ธ Apple.com -> Banana.com ์ฟ ํ‚ค๊ฐ€ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์—ˆ์œผ๋‚˜ ํฌ๋กฌ80์—์„œ๋Š” ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
  • if samesite = lax:
    ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ด์–ด๋„ ์ œํ•œ์ ์ธ http method์— ๋Œ€ํ•ด์„œ ์˜ˆ) ์„œ๋ฒ„์˜ ์žฌ์›(ํ˜น์€ ์ƒํƒœ)์— ๋Œ€ํ•ด ์˜ํ–ฅ์„ ๋ผ์น˜์ง€ ์•Š๋Š” get์š”์ฒญ์— ๋Œ€ํ•ด์„œ๋Š” ์ฟ ํ‚ค์ด๋™์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • if samsite = strict:
    resource๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋„๋ฉ”์ธ๊ณผ ์‚ฌ์šฉํ•˜๋Š” ๋„๋ฉ”์ธ์‚ฌ์ด์— ๋„๋ฉ”์ธ์ด ์ผ์น˜ํ•˜์—ฌ์•ผ ์ฟ ํ‚ค๊ฐ€ ์ด๋™ํ•œ๋‹ค.

*{domain}๊ฐ’์€ ๋“ฑ๋กํ•œ ๋„๋ฉ”์ธ๋ช…

# Django https & domain ํ™˜๊ฒฝ์—์„œ ์ฟ ํ‚ค ์„ค์ •
from .base import *
import os

DEBUG = False

SESSION_COOKIE_SECURE = True
SESSION_COOKIE_DOMAIN = '{domain}.com'
SESSION_COOKIE_SAMESITE = 'lax'

CSRF_COOKIE_SECURE = True
CSRF_COOKIE_DOMAIN = '{domain}.com'
CSRF_COOKIE_SAMESITE = 'lax'
CSRF_TRUSTED_ORIGINS = ['api.{domain}.com', 'https://api.{domain}.com']

DATABASES = os.environ.get('DATABASES')

ALLOWED_HOSTS += [
    '{domain}.com',
    'api.{domain}.com'
]

CORS_ALLOWED_ORIGINS += [
    'https://{domain}.com',
    'https://admin.{domain}.com',
]

CORS_ALLOW_HEADERS += [
    # etc
]
# JsonResponse๋กœ ์ฟ ํ‚ค ๋ฐœ๊ธ‰ํ•˜๊ธฐ
from django.http import JsonResponse

'''
'''

class CustomerView(View):
    
    
    @csrf_exempt
    @require_http_methods(['POST'])
    def signup(request):
    	access_jwt = jwt_publish(user_id)
    	'''
    	'''
	response = JsonResponse({'message': '{domain} signup success', json_dumps_params={'ensure_ascii':False}, status=201)
        response.set_cookie(
            key='_utk', # ํ‚ค๊ฐ’ ์ง€์ •
            value=access_jwt,
            expires=datetime.timedelta(hours=6), # ์ฟ ํ‚ค ๋งŒ๋ฃŒ๊ธฐํ•œ ์„ค์ •
            path="/",
            domain='{domain}.com',
            secure=True, 
            httponly=True,
            samesite="lax",
        )
        
        return response

etc

์ด์™ธ์˜ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” simple-jwtํŒจํ‚ค์ง€๋ฅผ ์ด์šฉํ•˜๋ฉด header๋ฅผ ํ†ตํ•ด ํ† ํฐ์ธ์ฆ ์ฒ˜๋ฆฌ ๋ฐ ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€