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.