JWT یا کوکی سشن: تفاوت‌ها و زمان استفاده از هرکدام

کوکی سشن کلیدی است به وضعیتی که روی سرور نگه‌داری می‌شود؛ JWT خود وضعیت امضاشده را به سرور می‌برد. همه بده‌بستان‌ها از همین یک تفاوت می‌آیند.

همین حالا امتحان کنید: رمزگشای JWT

این مقایسه معمولاً به شکل «JWT در برابر کوکی» مطرح می‌شود، اما این دو از یک جنس نیستند. کوکی روشی برای فرستادن داده همراه هر درخواست است. JWT یک قالب داده است. انتخاب واقعی بین یک سشن سمت سرور است، که در آن مرورگر یک شناسه تصادفی نگه می‌دارد و سرور جست‌وجو می‌کند این شناسه مال چه کسی است، و یک توکن خودکفا، که در آن کلاینت ادعاهای (claims) امضاشده‌ای نگه می‌دارد که سرور می‌تواند بدون جست‌وجو بررسی‌شان کند. وقتی این نکته روشن شود، سؤال‌های مربوط به باطل کردن توکن، محل ذخیره و CSRF هم سر جای خودشان قرار می‌گیرند.

سشن سمت سرور چطور کار می‌کند

بعد از ورود موفق، سرور یک رکورد سشن (شناسه کاربر، نقش‌ها، زمان انقضا) در حافظه، پایگاه داده یا مخزنی مثل Redis می‌سازد و یک شناسه غیرقابل‌حدس را در یک کوکی برای مرورگر می‌فرستد. مرورگر این کوکی را به همه درخواست‌های بعدی به آن سایت ضمیمه می‌کند و سرور برای هر درخواست رکورد مربوط را بارگذاری می‌کند. خود شناسه به‌تنها هیچ معنایی ندارد؛ ۳۲ بایت تصادفی به ۴۳ نویسه تبدیل می‌شود.

Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400

پیشوند __Host- باعث می‌شود مرورگرها کوکی را رد کنند، مگر اینکه Secure باشد، Path=/ داشته باشد و ویژگی Domain را تنظیم نکرده باشد؛ به این ترتیب یک زیردامنه نمی‌تواند آن را بازنویسی کند.

JWT چطور کار می‌کند

JSON Web Token (RFC 7519) از سه بخش کدگذاری‌شده با Base64URL تشکیل شده که با نقطه به هم وصل شده‌اند: یک هدر که الگوریتم را مشخص می‌کند، یک payload شامل ادعاهایی مثل sub و exp، و یک امضا روی دو بخش اول. سرور امضا و زمان انقضا را با یک کلید بررسی می‌کند و بدون پرس‌وجو از پایگاه داده به ادعاها اعتماد می‌کند. JWT معمولاً در هدر Authorization: Bearer فرستاده می‌شود.

payload فقط کدگذاری شده، نه رمزنگاری. هر کسی که توکن را داشته باشد می‌تواند آن را بخواند؛ کافی است یک توکن را در رمزگشای JWT بچسبانید تا خودتان ببینید. هیچ‌وقت راز یا داده شخصی‌ای را که نمی‌خواهید کاربر ببیند در JWT نگذارید. توکن‌های رمزنگاری‌شده هم وجود دارند (JWE، RFC 7516)، اما بیشتر JWTهایی که استفاده می‌شوند فقط امضا شده‌اند. برای اینکه بدانید چرا این بخش‌ها به‌جای + و / از - و _ استفاده می‌کنند، راهنمای تفاوت Base64 و Base64URL را ببینید.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4

// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }

همان‌طور که در کد بالا می‌بینید، برای خواندن payload در مرورگر اصلاً به کلید نیاز نیست.

بده‌بستان‌ها کنار هم

سشن سمت سرورJWT
آنچه کلاینت نگه می‌داردیک شناسه تصادفی و بی‌معناادعاهای امضاشده و خوانا
آنچه سرور لازم داردیک مخزن سشن که همه سرورها به آن دسترسی دارندفقط کلید بررسی امضا
خارج کردن کاربر از همه دستگاه‌هاحذف سشن: فوریپیش از exp بدون وضعیت اضافه ممکن نیست
تغییر نقش یا دسترسیاز درخواست بعدی اعمال می‌شودوقتی توکن جدید صادر شود اعمال می‌شود
روش معمول ارسالکوکی، به‌صورت خودکارهدر Authorization یا یک کوکی
حجم در هر درخواستشناسه ۴۳ نویسه‌ای در نمونه ما۱۵۲ تا ۵۴۰ نویسه در نمونه‌های ما

باطل کردن، انقضا و refresh token

بی‌حالت بودن (stateless) مهم‌ترین نقطه قوت JWT است و در عین حال مهم‌ترین نقطه ضعفش. یک توکن معتبر تا زمان انقضا معتبر می‌ماند، حتی اگر کاربر از حساب خارج شود، رمز عبورش را عوض کند یا مسدود شود. راه‌حل معمول این است که عمر access tokenها کوتاه باشد، اغلب چند دقیقه به‌جای چند ساعت، و کنارشان یک refresh token با عمر طولانی‌تر باشد که کلاینت آن را با access tokenهای جدید عوض می‌کند. refresh token با وضعیتی که روی سرور نگه‌داری می‌شود بررسی می‌شود، پس باطل کردن آن جلوی صدور access tokenهای جدید را می‌گیرد.

سند OAuth 2.0 Security Best Current Practice (RFC 9700) روشی به نام چرخش refresh token (rotation) را شرح می‌دهد: هر بار refresh یک refresh token جدید برمی‌گرداند و توکن قبلی را باطل می‌کند، تا اگر توکن دزدی‌شده‌ای دوباره استفاده شود، قابل تشخیص باشد. اگر باطل کردن فوری access tokenها را هم لازم دارید، در نهایت مجبور می‌شوید یک فهرست سیاه از شناسه‌های توکن (jti) یا یک شماره نسخه توکن برای هر کاربر نگه دارید، که باز هم یعنی وضعیت سمت سرور. در این نقطه بررسی کنید که آیا یک سشن ساده راه ساده‌تری نیست.

توکن را کجا ذخیره کنیم: کوکی، localStorage و XSS

هر چیزی که در localStorage یا sessionStorage باشد، توسط همه اسکریپت‌هایی که در صفحه شما اجرا می‌شوند قابل خواندن است. یک باگ cross-site scripting (XSS) یا یک اسکریپت شخص ثالث آلوده کافی است تا توکن کپی شود و تا زمان انقضا از هر جایی به کار برود.

کوکی‌ای که با HttpOnly علامت خورده باشد اصلاً توسط JavaScript خوانده نمی‌شود، پس XSS نمی‌تواند آن را بدزدد. با این حال XSS همچنان می‌تواند از صفحه باز قربانی درخواست بفرستد و مرورگر کوکی را به آن ضمیمه می‌کند؛ پس HttpOnly آسیب را محدود می‌کند، نه اینکه جلویش را بگیرد. جلوگیری از XSS در هر حالت ضروری است؛ کوکی‌های HttpOnly فقط باعث می‌شوند یک حمله موفق برای مهاجم کم‌فایده‌تر باشد.

CSRF و SameSite

راحتی کوکی همان چیزی است که آن را پرخطر می‌کند: مرورگر آن را به‌صورت خودکار ضمیمه می‌کند، حتی به درخواست‌هایی که یک وب‌سایت دیگر راه انداخته است. این همان جعل درخواست میان‌سایتی یا cross-site request forgery (CSRF) است. توکن‌هایی که در هدر Authorization فرستاده می‌شوند در معرض CSRF کلاسیک نیستند، چون مرورگر خودش bearer token را ضمیمه نمی‌کند و کد خود شما باید آن را اضافه کند.

  • SameSite=Strict: کوکی هرگز همراه درخواست‌هایی که از سایت دیگری شروع شده‌اند فرستاده نمی‌شود، حتی وقتی کاربر روی لینکی به سایت شما کلیک می‌کند.
  • SameSite=Lax: وقتی کاربر با یک لینک معمولی به سایت شما می‌آید (یک GET در سطح بالا) فرستاده می‌شود، اما همراه ارسال فرم میان‌سایتی، fetchها یا منابع جاسازی‌شده نه. Chrome برای کوکی‌هایی که این ویژگی را تنظیم نکرده‌اند Lax را اعمال می‌کند؛ به‌جای تکیه بر این رفتار، آن را صریحاً تنظیم کنید.
  • SameSite=None: همه‌جا فرستاده می‌شود و باید همراه Secure باشد (در غیر این صورت Chrome آن را رد می‌کند). فقط برای استفاده واقعاً میان‌سایتی، مثل ویجت‌های جاسازی‌شده، لازم است.

«هم‌سایت» (same site) یعنی دامنه قابل ثبت یکسان، پس app.example.com و forum.example.com یک سایت به حساب می‌آیند. SameSite از شما در برابر یک زیردامنه آلوده محافظت نمی‌کند. برای درخواست‌هایی که وضعیت را تغییر می‌دهند، یک توکن CSRF هم نگه دارید یا هدر Origin را هم بررسی کنید.

حجم توکن

شناسه سشن، مهم نیست چقدر درباره کاربر اطلاعات داشته باشید، همیشه کوچک می‌ماند. اما JWT با هر ادعا و با الگوریتم امضا بزرگ‌تر می‌شود. توکن‌هایی که برای این راهنما ساختیم، برای HS256 با سه ادعا ۱۵۲ نویسه بودند، بعد از افزودن نام، آدرس ایمیل و نقش ۲۴۱ نویسه، و برای همان ادعاها با امضای RS256 و یک کلید ۲۰۴۸ بیتی ۵۴۰ نویسه. RFC 6265 مرورگرها را فقط ملزم می‌کند از کوکی‌هایی با دست‌کم ۴٬۰۹۶ بایت، شامل نام و ویژگی‌ها، پشتیبانی کنند؛ پس JWTی که از مجوزها پر شده باشد می‌تواند از ظرفیت یک کوکی بزرگ‌تر شود. همین حجم همراه تک‌تک درخواست‌ها هم فرستاده می‌شود.

اشتباه‌هایی که امنیت JWT را از بین می‌برند

  • اعتماد به هدر alg. الگوریتم‌هایی را که سرورتان می‌پذیرد ثابت کنید. پذیرفتن none، یا اجازه دادن به توکن برای تغییر از RS256 به HS256 به طوری که کلید عمومی به‌عنوان راز HMAC به کار برود، به مهاجم اجازه جعل توکن می‌دهد.
  • رمزگشایی به‌جای اعتبارسنجی. خواندن payload هیچ چیزی را ثابت نمی‌کند؛ اول امضا را با یک کتابخانه‌ای که به‌روز نگه‌داری می‌شود بررسی کنید.
  • نادیده گرفتن بررسی ادعاها. exp، nbf، iss و aud را اعتبارسنجی کنید تا توکنی که برای یک سرویس صادر شده، توسط سرویس دیگری پذیرفته نشود.

RFC 8725 با عنوان JWT Best Current Practices این موارد و موارد بیشتری را پوشش می‌دهد.

هرکدام کجا مناسب است

موقعیتمعمولاً گزینه بهتر
وب‌اپلیکیشنی که صفحه‌ها را روی سرور می‌سازد و روی یک دامنه استکوکی سشن
اپلیکیشن تک‌صفحه‌ای (SPA) که از همان سایتِ API خودش سرو می‌شودکوکی سشن، یا یک backend-for-frontend که توکن‌ها را روی سرور نگه می‌دارد
اپلیکیشن موبایل بومیaccess token کوتاه‌عمر به‌همراه refresh token در فضای ذخیره‌سازی امن پلتفرم
میکروسرویس‌هایی که درخواست‌ها را بدون فراخوانی سرویس احراز هویت بررسی می‌کنندaccess tokenهای JWT امضاشده با کلید نامتقارن مثل RS256 یا ES256
دسترسی شخص ثالث به API شماaccess tokenهای OAuth 2.0، که اغلب JWT هستند

راه میانه: JWT داخل یک کوکی HttpOnly

بسیاری از اپلیکیشن‌ها خود JWT را در یک کوکی HttpOnly; Secure; SameSite قرار می‌دهند. اسکریپت‌ها نمی‌توانند آن را بخوانند و سرورها همچنان می‌توانند بدون جست‌وجوی سشن آن را بررسی کنند. اما بده‌بستان‌ها هم همراهش می‌آیند: کوکی خودکار فرستاده می‌شود، پس دوباره به دفاع در برابر CSRF نیاز دارید؛ محدودیت ۴ کیلوبایتی کوکی اعمال می‌شود؛ و باطل کردن توکن دقیقاً به همان سختی هر JWT دیگری است. این طراحی برای یک اپلیکیشن مرورگری که پشتش APIهای بی‌حالت قرار دارند درست است، به شرطی که از نظر CSRF با آن مثل یک کوکی رفتار کنید و از نظر باطل کردن مثل یک JWT.

سوالات متداول

آیا JWT از کوکی سشن امن‌تر است؟

نه، هیچ‌کدام ذاتاً امن‌تر نیست. سشن‌ها راحت‌تر باطل می‌شوند و هیچ اطلاعاتی به کلاینت نشان نمی‌دهند. JWTها نیاز به جست‌وجوی سشن را از بین می‌برند اما باطل کردنشان پیش از انقضا دشوار است. اینکه اعتبارنامه کجا ذخیره می‌شود و CSRF و XSS چطور مدیریت می‌شوند، از خود قالب مهم‌تر است.

آیا JWT را در localStorage ذخیره کنم؟

ساده است، اما توکن را در معرض همه اسکریپت‌های صفحه قرار می‌دهد و یک باگ XSS کافی است تا لو برود. یک کوکی HttpOnly و Secure با ویژگی SameSite صریح، توکن را از دسترس JavaScript دور نگه می‌دارد، به این قیمت که به محافظت در برابر CSRF نیاز پیدا می‌کنید.

آیا می‌شود یک JWT را باطل کرد؟

خودش به‌تنها نه. یک توکن امضاشده تا زمان exp معتبر می‌ماند. برای قطع زودتر دسترسی، عمر access tokenها را کوتاه نگه دارید، refresh token را باطل کنید یا شناسه توکن‌ها را با یک فهرست سیاه مقایسه کنید، که وضعیت سمت سرور اضافه می‌کند.

آیا داده‌های داخل JWT رمزنگاری شده‌اند؟

معمولاً نه. هدر و payload متن JSON کدگذاری‌شده با Base64URL هستند که هر کسی می‌تواند رمزگشایی‌شان کند؛ فقط امضا جلوی تغییر آن‌ها را می‌گیرد. اگر محتوا باید محرمانه بماند از JWE استفاده کنید، یا داده‌های حساس را در توکن نگذارید.
همین حالا امتحان کنید: رمزگشای JWT