מלכודת הטוקן (token trap) פגעה באתר חי כאשר חמישה טאבים שנפתחו על ידי משתמש יחיד ניסו כולם לרענן JWT פגוף באותה מילישנייה, מה שהציף את ה-backend בבקשות רענון כפולות וביטל באופן מיידי את הסשן. כל הטאבים הוציאו את המשתמש מהמערכת, מה שהוכיח שפתרון של טאב בודד לרענון טוקן כבר אינו מספיק.
למה מלכודת הטוקן חשובה
אפליקציות מודרניות בעמוד יחיד (SPAs) משתמשות ב-access tokens בעלי תוקף קצר וב-refresh token בעל תוקף ארוך. כאשר ה-access token פג תוקף, הלקוח שולח בקשת רענון, מקבל צמד טוקנים חדש, ומנסה שוב את הקריאה המקורית. מפתחים רבים מגנים על התהליך הזה באמצעות דגל בזיכרון (למשל, isRefreshing = true) או תור בקשות (request queue), אך בודקים זאת בטאב בודד. בעולם האמיתי, משתמשים משאירים מספר טאבים פתוחים: דף הגדרות, לוח בקרה (dashboard) של אנליטיקה ומספר תצוגות נתונים. כאשר ה-access token פג תוקף, כל טאב מזהה את שגיאת ה-401 באופן עצמאי, כל אחד מפעיל בקשת רענון, וה-backend — במיוחד כאשר הוא אוכף refresh-token rotation — מתייחס לבקשה השנייה כאל ניסיון שידור חוזר (replay) ומבטל את הסשן כולו.
בידוד JavaScript יוצר את הבעיה
כל טאב בדפדפן מריץ context של JavaScript משלו. משתנים, טיימרים ודגלים בזיכרון (in-memory flags) אינם גלויים לטאבים אחרים, גם כשהם חולקים את אותו origin. דגל שאומר "רענון כבר נמצא בתהליך" חי רק בתוך הטאב שהגדיר אותו. לטאבים האחרים אין דרך לדעת שהטוקן מתרענן במקום אחר, ולכן כולם משיקים קריאת רשת משלהם. הבעיה אינה באג ב-interceptor; זוהי מגבלה יסודית של בידוד מצב (state isolation) בצד הלקוח.
Web Locks API מציל את המצב
כאשר טאב מחזיק ב-lock, כל טאב אחר שמבקש את אותו lock חייב לחכות עד שהוא ישוחרר.
איך זה עובד עבור רענון טוקן
- זיהוי 401 – כל טאב שמקבל תגובת unauthorized קורא ל-
navigator.locks.request('auth_token_refresh_lock', async lock => { … }). - רכישת ה-lock – אם אף טאב אחר לא מחזיק ב-lock, הטאב הנוכחי ממשיך; אחרת, הוא עוצר עד שה-lock יהיה פנוי.
- רענון פעם אחת – מחזיק ה-lock שולח את בקשת הרענון, שומר את ה-access token החדש וחותמת זמן (timestamp) ב-
localStorage, ואז משחרר את ה-lock באופן אוטומטי כשה-callback מסתיים. - דילוג על עבודה כפולה – כאשר טאב שמחכה מקבל סוף סוף את ה-lock, הוא קורא את ה-timestamp מ-
localStorage. אם הטוקן רוענן בתוך חלון זמן ניתן להגדרה (למשל, בשניות האחרונות), הטאב מדלג על קריאת הרשת ומעדכן את הטוקן בזיכרון שלו מתוך ה-localStorage. - טיפול בקריסות – אם טאב קורס או נסגר בזמן שהוא מחזיק ב-lock, הדפדפן משחרר את ה-lock, מה שמאפשר לטאב אחר לנסות שוב את הרענון.
יתרונות במבט חטוף
- אפס קריאות רשת מיותרות – רק הטאב הראשון מדבר עם ה-backend.
- ללא הרס הסשן – ה-refresh-token rotation רואה שימוש יחיד, מה ששומר על הסשן פעיל.
- התאוששות חלקה – שחרור ה-lock המנוהל על ידי הדפדפן מונע deadlocks אם טאב נעלם.
צ'קליסט יישום
- עטפו את לוגיקת הרענון ב-interceptor של Axios (או fetch) באמצעות בקשת lock.
- שמרו את הטוקן המעודכן וחותמת זמן במילישניות ב-
localStorage(או ב-sessionStorageאם אתם מעדיפים נתונים לכל סשן). - כאשר ה-lock ניתן, השוו את ה-timestamp השמור ל-
Date.now(). אם ההפרש נמוך מהסף שהגדרתם, קראו את הטוקן מהאחסון במקום לקרוא ל-backend. - ודאו שה-interceptor מעדכן את כותרות הבקשה (request headers) עם הטוקן שנשלף מהאחסון לפני ניסיון חוזר של קריאת ה-API המקורית.
- בדקו את התהליך עם מספר טאבים, תוך סימולציה של שיהוי רשת (network latency) כדי לוודא שרק בקשת רענון אחת מגיעה לשרת.
מה יכול להשתבש
מה כדאי לשים לב אליו בהמשך
שורה תחתונה
התייחסו לכל טאב בדפדפן כאל צומת (node) במערכת מבוזרת קטנה. על ידי שימוש ב-Web Locks API כדי לסדר (serialize) את רענוני ה-JWT, אתם מבטלים קריאות כפולות, מגנים על ה-refresh-token rotation ושומרים על המשתמשים מחוברים בכל הטאבים הפתוחים שלהם.
