توکنهای وب JSON (JWT) به استاندارد پیشفرض برای ایمنسازی APIها و مدیریت احراز هویت کاربران در برنامههای وب مدرن تبدیل شدهاند. اگرچه JWTها جذابیت یک معماری بدون حالت را ارائه میدهند—جایی که سرور نیازی به ذخیره دادههای جلسه ندارد—این راحتی چالشهای امنیتی قابل توجهی را به همراه دارد. به طور خاص، اگر یک JWT دزدیده شود، تا زمان انقضای آن معتبر باقی میماند. این پست استراتژیهای مستحکمی برای چرخش و لغو JWT را بررسی میکند و نشان میدهد که این مکانیسمها چگونه در محیطهای حالتدار و بدون حالت عمل میکنند.
مشکل اصلی: سرقت توکن و انقضا
در یک پیادهسازی ساده JWT، پس از صدور یک توکن، سرور تا رسیدن به ادعای exp (انقضا) به امضای آن اعتماد میکند. اگر مهاجم این توکن را دستچین کند، تا زمانی که این پنجره بسته شود، دسترسی غیرمجاز خواهد داشت. برای کاهش این خطر، باید چرخش توکن (صدور توکنهای جدید) و لغو (نامعتبر کردن زودهنگام توکنها) را پیادهسازی کنیم.
استراتژی ۱: رویکرد بدون حالت با توکنهای دسترسی کوتاهمدت
در یک معماری کاملاً بدون حالت، سرور نمیتواند برای بررسی اینکه آیا توکنی لغو شده است یا خیر، به یک پایگاه داده مرکزی مراجعه کند. بنابراین، اصلیترین دفاع، کاهش پنجره فرصت برای مهاجمان است. ما این کار را با استفاده از توکنهای دسترسی کوتاهمدت همراه با توکنهای تازهسازی (Refresh Token) بلندمدت انجام میدهیم.
نحوه عملکرد:
- کلاینت یک توکن دسترسی کوتاهمدت (مثلاً ۱۵ دقیقه) دریافت میکند.
- هنگامی که توکن دسترسی منقضی میشود، کلاینت از توکن تازهسازی برای دریافت یک توکن دسترسی جدید استفاده میکند.
- توکن تازهسازی به صورت امن ذخیره میشود (مثلاً در کوکی HttpOnly) و عمر طولانیتری دارد (مثلاً ۷ روز).
حتی در یک تنظیمات بدون حالت، میتوانیم لغو حداقلی را با بررسی یک «لیست سیاه توکن» یا زمانمهر «آخرین فعالیت» که در یک ذخیرهسازی کلید-مقدار سریع مانند Redis ذخیره شده است، پیادهسازی کنیم. اگرچه این موضوع وابستگی حالت کمی را معرفی میکند، اما در مقایسه با مدیریت جلسه سنتی، ناچیز است.
// کد شبه برای بررسی یک JWT با یک بررسی سریع Redis
async function verifyToken(token, redisClient) {
const decoded = jwt.verify(token, SECRET_KEY);
// بررسی کنید که آیا شناسه توکن در لیست سیاه کوتاهمدت وجود دارد یا بهروزرسانی شده است
const tokenRecord = await redisClient.get(`token:${decoded.jti}`);
if (tokenRecord === 'revoked') {
throw new Error('Token revoked');
}
// اختیاری: بررسی کنید که آیا «آخرین تغییر رمز عبور» کاربر جدیدتر از صدور توکن است یا خیر
const user = await getUser(decoded.sub);
if (decoded.iat < user.lastPasswordChange) {
throw new Error('Token issued before password change');
}
return decoded;
}
استراتژی ۲: رویکرد حالتدار با جلسات سمت سرور
در یک معماری حالتدار، سرور سوابقی از جلسات فعال را حفظ میکند. این کنترل دقیقتری بر اعتبار توکن فراهم میکند اما به زیرساخت بیشتری نیاز دارد. در اینجا، JWTها همچنان میتوانند به عنوان حامل ادعاها استفاده شوند، اما سرور آنها را با یک پایگاه داده از جلسات فعال اعتبارسنجی میکند.
مکانیسمهای لغو:
- خروج (Logout): وقتی کاربر خارج میشود، سرور سوابق جلسه را از پایگاه داده حذف میکند. هر درخواست آینده با آن JWT در اعتبارسنجی شکست میخورد زیرا سرور نمیتواند جلسه مرتبط با آن را پیدا کند.
- خروج اجباری: سرور میتواند تمام جلسات برای یک شناسه کاربری خاص را حذف کند و بلافاصله تمام توکنهای فعال را در تمام دستگاهها نامعتبر کند.
پیادهسازی: لیست سیاه توکن در Node.js
چه حالتدار و چه بدون حالت (با کمی حالت)، پیادهسازی یک لیست سیاه برای لغو فوری ضروری است. در زیر نمونهای از نحوه قرار دادن یک JWT در لیست سیاه با استفاده از Node.js و Redis آورده شده است. ما ادعای jti (شناسه JWT) را به عنوان کلید ذخیره میکنیم و زمان انقضا را برابر با اعتبار باقیمانده توکن تنظیم میکنیم.
const jwt = require('jsonwebtoken');
const redis = require('redis');
const redisClient = redis.createClient();
// قرار دادن توکن در لیست سیاه هنگام خروج
async function blacklistToken(req, res) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token); // بدون بررسی برای دریافت jti رمزگشایی کنید
if (!decoded || !decoded.jti) {
return res.status(400).send('Invalid token structure');
}
// محاسبه TTL بر اساس زمان انقضای توکن
const now = Math.floor(Date.now() / 1000);
const ttl = decoded.exp - now;
// ذخیره jti در Redis با زمان انقضا
await redisClient.setex(`blacklist:${decoded.jti}`, ttl, 'revoked');
res.status(200).send('Successfully logged out');
}
// میانافزار برای بررسی لیست سیاه در حین اعتبارسنجی درخواست
async function checkBlacklist(req, res, next) {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
if (decoded && decoded.jti) {
const isBlacklisted = await redisClient.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
return res.status(401).send('Token has been revoked');
}
}
next();
}
بهترین شیوهها برای چرخش امن
- از شناسههای منحصر به فرد استفاده کنید: همیشه یک ادعای
jtiمنحصر به فرد را در JWTهای خود گنجانید. این کار امکان شناسایی دقیق توکنها را برای اهداف لغو فراهم میکند. - توکنهای تازهسازی را بچرخانید: هر بار که یک توکن دسترسی تازهسازی میشود، یک توکن تازهسازی جدید صادر کنید و توکن قدیمی را نامعتبر کنید. این کار از حملات تکراری جلوگیری میکند اگر یک توکن تازهسازی پس از استفاده دزدیده شود.
- ذخیرهسازی امن: هرگز JWTها را در
localStorageذخیره نکنید اگر برنامه شما در برابر XSS آسیبپذیر است. در عوض از کوکیهایHttpOnlyوSecureبرای کاهش خطرات اسکریپتنویسی بین سایتی استفاده کنید. - برای ناهنجاریها نظارت کنید: ثبت وقایع و نظارت را پیادهسازی کنید تا الگوهای غیرمعمول استفاده از توکن، مانند ورود همزمان چندگانه از مکانهای جغرافیایی مختلف را تشخیص دهید.
نتیجهگیری
پیادهسازی چرخش و لغو امن JWT نیازمند تعادل بین امنیت و عملکرد است. معماریهای بدون حالت به شدت به عمر کوتاه توکنها و لایههای کشگذاری کارآمد (مانند Redis) برای مدیریت لغو بدون ذخیره جلسات کامل متکی هستند. معماریهای حالتدار لغو آسانتری را با هزینه افزایش حافظه سرور و بار پایگاه داده ارائه میدهند. برای بیشتر برنامههای مدرن، یک رویکرد ترکیبی—استفاده از توکنهای دسترسی کوتاهمدت، چرخش توکنهای تازهسازی و به کارگیری یک ذخیرهسازی کلید-مقدار سریع برای لیست سیاه توکنها—بهترین تعادل بین امنیت، مقیاسپذیری و تجربه کاربری را فراهم میکند.