مقدمه پیوند به عنوان
احراز هویت همیشه یکی از مهمترین بخشهای توسعه API بوده که از دسترسی غیرمجاز به منابع حساس جلوگیری میکنه. روشهای مختلفی برای این کار وجود داره، ولی استفاده از توکنهایJSON Web Token (JWT)
، به خاطر سبک بودن، انعطافپذیری و مخصوصاً stateless
بودن بسیار پرطرفداره. یک نکته مهم در استفاده از JWT، نحوه ذخیرهسازی توکن هاست. اگر توکنها رو در یک جای ناامن مثل localStorage
ذخیره کنیم، احتمال سرقت یا سوءاستفاده از اطلاعات بالا میره.
در این مقاله قصد داریم در مورد نحوه پیادهسازی سیستم احراز هویت با JWT
و کوکی HttpOnly
صحبت کنیم تا بتونیم امنیت APIهامون رو افزایش بدیم.

JWT دقیقا چیه؟ پیوند به عنوان
JWT یا همون JSON Web Token
یک استاندارد برای انتقال امن اطلاعات بین سیستمهای مختلف است. این اطلاعات به صورت توکن رمزنگاریشده ردوبدل میشن که میتونن شامل اطلاعات مختلفی مثل شناسه کاربر، زمان انقضا و … باشن. توکنهای JWT از سه بخش اصلی تشکیل شدن:
- Header: شامل اطلاعاتی درباره نوع توکن و الگوریتم رمزنگاریه.
- Payload: دادههای اصلی مثل شناسه کاربر یا نقش رو نگه میداره.
- Signature: با استفاده از یک کلید خصوصی تولید میشه و مطمئن میشه که محتوا دستکاری نشده.

اما چرا باید از JWT استفاده کنیم؟ مهمترین مزیتش اینه که stateless هست، یعنی نیازی نیست سرور برای احراز هویت، اطلاعات توکن رو در دیتابیس ذخیره کنه. این باعث میشه سیستم مقیاسپذیرتر بشه و از کوئریهای اضافی به دیتابیس جلوگیری بشه. سیستم JWT از دو نوع توکن استفاده میکنه:
- Access Token: برای احراز هویت در APIها استفاده میشه و عمر کوتاهی داره تا در صورت لو رفتن، خطر کمتری داشته باشه.
- Refresh Token: طول عمر بیشتری داره و به کاربر اجازه میده بدون لاگین مجدد، Access Token جدید بگیره.
برای اطلاعات بیشتر در این مورد میتونین به سایت jwt.io سر بزنین.
توکنهای JWT رو کجا ذخیره کنیم؟ پیوند به عنوان
زمانی که از JWT برای احراز هویت استفاده میکنیم، باید توکنهای کاربر رو ذخیره کنیم تا در هر درخواست به سرور ارسال بشه. حالا سوال اینه که:
اکثراً به دلیل راحتی، توکنها رو در localStorage
ذخیره میکنن. اما این روش مشکلاتی داره. یکی از بزرگترین خطرات این کار حملات XSS
هست که به طور کلی در این حمله، اگر مهاجم بتونه یه کد مخرب رو در مرورگر قربانی اجرا کنه، به راحتی به localStorage
دسترسی پیدا میکنه و توکنهای امنیتی رو به سرقت میبره!
در نتیجه، بهترین کار استفاده از HttpOnly Cookie
هست. این نوع کوکی توسط سرور تنظیم میشه و مرورگر به طور خودکار توکنها رو در درخواستهای بعدی ارسال میکنه. مهمتر از همه، چون این کوکیها خارج از دسترس جاوااسکریپت هستن، خطر سرقت توکن از طریق XSS
وجود نداره.
البته ذخیره توکنهای JWT
در کوکی به تنهایی کافی نیست و ممکنه سیستم رو در برابر حملات CSRF
آسیبپذیر کنه. در بخش بعدی مقاله درباره جلوگیری از حملات CSRF
هم صحبت خواهیم کرد.
پیادهسازی پروژه پیوند به عنوان
در این بخش قصد داریم از صفر یک پروژه جنگو بسازیم و با استفاده از کتابخانههای DRF
و SimpleJWT
سیستم احراز هویت رو پیادهسازی کنیم.
1. ایجاد پروژه جدید پیوند به عنوان
در ابتدا یک پروژه جدید ایجاد کرده و کتابخانههای مورد نیاز رو نصب میکنیم. برای این کار میتونین از دستورات زیر استفاده کنین:
mkdir drf-jwt-httponly-cookie
cd drf-jwt-httponly-cookie
uv init
uv add django djangorestframework djangorestframework-simplejwt
source .venv/bin/activate # Activate virtual environment
django-admin startproject backend
uv
برای مدیریت پکیجها استفاده میکنیم. uv
یک جایگزین برای ابزارهایی مثل pip
و poetry
هست که با زبان Rust
نوشته شده و قابلیتها و سرعت خیلی بیشتری نسبت به ابزارهای مشابه داره. پیشنهاد میکنم حتما امتحانش کنید. (داکیومنت uv)تا اینجا ساختار پروژه ما به این شکل هست:
├── backend
│ ├── backend
│ │ ├── asgi.py
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ └── manage.py
├── pyproject.toml
└── uv.lock
برای استفاده از کتابخانهها، فایل settings.py
را به شکل زیر تغییر میدیم:
# backend/settings.py
from datetime import timedelta
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party apps
"rest_framework",
"rest_framework_simplejwt",
]
# Simple JWT
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
# Auth
"AUTH_HEADER_TYPES": ("Bearer",),
}
2. پیادهسازی Login API پیوند به عنوان
برای پیادهسازی سیستم لاگین، ابتدا یک app جدید ایجاد میکنیم:
python manage.py startapp accounts
INSTALLED_APP
در settings.py
اضافه کنید.قبل از نوشتن API ما نیاز داریم تا تنظیمات جدیدی رو برای کوکیهای مربوط به Access Token
و Refresh Token
اضافه کنیم.
# backend/settings.py
from datetime import timedelta
# Simple JWT
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
# Auth
"AUTH_HEADER_TYPES": ("Bearer",),
# Auth Cookie
"AUTH_COOKIE_ACCESS": "access_token",
"AUTH_COOKIE_REFRESH": "refresh_token",
"AUTH_COOKIE_DOMAIN": None, # ".example.com" or None for standard domain cookie
"AUTH_COOKIE_SECURE": False, # Whether the auth cookies should be secure (https:// only).
"AUTH_COOKIE_HTTP_ONLY": True,
"AUTH_COOKIE_SAMESITE": "Lax", # The flag restricting cookie leaks on cross-site requests. 'Lax', 'Strict' or None to disable the flag.
"AUTH_COOKIE_REFRESH_PATH": "/accounts/auth/",
}
برای امنیت بیشتر کوکیها، پارامترهای زیر رو باید تنظیم کنیم:
- Secure: اگر مقدار این پارامتر True باشه، کوکی فقط از طریق
HTTPS
ارسال میشه. این گزینه رو در محیطproduction
حتما فعال کنید. - Domain: این پارامتر مشخص میکنه کوکی روی چه دامنهای معتبر باشه. اگر API و کلاینت روی دامنههای مختلفی هستن، میتونید به شکل
Domain=.example.com
تنظیم کنید. - Path: به صورت پیشفرض مقدار این پارامتر
/
هست، که باعث میشه کوکی در تمام درخواستها ارسال بشه. اما چون Refresh Token فقط برای مسیرهای خاصی لازمه، این مقدار رو برای کوکی مربوط به Refresh Token طوری تنظیم میکنیم که فقط در درخواستهای مورد نیاز ارسال بشه.
برای اضافه کردن توکنهای احراز هویت در کوکی، تابع زیر رو به پروژه اضافه میکنیم تا بعداً برای لاگین کاربر ازش استفاده کنیم:
# accounts/jwt.py
from django.conf import settings
from rest_framework.response import Response
def set_token_cookies(
response: Response,
access_token: str | None = None,
refresh_token: str | None = None,
) -> None:
if access_token:
response.set_cookie(
key=settings.SIMPLE_JWT["AUTH_COOKIE_ACCESS"],
value=access_token,
max_age=settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"],
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"],
domain=settings.SIMPLE_JWT["AUTH_COOKIE_DOMAIN"],
httponly=settings.SIMPLE_JWT["AUTH_COOKIE_HTTP_ONLY"],
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"],
)
if refresh_token:
response.set_cookie(
key=settings.SIMPLE_JWT["AUTH_COOKIE_REFRESH"],
value=refresh_token,
max_age=settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"],
path=settings.SIMPLE_JWT["AUTH_COOKIE_REFRESH_PATH"],
secure=settings.SIMPLE_JWT["AUTH_COOKIE_SECURE"],
domain=settings.SIMPLE_JWT["AUTH_COOKIE_DOMAIN"],
httponly=settings.SIMPLE_JWT["AUTH_COOKIE_HTTP_ONLY"],
samesite=settings.SIMPLE_JWT["AUTH_COOKIE_SAMESITE"],
)
حالا باید API لاگین کاربر رو ایجاد کنیم که نام کاربری و رمز عبور رو دریافت کنه و اگر اطلاعات درست بود، توکنهای احراز هویت رو در کوکی ذخیره و ارسال کنه.
# accounts/serializers.py
from rest_framework import serializers
from rest_framework_simplejwt.serializers import PasswordField
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = PasswordField()
# accounts/views.py
from django.contrib.auth import authenticate
from rest_framework import status
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from accounts.jwt import set_token_cookies
from accounts.serializers import LoginSerializer
class LoginAPIView(APIView):
serializer_class = LoginSerializer
authentication_classes = ()
permission_classes = ()
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
username = serializer.validated_data["username"]
password = serializer.validated_data["password"]
user = authenticate(request, username=username, password=password)
if not user:
raise AuthenticationFailed
response = Response({}, status=status.HTTP_200_OK)
# Set auth cookies
refresh = RefreshToken.for_user(user)
set_token_cookies(response, str(refresh.access_token), str(refresh))
return response
# accounts/urls.py
from django.urls import path
from accounts import views
app_name = "accounts"
urlpatterns = [
path("auth/login/", views.LoginAPIView.as_view(), name="login"),
]
برای اضافه کردن URL جدید به پروژه، حتما فایل urls.py
اصلی رو هم به این شکل تغییر بدین:
# backend/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("accounts.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
3. پیادهسازی Refresh Token API پیوند به عنوان
در این مرحله باید یک API ایجاد کنیم تا وقتی Access Token کاربر منقضی شد، بدون نیاز به لاگین مجدد، توکن جدید دریافت کنه. برای این کار از View پیشفرض کتابخونه SimpleJWT استفاده میکنیم، با این تفاوت که:
- توکنهای جدید در کوکی
HttpOnly
ذخیره میشن. Refresh Token
از کوکی خونده میشه و برای Serializer ارسال میشه.
# accounts/views.py
from django.conf import settings
from rest_framework import status
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from rest_framework_simplejwt.tokens import Token
from rest_framework_simplejwt.views import TokenRefreshView
from accounts.jwt import set_token_cookies
class RefreshTokenAPIView(TokenRefreshView):
def post(self, request: Request, *args, **kwargs) -> Response:
try:
serializer = self.get_serializer(data={"refresh": self.get_refresh_token_from_cookie()})
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0]) from e
response = Response({}, status=status.HTTP_200_OK)
# Set auth cookies
access_token = serializer.validated_data.get("access")
refresh_token = serializer.validated_data.get("refresh")
set_token_cookies(response, access_token, refresh_token)
return response
def get_refresh_token_from_cookie(self) -> Token:
refresh = self.request.COOKIES.get(settings.SIMPLE_JWT["AUTH_COOKIE_REFRESH"])
if not refresh:
raise PermissionDenied
return refresh
# accounts/urls.py
from django.urls import path
from accounts import views
app_name = "accounts"
urlpatterns = [
# Auth
path("auth/refresh_token/", views.RefreshTokenAPIView.as_view(), name="refresh-token"),
path("auth/login/", views.LoginAPIView.as_view(), name="login"),
]
4. پیادهسازی سیستم احراز هویت JWT با کوکی پیوند به عنوان
تا اینجا تونستیم کاربر رو لاگین کنیم و توکنهای لازم رو در کوکیهای کاربر ذخیره کنیم. حالا برای احراز هویت کاربر بر اساس کوکی باید یک Authentication Class
جدید بنویسیم تا Access Token
رو از کوکی بخونه و بر اساس اون احراز هویت کاربر انجام بشه.
# accounts/authentication.py
from django.conf import settings
from rest_framework_simplejwt.authentication import JWTAuthentication
class JWTCookieAuthentication(JWTAuthentication):
def authenticate(self, request):
header = self.get_header(request)
if header is None:
raw_token = request.COOKIES.get(settings.SIMPLE_JWT["AUTH_COOKIE_ACCESS"]) or None
else:
raw_token = self.get_raw_token(header)
if raw_token is None:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
در نهایت باید این کلاس رو به DRF
هم معرفی کنیم تا برای احراز هویت کاربر در APIها از این کلاس استفاده کنه. برای این کار تنظیمات REST_FRAMEWORK
رو در فایل settings.py
به شکل زیر تغییر میدیم:
# backend/settings.py
# DRF
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("accounts.authentication.JWTCookieAuthentication",),
}
برای تست سیستم احراز هویت، یک API ساده ایجاد میکنیم که اطلاعات کاربر رو فقط در صورتی نمایش بده که کاربر لاگین کرده باشه.
# accounts/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
"username",
"first_name",
"last_name",
)
# accounts/views.py
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from accounts.serializers import UserSerializer
class UserRetrieveAPIView(GenericAPIView):
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return self.request.user
def get(self, request):
serializer = self.get_serializer(instance=self.get_queryset())
return Response(serializer.data, status=status.HTTP_200_OK)
# accounts/urls.py
from django.urls import path
from accounts import views
app_name = "accounts"
urlpatterns = [
# Auth
path("auth/refresh_token/", views.RefreshTokenAPIView.as_view(), name="refresh-token"),
path("auth/login/", views.LoginAPIView.as_view(), name="login"),
# User
path("user/", views.UserRetrieveAPIView.as_view(), name="user"),
]
در بخش دوم چه مواردی بررسی خواهد شد؟ پیوند به عنوان
خب تا اینجا در بخش اول مقاله با مفاهیم اصلی JWT و چالشهایی که ممکنه در استفاده از اون پیش بیاد آشنا شدیم و سعی کردیم یک سیستم احراز هویت امن با استفاده از JWT و کوکیهای HttpOnly پیادهسازی کنیم. اما این پایان کار نیست! در بخش دوم مقاله قراره به موضوعات دیگهای مثل پیادهسازی Logout API
و راهکارهای جلوگیری از حملات CSRF
بپردازیم.
بخش دوم مقاله رو در اینجا بخونید.
همچنین میتونید کدهای کامل پروژه رو از گیتهاب دریافت کنید. اگر این پروژه براتون مفید بود، خوشحال میشم به پروژه استار بدید.
اگر نظری، سوالی یا پیشنهادی دارید، لطفا در لینکدین باهام به اشتراک بذارید! 🙌
آدرس لینکدین: linkedin.com/in/mobin-ghoveoud