تله توکن (token trap) زمانی به یک سایت عملیاتی ضربه زد که پنج تب باز شده توسط یک کاربر، همگی سعی کردند در یک میلیثانیه یک JWT منقضی شده را بازسازی کنند؛ این کار باعث هجوم درخواستهای بازسازی (refresh requests) تکراری به بکاِند و باطل شدن فوری نشست (session) شد. تمام تبها کاربر را از حساب خارج کردند، که ثابت کرد راهکار «تکتبی» برای بازسازی توکن دیگر کافی نیست.
چرا تله توکن اهمیت دارد
اپلیکیشنهای مدرن تکصفحهای (SPA) از توکنهای دسترسی (access tokens) با عمر کوتاه و توکنهای بازسازی (refresh tokens) با عمر طولانی استفاده میکنند. وقتی توکن دسترسی منقضی میشود، کلاینت یک درخواست بازسازی ارسال میکند، یک جفت توکن جدید دریافت میکند و فراخوانی اصلی را دوباره امتحان میکند. اکثر توسعهدهندگان این جریان را با یک پرچم در حافظه (مثلاً isRefreshing = true) یا یک صف درخواست (request queue) محافظت میکنند و آن را در یک تب واحد تست میکنند. در دنیای واقعی، کاربران چندین تب را باز نگه میدارند: یک صفحه تنظیمات، یک داشبورد تحلیلی و چند نمای داده. وقتی توکن دسترسی منقضی میشود، هر تب بهطور مستقل خطای 401 را تشخیص میدهد، هر کدام یک درخواست بازسازی ارسال میکنند و بکاِند — بهویژه زمانی که قابلیت چرخش توکن بازسازی (refresh-token rotation) را اعمال میکند — درخواست دوم را به عنوان یک حمله بازپخش (replay) تلقی کرده و کل نشست را باطل میکند.
ایزولاسیون JavaScript مشکل را ایجاد میکند
هر تب مرورگر یک کانتکست JavaScript مجزا را اجرا میکند. متغیرها، تایمرها و پرچمهای در حافظه (in-memory flags) برای تبهای دیگر نامرئی هستند، حتی زمانی که منشأ (origin) یکسانی داشته باشند. پرچمی که میگوید «یک عملیات بازسازی در حال انجام است»، فقط درون همان تبی که آن را تنظیم کرده زندگی میکند. تبهای دیگر راهی برای دانستن اینکه توکن در جای دیگری در حال بازسازی است ندارند، بنابراین همگی فراخوانی شبکه خود را اجرا میکنند. مشکل، باگ در اینترسپتور (interceptor) نیست؛ بلکه یک محدودیت بنیادی در ایزولاسیون وضعیت (state isolation) سمت کلاینت است.
نجات با Web Locks API
وقتی یک تب قفل را در اختیار دارد، هر تب دیگری که درخواست همان قفل را بدهد، باید تا زمان آزاد شدن آن منتظر بماند.
نحوه عملکرد برای بازسازی توکن
- تشخیص خطای 401 – هر تبی که پاسخ غیرمجاز دریافت میکند،
navigator.locks.request('auth_token_refresh_lock', async lock => { … })را فراخوانی میکند. - دریافت قفل – اگر هیچ تب دیگری قفل را در اختیار نداشته باشد، تب فعلی ادامه میدهد؛ در غیر این صورت تا زمانی که قفل آزاد شود، متوقف میماند.
- یکبار بازسازی کردن – دارنده قفل، درخواست بازسازی را ارسال میکند، توکن دسترسی جدید و یک برچسب زمانی (timestamp) را در
localStorageذخیره میکند و سپس با اتمام کالبک، بهطور خودکار قفل را آزاد میکند. - نادیده گرفتن کارهای تکراری – وقتی یک تب منتظر، بالاخره قفل را دریافت کرد، برچسب زمانی را از
localStorageمیخواند. اگر توکن در یک بازه زمانی قابل تنظیم (مثلاً چند ثانیه اخیر) بازسازی شده باشد، تب از فراخوانی شبکه صرفنظر کرده و توکن در حافظه خود را ازlocalStorageبهروزرسانی میکند. - مدیریت کرشها – اگر تبی در حین نگه داشتن قفل کرش کند یا بسته شود، مرورگر قفل را آزاد میکند و به تب دیگری اجازه میدهد بازسازی را دوباره امتحان کند.
مزایا در یک نگاه
- صفر کردن فراخوانیهای شبکه مازاد – فقط اولین تب با بکاِند صحبت میکند.
- عدم تخریب نشست – چرخش توکن بازسازی (refresh-token rotation) تنها یک بار استفاده را مشاهده میکند و نشست را زنده نگه میدارد.
- بازیابی نرم – آزادسازی قفل توسط مرورگر، از بنبستها (deadlocks) در صورت ناپدید شدن یک تب جلوگیری میکند.
چکلیست پیادهسازی
- منطق بازسازی را در اینترسپتور Axios (یا fetch) خود با یک درخواست قفل (lock request) محصور کنید.
- توکن بازسازی شده و یک برچسب زمانی میلیثانیهای را در
localStorage(یاsessionStorageاگر دادههای مربوط به هر نشست را ترجیح میدهید) ذخیره کنید. - وقتی قفل اعطا شد، برچسب زمانی ذخیره شده را با
Date.now()مقایسه کنید. اگر اختلاف کمتر از آستانه تعیینشده شما بود، به جای فراخوانی بکاِند، توکن را از حافظه بخوانید. - مطمئن شوید که اینترسپتور قبل از تلاش مجدد برای فراخوانی اصلی API، هدرهای درخواست را با توکن بازیابی شده از حافظه بهروزرسانی میکند.
- جریان کار را با چندین تب تست کنید و تأخیر شبکه را شبیهسازی کنید تا تأیید شود که تنها یک درخواست بازسازی به سرور میرسد.
چه مشکلاتی ممکن است پیش بیاید
مواردی که باید در آینده زیر نظر داشت
نتیجهگیری
با هر تب مرورگر مانند یک گره (node) در یک سیستم توزیعشده کوچک رفتار کنید. با استفاده از Web Locks API برای سریالسازی (serialize) بازسازیهای JWT، شما فراخوانیهای تکراری را حذف میکنید، از چرخش توکن بازسازی محافظت میکنید و کاربران را در تمام تبهای باز خود متصل نگه میدارید.
