diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c8da6ef..fd15e57 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -15,7 +15,7 @@ services: volumes: - .:/backend # command : 컨테이너가 띄워질 때 실행하는 명령어 / 서버실행 -# command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000" + # command: sh -c "python manage.py makemigrations && python manage.py migrate && python manage.py runserver 0.0.0.0:8000" command: "gunicorn gtd.wsgi --preload --bind 0.0.0.0:8000 --timeout 240" restart: on-failure ports: @@ -60,6 +60,14 @@ services: networks: - gtd + redis: + image: "redis:alpine" + container_name: redis + ports: + - 6379:6379 + networks: + - gtd + # prometheus: # image: prom/prometheus # volumes: diff --git a/gtd/settings.py b/gtd/settings.py index 6ba7814..f68eb94 100644 --- a/gtd/settings.py +++ b/gtd/settings.py @@ -90,8 +90,8 @@ SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), - 'ROTATE_REFRESH_TOKENS': False, - 'BLACKLIST_AFTER_ROTATION': False, + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, 'UPDATE_LAST_LOGIN': False, 'ALGORITHM': 'HS256', @@ -250,4 +250,16 @@ 'PATCH', 'POST', 'PUT', -) \ No newline at end of file +) + +# redis +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + diff --git a/requirements.txt b/requirements.txt index 05d47eb..2f11749 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,6 @@ django-celery-results==2.5.1 gevent==23.9.1 openai==1.7.0 django-prometheus==2.3.1 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +redis==5.0.1 +django-redis==5.4.0 \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index bef51e9..e8f467e 100644 --- a/users/urls.py +++ b/users/urls.py @@ -5,5 +5,5 @@ urlpatterns = [ path('register/', RegisterAPIView.as_view()), # 회원가입 path("auth/", AuthAPIView.as_view()), # post - 로그인, delete - 로그아웃, get - 유저정보 - path('auth/refresh', TokenRefreshView.as_view()), # 토큰 재발급 + path('auth/refresh/', TokenRefreshView.as_view()), # 토큰 재발급 ] diff --git a/users/views.py b/users/views.py index 32e432b..bad7679 100644 --- a/users/views.py +++ b/users/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets, status from rest_framework.permissions import IsAuthenticated -# Create your views here. + import jwt from gtd.settings import * @@ -12,6 +12,8 @@ from django.contrib.auth import authenticate from django.shortcuts import get_object_or_404 +from django.core.cache import cache +from rest_framework_simplejwt.tokens import UntypedToken # swagger 관련 from rest_framework.views import APIView @@ -20,7 +22,6 @@ # from drf_yasg import openapi - class RegisterAPIView(APIView): @swagger_auto_schema(request_body=SwaggerRegisterPostSerializer) # 회원가입 @@ -86,12 +87,11 @@ def get(self, request): serializer = SignSerializer(instance=user) return Response(serializer.data, status=status.HTTP_200_OK) - - except(jwt.exceptions.ExpiredSignatureError): + except jwt.exceptions.ExpiredSignatureError: # 토큰 만료 시 토큰 갱신 - data = {'refresh': request.COOKIES.get('refresh', None)} serializer = TokenRefreshSerializer(data=data) + if serializer.is_valid(raise_exception=True): access = serializer.data.get('access', None) refresh = serializer.data.get('refresh', None) @@ -99,41 +99,54 @@ def get(self, request): pk = payload.get('user_id') user = get_object_or_404(User, pk=pk) serializer = SignSerializer(instance=user) + res = Response(serializer.data, status=status.HTTP_200_OK) res.set_cookie('access', access) res.set_cookie('refresh', refresh) return res + raise jwt.exceptions.InvalidTokenError - except(jwt.exceptions.InvalidTokenError): + except jwt.exceptions.InvalidTokenError: # 사용 불가능한 토큰 return Response(status=status.HTTP_400_BAD_REQUEST) - @swagger_auto_schema(request_body=SwaggerLoginPostSerializer) + def put(self, request): + flag = request.data.get('flag') + key = request.data.get('key') + + if flag == 'set': + value = request.data.get('value') + cache.set(key, value) + elif flag == 'get': + res = cache.get(key) + return Response({key : res}, status=status.HTTP_200_OK) + + return Response({'msg': 'success'}, status=status.HTTP_200_OK) + # 로그인 def post(self, request): - # 유저 인증 user = authenticate( email=request.data.get("email"), password=request.data.get("password") ) if user is not None: - serializer = SignSerializer(user) - # 토큰 접근 token = TokenObtainPairSerializer.get_token(user) refresh_token = str(token) access_token = str(token.access_token) + + # 리프레쉬 토큰을 Redis에 저장 + cache.set(user.id, refresh_token, timeout=int(token.lifetime.total_seconds())) + res = Response( { - # "user": serializer.data, "message": "login success", "token": { "access": access_token, "refresh": refresh_token, - }, + } }, status=status.HTTP_200_OK, ) - # 토큰, 쿠키에 저장 res.set_cookie("access", access_token, httponly=True) res.set_cookie("refresh", refresh_token, httponly=True) return res @@ -142,10 +155,26 @@ def post(self, request): # 로그아웃 def delete(self, request): + access_token = request.COOKIES.get('access') + refresh_token = request.COOKIES.get('refresh') + user = User.objects.filter(email=request.data.get("email")).first() + + # access 토큰 검증 + try: + UntypedToken(access_token) + except jwt.exceptions.InvalidTokenError: + return Response({'message': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST) + # 쿠키에 저장된 토큰 삭제 response = Response({ "message": "Logout success" }, status=status.HTTP_202_ACCEPTED) response.delete_cookie("access") response.delete_cookie("refresh") + + # Redis에서 리프레쉬 토큰 삭제 + cache.delete(user.id) + return response + +