
در پروژههای مبتنی بر API، یکی از چالشهای اصلی، پیادهسازی احراز هویت امن و قابل اطمینان برای کاربران است؛ مخصوصاً وقتی پای نگهداری توکنها به میان میآید.
در این مطلب، میخواهیم با استفاده از Django REST Framework و کتابخانهی Simple JWT، سیستمی برای مدیریت احراز هویت طراحی کنیم که هم امن باشد، هم از نظر تجربه کاربری روان و بدون دردسر.
در مسیر این آموزش با مفاهیمی مثل:
تولید و مدیریت توکنهای JWT
ذخیره توکنها در کوکی
بلکلیست کردن رفرش توکنها
آشنا خواهیم شد. هدف ما ساختن سیستمیست که بتواند هم نیازهای امنیتی را پوشش دهد و هم در پروژههای واقعی و چندفرانتی کاربردی باشد.
وارد پوشهای شوید که میخواهید پروژهی جنگو (Django) در آن ساخته شود، سپس دستورات زیر را برای ایجاد محیط مجازی و فعالسازی آن اجرا کنید:
python -m venv .venv .\.venv\Scripts\activate.ps1 # powershell
pip install django pip install djangorestframework pip install djangorestframework-simplejwt pip install djangorestframework-simplejwt[token_blacklist]
django-admin startproject config .
این دستور باعث میشه فایلهای پروژه در همین مسیر ساخته بشن، نه داخل پوشهی جدید.
python manage.py startapp accounts
در ادامه، باید اپلیکیشنهایی که در پروژه استفاده میکنیم، از جمله ماژولهای مربوط به REST و JWT را در تنظیمات Django فعال کنیم.
# config\settings.py INSTALLED_APPS = [ 'rest_framework', 'rest_framework_simplejwt.token_blacklist', 'accounts', ]
در این مرحله، مشخص میکنیم که احراز هویت پیشفرض پروژه از نوع JWT باشد تا Django REST Framework توکنهای JWT را بهعنوان روش تأیید هویت کاربران بشناسد.
# config\settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), }
در ادامه، با اضافه کردن تنظیمات مربوط به Simple JWT، مشخص میکنیم که توکنها چه مدت اعتبار داشته باشند، در زمان رفرش شدن آیا توکن قبلی بلاک شود یا نه، و هدر توکنها با چه پیشوندی ارسال شود.
# config\settings.py from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 'TOKEN_BLACKLIST_ENABLED': True, }
ابتدا باید تغییرات ایجاد شده در مدلها را با دستور makemigrations به فایلهای مایگریشن تبدیل کنیم، سپس با اجرای دستور migrate این تغییرات را به پایگاه داده اعمال کنیم.
python manage.py makemigrations python manage.py migrate
در این بخش، یک Serializer برای ثبتنام کاربران با استفاده از مدل پیشفرض User جنگو میسازیم. این Serializer دو فیلد رمز عبور (password) و تکرار آن (password2) را دریافت میکند تا صحت مطابقت رمز عبور را بررسی کند. سپس، با متد create کاربر جدید را ساخته و پسورد را به صورت امن (هش شده) ذخیره میکند.
# accounts\serializers.py from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError as DjangoValidationError from django.contrib.auth.models import User from rest_framework_simplejwt.serializers import TokenRefreshSerializer from rest_framework import serializers class RegisterSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'}) password2 = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'}) class Meta: model = User fields = ('username', 'email', 'password', 'password2') def validate(self, data): if data['password'] != data['password2']: raise serializers.ValidationError({"password2": "Passwords must match."}) try: validate_password(data['password']) except DjangoValidationError as e: raise serializers.ValidationError({'password': list(e.messages)}) return data def create(self, validated_data): user = User.objects.create( username=validated_data['username'], email=validated_data.get('email', '') ) user.set_password(validated_data['password']) user.save() return user class CookieTokenRefreshSerializer(TokenRefreshSerializer): pass
با این روش، ما امنیت رمز عبور را تضمین میکنیم و همچنین از ورود رمزهای عبور ناسازگار جلوگیری میکنیم. همچنین چون از مدل پیشفرض User استفاده شده، نیازی به تعریف مدل اختصاصی نیست و میتوانیم به سرعت فرآیند ثبتنام را پیاده کنیم.
در این مرحله، یک view برای ثبتنام کاربران میسازیم که دادههای ورودی رو از طریق RegisterSerializer پردازش میکنه و در صورت اعتبارسنجی موفق، یک کاربر جدید در دیتابیس ایجاد میشه. چون این endpoint برای کاربران مهمان (غیر لاگینشده) قابل دسترسه، سطح دسترسی اون رو AllowAny قرار میدیم.
# accounts\views.py from rest_framework import generics, permissions, status from rest_framework.response import Response from .serializers import RegisterSerializer from rest_framework_simplejwt.tokens import RefreshToken ACCESS_TOKEN_LIFETIME_SECONDS = 5 * 60 REFRESH_TOKEN_LIFETIME_SECONDS = 7 * 24 * 60 * 60 class RegisterView(generics.CreateAPIView): serializer_class = RegisterSerializer permission_classes = [permissions.AllowAny] def post(self, request, *args, **kwargs): # Register user serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() # Generate tokens refresh = RefreshToken.for_user(user) access = refresh.access_token # Response object res = Response({'detail': 'User registered and logged in.'}, status=status.HTTP_201_CREATED) # Set tokens in HttpOnly cookies res.set_cookie( key='access_token', value=str(access), httponly=True, secure=True, samesite='Lax', max_age=ACCESS_TOKEN_LIFETIME_SECONDS ) res.set_cookie( key='refresh_token', value=str(refresh), httponly=True, secure=True, samesite='Lax', max_age=REFRESH_TOKEN_LIFETIME_SECONDS ) return res
در این view، بعد از اینکه کاربر جدید با موفقیت ثبتنام میکنه، بلافاصله یک جفت توکن JWT (شامل Access و Refresh Token) برای او ایجاد میشه.
بهجای اینکه توکنها رو در بدنهی پاسخ برگردونیم (که امنیت کمتری داره)، اونها رو با استفاده از set_cookie در کوکیهای HttpOnly قرار دادیم. این یعنی:
توکنها از طریق JavaScript قابل دسترسی نیستند (جلوگیری از حملات XSS).
با تنظیم گزینهی secure=True، این کوکیها فقط در کانکشنهای HTTPS ارسال میشن.
گزینهی samesite='Lax' یا 'Strict' کمک میکنه از حملات CSRF جلوگیری بشه.
همچنین زمان انقضای کوکیها با max_age مشخص شده که باید با تنظیمات SIMPLE_JWT هماهنگ باشه.
با این کار، کاربر پس از ثبتنام بهصورت خودکار لاگین هم میشه و توکنها بهصورت امن در مرورگر ذخیره میشن، بدون نیاز به دخالت مستقیم فرانتاند برای ذخیرهسازی دستی توکن.
اما در حالت عادی، کتابخانهی Simple JWT انتظار داره که توکن JWT در هدر Authorization هر درخواست ارسال بشه.
ولی وقتی ما توکن JWT رو بهصورت امن (با HttpOnly) در کوکی ذخیره میکنیم، فرانتاند نمیتونه مستقیماً این توکن رو بخونه و داخل هدر بذاره.
برای حل این مشکل، یک middleware سفارشی مینویسیم که قبل از رسیدن request به viewها، کوکی حاوی توکن را بررسی کند و در صورت وجود، توکن را در هدر Authorization قرار دهد. اینطوری Simple JWT بدون نیاز به هیچ تغییر خاصی توکن را شناسایی میکند.
# config\middleware\jwt_cookie_auth.py class JWTTokenCookieMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): access_token = request.COOKIES.get('access_token') if access_token: request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}' return self.get_response(request)
حالا باید این میدلور رو قبل از میدلور AuthenticationMiddleware اضافه کنیم.
# config\settings.py MIDDLEWARE = [ 'config.middleware.jwt_cookie_auth.JWTTokenCookieMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', ]
خب الان که بعد از ساخت حساب کاربری توکنها رو توی کوکی تنظیم کردیم باید همین کار رو برای ویوهای رفرش توکن و لاگین انجام بدیم تا همه چیز درست کار کنه. اول رفرش توکن:
# accounts\views.py from rest_framework_simplejwt.views import TokenRefreshView from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from rest_framework.response import Response from rest_framework import status from datetime import timedelta ACCESS_TOKEN_LIFETIME_SECONDS = 5 * 60 REFRESH_TOKEN_LIFETIME_SECONDS = 7 * 24 * 60 * 60 class CustomTokenRefreshView(APIView): def post(self, request, *args, **kwargs): refresh_token = request.COOKIES.get('refresh_token') if not refresh_token: return Response({"refresh": ["Refresh token not found in cookies."]}, status=400) serializer = CookieTokenRefreshSerializer(data={'refresh': refresh_token}, context={'request': request}) try: serializer.is_valid(raise_exception=True) except TokenError as e: raise InvalidToken(e.args[0]) access_token = serializer.validated_data.get("access") refresh_token = serializer.validated_data.get("refresh") response = Response({"detail": "Tokens refreshed and stored in cookies."}) response.set_cookie( key='access_token', value=access_token, httponly=True, secure=True, samesite='Lax', max_age=ACCESS_TOKEN_LIFETIME_SECONDS ) response.set_cookie( key='refresh_token', value=refresh_token, httponly=True, secure=True, samesite='Lax', max_age=REFRESH_TOKEN_LIFETIME_SECONDS ) return response
و بعد لاگین:
# accounts\views.py from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.exceptions import InvalidToken, TokenError from rest_framework.response import Response from rest_framework import status ACCESS_TOKEN_LIFETIME_SECONDS = 5 * 60 REFRESH_TOKEN_LIFETIME_SECONDS = 7 * 24 * 60 * 60 class CustomTokenObtainPairView(TokenObtainPairView): def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) try: serializer.is_valid(raise_exception=True) except TokenError as e: raise InvalidToken(e.args[0]) access_token = serializer.validated_data.get('access') refresh_token = serializer.validated_data.get('refresh') response = Response({"detail": "Login successful. Tokens stored in cookies."}, status=status.HTTP_200_OK) response.set_cookie( key='access_token', value=access_token, httponly=True, secure=True, samesite='Lax', max_age=ACCESS_TOKEN_LIFETIME_SECONDS ) response.set_cookie( key='refresh_token', value=refresh_token, httponly=True, secure=True, samesite='Lax', max_age=REFRESH_TOKEN_LIFETIME_SECONDS ) return response
برای پیادهسازی عملیات لاگاوت در سیستم JWT که توکنها در کوکیهای HttpOnly ذخیره شدهاند، لازم است توکنهای ذخیره شده در کوکی حذف شوند و همچنین توکن رفرش بلاکلیست شود تا دیگر امکان استفاده مجدد از آن وجود نداشته باشد. این کار باعث میشود امنیت سیستم بالاتر رفته و کاربر عملاً از سیستم خارج شود.
ویوی زیر این مراحل را به صورت کامل انجام میدهد: ابتدا توکن رفرش را از کوکی دریافت میکند، سپس آن را در لیست سیاه قرار میدهد و در نهایت کوکیهای مربوط به توکنها را حذف میکند.
# accounts\views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken, OutstandingToken from rest_framework.permissions import IsAuthenticated class LogoutView(APIView): permission_classes = [IsAuthenticated] def post(self, request): try: refresh_token = request.COOKIES.get('refresh_token') if refresh_token is None: return Response({"detail": "No refresh token found"}, status=status.HTTP_400_BAD_REQUEST) token = OutstandingToken.objects.get(token=refresh_token) BlacklistedToken.objects.create(token=token) response = Response({"detail": "Logged out successfully"}, status=status.HTTP_200_OK) response.delete_cookie('access_token') response.delete_cookie('refresh_token') return response except OutstandingToken.DoesNotExist: return Response({"detail": "Invalid refresh token"}, status=status.HTTP_400_BAD_REQUEST)
با اجرای این ویو، توکن رفرش کاربر به لیست سیاه اضافه میشود و کوکیهای توکن از مرورگر حذف میشوند. این باعث میشود که دیگر نتوان از توکنهای قبلی برای دسترسی یا رفرش استفاده کرد و کاربر به طور کامل از سیستم خارج میشود.
این روش یکی از امنترین راهها برای پیادهسازی لاگاوت در سیستمهای مبتنی بر JWT است که توکنها را در کوکی HttpOnly ذخیره میکنند.
بعد از اینکه ویوهای مربوط به رجیستر، لاگین، رفرش توکن و لاگاوت را نوشتیم، باید آنها را در فایل urls.py تعریف کنیم تا بتوانیم از طریق آدرسهای مشخص شده، این عملیاتها را فراخوانی کنیم. معمولاً یک فایل urls.py در اپ مربوط به احراز هویت میسازیم و مسیرهای مورد نیاز را آنجا قرار میدهیم.
# accounts\urls.py from django.urls import path from accounts.views import RegisterView, CustomTokenObtainPairView, CustomTokenRefreshView, LogoutView urlpatterns = [ path('register/', RegisterView.as_view(), name='register'), path('login/', CustomTokenObtainPairView.as_view(), name='login'), path('refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'), path('logout/', LogoutView.as_view(), name='logout'), ]
# config\urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('accounts.urls')), ]
با این تنظیمات، API ما مسیرهای مشخصی برای عملیاتهای اصلی احراز هویت دارد. این آدرسها را فرانتاند میتواند برای ارسال درخواستهای HTTP به کار ببرد.
حالا مسیرهای API به این شکل خواهند بود:
http://127.0.0.1:8000/api/register/ http://127.0.0.1:8000/api/login/ http://127.0.0.1:8000/api/refresh/ http://127.0.0.1:8000/api/logout/
با دستور runserver پروژه رو اجرا میکنیم و پروژه معمولا روی پورت 8000 منتظر دریافت درخواست است.
python manage.py runserver

در ادامه نمونه درخواستهای ارسال شده با نرمافزار HTTPie رو قرار میبینید.




اگر هنگام اجرا با خطایی مواجه شدید یا بخشی از آموزش برایتان مبهم بود، میتوانید به کد کامل اون رو دسترسی داشته باشید: github.com/mohammad3a1eh/django-api-simple-jwt
تمام فایلها و تنظیمات بهصورت یکپارچه در آنجا قرار داده شدهاند.
در نهایت، همانطور که از عنوان مطلب «ساخت سیستم احراز هویت با Simple JWT در Django: گامبهگام تا امنیت کامل» برمیآید، هدف این آموزش فراهم کردن یک مسیر شفاف و کاربردی برای پیادهسازی این سیستم است. اما طبیعیست که امنیت کامل، نیازمند بررسی دقیقتر، تستهای امنیتی و تنظیمات نهایی متناسب با شرایط محیط اجرایی باشد.
موفق باشید.