Web Locks API สามารถหยุดยั้งไม่ให้แท็บที่เปิดอยู่ถึงห้าแท็บส่งคำขอ refresh-token ไปยัง auth server พร้อมๆ กัน ซึ่งช่วยป้องกันไม่ให้ผู้ใช้ถูกล็อกเอาต์ออกอย่างกะทันหัน การประสานงานการรีเฟรชระหว่างแท็บจะช่วยให้การส่งคำขอเพียงครั้งเดียวสามารถแทนที่การส่งคำขอจำนวนมากที่มักจะไปกระตุ้นให้เกิดการยกเลิกเซสชัน (session-kill) เมื่อมีการใช้งาน Refresh Token Rotation

ภาระงานที่ซ่อนอยู่ในการใช้งานแบบหลายแท็บ

แอปพลิเคชันแบบ single-page ทั่วไปมักจะเพิ่ม Axios interceptor เพื่อคอยตรวจจับการตอบกลับแบบ 401 จากนั้นจะเปลี่ยนค่า flag isRefreshing เป็น Boolean และจัดคิวคำขอที่กำลังจะส่งออกไปจนกว่าจะได้รับ JWT ใหม่ เมื่อทดสอบในแท็บเดียว ขั้นตอนนี้จะทำงานได้อย่างไม่มีที่ติ

แต่หากเปิดแอปเดียวกันในห้าแท็บ และปล่อยให้ access token หมดอายุ ทั้งห้าแท็บจะตรวจพบ 401 ในเสี้ยววินาทีเดียวกัน แต่ละแท็บจะคิดว่าตนเองต้องทำการรีเฟรช ดังนั้นจึงมีการส่งคำขอ refresh-token ที่เหมือนกันห้าครั้งไปยัง auth server เมื่อใช้ Refresh Token Rotation ซึ่งเป็นมาตรการความปลอดภัยที่จะยกเลิกความถูกต้องของ refresh token เดิมทันทีที่มีการออกโทเคนใหม่ เซิร์ฟเวอร์จะมองว่าคำขอที่สองเป็นการโจมตีแบบ replay attack จึงทำเครื่องหมายว่าเซสชันนี้ถูกบุกรุกและยกเลิกเซสชันนั้นทันที ส่งผลให้ผู้ใช้ถูกล็อกเอาต์ออกจากทุกแท็บในพริบตา

สาเหตุหลักมาจากโมเดลการแยกส่วน (isolation model) ของ JavaScript ตัวแปรอย่าง isRefreshing จะมีอยู่เฉพาะในแท็บที่ตั้งค่าไว้เท่านั้น แท็บอื่นๆ ไม่มีทางรู้เลยว่ากำลังมีการรีเฟรชเกิดขึ้นอยู่ ผลลัพธ์ที่ได้คือปัญหา concurrency แบบคลาสสิก แต่ "process" ในที่นี้คือแท็บของเบราว์เซอร์แทนที่จะเป็น threads

ทำไม cross-tab lock ถึงเป็นเครื่องมือที่เหมาะสม

สิ่งที่เราต้องการคือวิธีที่แท็บต่างๆ จะสามารถสื่อสารกันเกี่ยวกับทรัพยากรที่ใช้ร่วมกันได้ ซึ่งในกรณีนี้คือ JWT ตัวใหม่ Web Locks API ที่เปิดให้ใช้งานผ่าน navigator.locks สามารถทำสิ่งนั้นได้โดยตรง มันช่วยให้สคริปต์สามารถขอ lock ที่มีการตั้งชื่อไว้ ซึ่งเบราว์เซอร์จะบังคับใช้ข้ามทุก context ที่อยู่ภายใต้ origin เดียวกัน หากมีการถือ lock อยู่แล้ว ผู้เรียกรายอื่นจะถูกจัดเข้าคิวจนกว่าผู้ที่ถือ lock จะปล่อยมัน หรือเบราว์เซอร์จะยกเลิกการทำงาน (เช่น เมื่อแท็บค้างหรือล่ม) โดยไม่ต้องใช้เซิร์ฟเวอร์ภายนอก ไม่ต้องทำ polling เป็นเพียงการประสานงานผ่านเบราว์เซอร์โดยตรง

การนำขั้นตอนการรีเฟรชแบบใช้ lock ไปใช้งาน

  1. ตรวจจับ 401 – Axios interceptor จะดักจับการตอบกลับที่ไม่ได้รับอนุญาตเหมือนเดิม
  2. ขอ exclusive lock – แท็บจะเรียก navigator.locks.request('auth_token_refresh_lock', async lock => { … }) โดยจะมีเพียงแท็บเดียวเท่านั้นที่สามารถเข้าสู่ callback ได้ในแต่ละครั้ง
  3. ตรวจสอบซ้ำก่อนส่งคำขอผ่านเครือข่าย – ภายใน lock ให้ลองอ่าน timestamp (หรือตัว token เอง) จาก localStorage หาก timestamp นั้นมีอายุไม่ถึงไม่กี่วินาที แสดงว่ามีแท็บอื่นรีเฟรชโทเคนไปแล้ว แท็บปัจจุบันจะข้ามการเรียกเครือข่ายและอ่าน JWT ใหม่จาก storage แทน
  4. รีเฟรชหากจำเป็น – หาก timestamp ที่เก็บไว้ล้าสมัย ให้ส่งคำขอรีเฟรช เก็บโทเคนใหม่และเวลาปัจจุบันลงใน localStorage จากนั้นจึงปล่อย lock โดยการ return ออกจาก callback
  5. ดำเนินการคำขอที่ค้างอยู่ในคิวต่อ – แท็บอื่นๆ ที่รออยู่จะได้รับ lock ต่อกันไปตามลำดับ เมื่อเห็น timestamp ใหม่ที่เพิ่งอัปเดต พวกเขาก็จะดำเนินการเสร็จสิ้นโดยไม่ต้องส่งคำขอใหม่
async function refreshIfNeeded() {
  await navigator.locks.request('auth_token_refresh_lock', async lock => {
    const lastRefresh = Number(localStorage.getItem('token_refreshed_at') || 0);
    const now = Date.now();
    if (now - lastRefresh < 5_000) return; // another tab already refreshed

    const newToken = await callRefreshEndpoint(); // actual network call
    localStorage.setItem('jwt', newToken);
    localStorage.setItem('token_refreshed_at', now.toString());
  });
}

รูปแบบนี้รับประกันว่า ไม่ว่าจะมีแท็บเปิดอยู่กี่แท็บก็ตาม จะมีเพียงคำขอรีเฟรชเดียวเท่านั้นที่ส่งไปถึงเซิร์ฟเวอร์

ประโยชน์ที่คุณสามารถวัดผลได้

  • ประสิทธิภาพของเครือข่าย (Network efficiency) – การส่งคำขอเพียงครั้งเดียวแทนที่ห้าครั้ง ช่วยลดแบนด์วิดท์และภาระของเซิร์ฟเวอร์ได้อย่างมหาศาล
  • ความปลอดภัยของเซสชัน (Session safety) – เมื่อใช้ Refresh Token Rotation เซิร์ฟเวอร์จะเห็นการใช้ refresh token เก่าเพียงครั้งเดียวเท่านั้น จึงไม่มองว่าเซสชันถูกบุกรุก
  • ความยืดหยุ่น (Resilience) – หากแท็บที่ถือ lock เกิดค้างหรือล่ม เบราว์เซอร์จะปล่อย lock โดยอัตโนมัติ ช่วยป้องกันการเกิด deadlock ที่จะทำให้แท็บทั้งหมดหยุดทำงาน
  • ความสามารถในการขยายตัว (Scalability) – ผู้ใช้สามารถเปิดแท็บจำนวนมากได้โดยไม่ต้องเสี่ยงกับการถูกล็อกเอาต์แบบโดมิโน เพราะการประสานงานเกิดขึ้นภายในเบราว์เซอร์

ข้อควรระวัง: การรองรับของเบราว์เซอร์และวิธีสำรอง

Web Locks API เป็นฟีเจอร์ที่ค่อนข้างใหม่ เบราว์เซอร์สมัยใหม่ที่ใช้ Chromium และ Firefox เวอร์ชันล่าสุดรองรับฟีเจอร์นี้แล้ว แต่เบราว์เซอร์รุ่นเก่าและ Safari ยังไม่รองรับ ในสภาพแวดล้อมที่ API นี้ใช้งานไม่ได้ นักพัฒนาต้องใช้วิธีสำรองที่น่าเชื่อถือน้อยกว่า เช่น การส่ง custom event ผ่าน localStorage หรือการใช้ Shared Worker เพื่อจำลองการส่งสัญญาณข้ามแท็บ วิธีการเหล่านี้ขาดการป้องกัน deadlock อัตโนมัติแบบที่ navigator.locks มี ดังนั้นจึงควรใช้งานด้วยความระมัดระวัง

สิ่งที่ควรติดตามต่อไป

  • ความคืบหน้าด้านการกำหนดมาตรฐาน – คอยติดตามอัตราการนำ API ไปใช้งาน; การรองรับที่กว้างขวางขึ้นจะทำให้แนวทางแบบ lock-based กลายเป็นค่าเริ่มต้นสำหรับการประสานงานระหว่างแท็บ (cross-tab coordination)
  • Library wrappers – มีเครื่องมือโอเพนซอร์สบางส่วนที่เริ่มทำ abstraction ให้กับรูปแบบการร้องขอ lock (lock request pattern) แล้ว ซึ่งช่วยให้เชื่อมต่อกับ Axios interceptors ที่มีอยู่เดิมได้ง่ายขึ้น
  • การตรวจสอบความปลอดภัย – แม้ว่าการล็อกจะช่วยแก้ปัญหา concurrency ได้ แต่ refresh endpoint ยังคงต้องบังคับใช้การทำ token rotation และ rate limiting ที่เหมาะสม เนื่องจากแท็บที่ประสงค์ร้ายเพียงแท็บเดียวอาจส่งคำขอจำนวนมหาศาลไปยังเซิร์ฟเวอร์ได้ หากการล็อกถูกข้ามไป

บทสรุปนั้นง่ายมาก: ให้มองว่ากลุ่มของแท็บที่เปิดอยู่คือระบบแบบ distributed system และมอบ native synchronization primitive ให้กับพวกมัน ด้วยการเชื่อมต่อ Web Locks API เข้ากับ token-refresh flow นักพัฒนาจะสามารถกำจัด "browser tab token trap" และช่วยให้ผู้ใช้ยังคงอยู่ในระบบได้ ไม่ว่าจะเปิดแท็บทิ้งไว้มากแค่ไหนก็ตาม