การจัดการเซสชันสำหรับเว็บแอป: คุกกี้ vs JWT vs refresh
การจัดการเซสชันสำหรับเว็บแอป เปรียบเทียบเซสชันคุกกี้, JWT และ refresh token โดยใช้แบบจำลองภัยคุกคามจริงจังและข้อกำหนดการออกจากระบบที่สมจริง

สิ่งที่การจัดการเซสชันกำลังทำจริงๆ
เซสชันคือวิธีที่แอปของคุณตอบคำถามเดียวหลังจากผู้ใช้ล็อกอินว่า: "ตอนนี้คุณคือใคร?" เมื่อคำตอบนั้นเชื่อถือได้ แอปก็จะตัดสินใจได้ว่าผู้ใช้เห็นอะไร เปลี่ยนอะไรได้บ้าง และการกระทำใดต้องถูกบล็อก
การ "คงสถานะล็อกอิน" ก็เป็นการตัดสินใจด้านความปลอดภัยเช่นกัน คุณกำลังเลือกว่าตัวตนของผู้ใช้ควรถูกยอมรับนานแค่ไหน หลักฐานของตัวตนนั้นเก็บอยู่ที่ไหน และจะเกิดอะไรขึ้นถ้าหลักฐานนั้นถูกคัดลอกไป
การตั้งค่าเว็บแอปส่วนใหญ่ใช้โครงสร้างพื้นฐานสามอย่างเป็นหลัก:
- เซสชันแบบเซิร์ฟเวอร์ที่เก็บในคุกกี้: เบราว์เซอร์เก็บคุกกี้ และเซิร์ฟเวอร์ค้นหาเซสชันในทุกคำขอ
- JWT access token: ไคลเอนต์ส่งโทเค็นที่เซ็นแล้ว ซึ่งเซิร์ฟเวอร์ตรวจสอบได้โดยไม่ต้องดูฐานข้อมูล
- Refresh token: สิทธิยาวกว่าที่ใช้ขอ access token ใหม่ที่อายุสั้นกว่า
สิ่งเหล่านี้ไม่ใช่ "สไตล์" ที่แข่งกันเท่านั้น แต่เป็นวิธีต่างๆ ในการจัดการเทรดออฟเดียวกัน: ความเร็วกับการควบคุม ความเรียบง่ายกับความยืดหยุ่น และ "เราสามารถเพิกถอนได้ตอนนี้เลยไหม?" กับ "มันจะหมดอายุเองไหม?"
วิธีประเมินที่มีประโยชน์คือ: ถ้ามีคนขโมยสิ่งที่แอปใช้เป็นหลักฐาน (คุกกี้หรือโทเค็น) พวกเขาทำอะไรได้บ้าง และนานแค่ไหน? เซสชันแบบคุกกี้มักได้เปรียบเมื่อคุณต้องการการควบคุมฝั่งเซิร์ฟเวอร์อย่างเข้มงวด เช่น การบังคับออกจากระบบทันทีหรือการล็อกเอาต์ทันที JWT เหมาะกับการตรวจสอบแบบสเตตเลสข้ามบริการ แต่จะยุ่งยากเมื่อต้องการเพิกถอนทันที
ไม่มีทางเลือกใดชนะในทุกสถานการณ์ วิธีที่ถูกต้องขึ้นกับ threat model ของคุณ ข้อกำหนดการออกจากระบบ และความซับซ้อนที่ทีมของคุณรับได้
Threat models ที่เปลี่ยนคำตอบที่เหมาะสม
การออกแบบเซสชันที่ดีขึ้นอยู่กับว่าโจมตีแบบไหนที่คุณต้องทนทานได้ ไม่ใช่แค่ว่าโทเค็นไหน "ดีที่สุด"
ถ้าแฮกเกอร์ขโมยข้อมูลจากการเก็บในเบราว์เซอร์ (เช่น localStorage) การขโมย JWT access token ค่อนข้างง่ายเพราะโค้ดหน้าเพจสามารถอ่านมันได้ คุกกี้ที่ถูกตั้งค่าเป็น HttpOnly จะแตกต่าง: โค้ดปกติในเพจอ่านไม่ได้ ทำให้การโจมตีแบบ "ขโมยโทเค็น" ง่ายขึ้นน้อยลง แต่ถ้าแฮกเกอร์มีอุปกรณ์ (โน้ตบุ๊กหาย, มัลแวร์, คอมพิวเตอร์ที่ใช้ร่วมกัน) คุกกี้ก็ยังถูกคัดลอกจากโปรไฟล์เบราว์เซอร์ได้
XSS (โค้ดของผู้โจมตีรันในเพจของคุณ) เปลี่ยนภาพทั้งหมด ถ้ามี XSS ผู้โจมตีอาจไม่จำเป็นต้องขโมยอะไรเลย พวกเขาสามารถใช้เซสชันที่ผู้ใช้ล็อกอินอยู่แล้วเพื่อทำคำสั่งได้ HttpOnly ช่วยป้องกันการอ่านความลับของเซสชัน แต่ไม่หยุดผู้โจมตีจากการส่งคำขอจากเพจ
CSRF (ไซต์อื่นทำให้เกิดการกระทำที่ไม่พึงประสงค์) มักคุกคามเซสชันแบบคุกกี้เพราะเบราว์เซอร์แนบคุกกี้ให้โดยอัตโนมัติ หากคุณพึ่งคุกกี้ คุณต้องมีการป้องกัน CSRF ชัดเจน: ตั้งค่า SameSite อย่างตั้งใจ โทเค็นป้องกัน CSRF และจัดการคำขอที่เปลี่ยนสถานะอย่างระมัดระวัง JWT ที่ส่งใน header Authorization ถูกโดน CSRF แบบคลาสสิกน้อยกว่า แต่ยังเสี่ยงต่อ XSS ถ้าคุณเก็บไว้ที่ JavaScript อ่านได้
การโจมตีแบบ replay (นำสิทธิที่ขโมยมาใช้ซ้ำ) เป็นจุดที่เซสชันฝั่งเซิร์ฟเวอร์เด่น: คุณสามารถเพิกถอน session ID ได้ทันที JWT ที่มีอายุสั้นช่วยลดเวลาที่สามารถ replay ได้ แต่ไม่หยุดการ replay ในช่วงที่โทเค็นยังมีผล
อุปกรณ์ใช้ร่วมกันและโทรศัพท์หายทำให้ "ลงชื่อออก" เป็น threat model จริงๆ การตัดสินใจมักขึ้นกับคำถามเช่น: ผู้ใช้สามารถบังคับให้ลงชื่อออกจากอุปกรณ์อื่นได้ไหม ต้องมีผลเร็วแค่ไหน ถ้า refresh token ถูกขโมยจะเกิดอะไรขึ้น และคุณอนุญาต session แบบ "จดจำฉัน" ไหม หลายทีมจะตั้งมาตรฐานเข้มงวดกว่าสำหรับพนักงานมากกว่าลูกค้า เปลี่ยนเวลา timeout และความคาดหวังการเพิกถอน
เซสชันแบบคุกกี้: มันทำงานอย่างไรและปกป้องอะไร
เซสชันแบบคุกกี้เป็นการตั้งค่าคลาสสิก หลังจากล็อกอิน เซิร์ฟเวอร์สร้างเร็กคอร์ดเซสชัน (มักเป็น ID พร้อมฟิลด์อย่าง user ID, เวลาสร้าง, และวันหมดอายุ) เบราว์เซอร์เก็บเฉพาะ session ID ในคุกกี้ ในทุกคำขอ เบราว์เซอร์ส่งคุกกี้นั้นกลับมา และเซิร์ฟเวอร์ค้นหาเซสชันเพื่อยืนยันผู้ใช้คือใคร
ข้อได้เปรียบด้านความปลอดภัยที่สำคัญคือการควบคุม เซสชันถูกตรวจสอบที่เซิร์ฟเวอร์ทุกครั้ง ถ้าคุณต้องเตะใครสักคนออก คุณลบหรือปิดเร็กคอร์ดเซสชันฝั่งเซิร์ฟเวอร์และมันจะหยุดทำงานทันที แม้ผู้ใช้ยังมีคุกกี้ก็ตาม
การป้องกันส่วนมากมาจากการตั้งค่าคุกกี้:
- HttpOnly: ป้องกันไม่ให้ JavaScript อ่านคุกกี้
- Secure: ส่งคุกกี้เฉพาะผ่าน HTTPS
- SameSite: จำกัดเมื่อเบราว์เซอร์ส่งคุกกี้ข้ามไซต์
ที่เก็บสถานะเซสชันส่งผลต่อการสเกล การเก็บในหน่วยความจำของแอปง่ายแต่พังเมื่อรันหลายเซิร์ฟเวอร์หรือรีสตาร์ทบ่อย ฐานข้อมูลทำงานได้ดีเพื่อความทนทาน Redis นิยมเมื่อคุณต้องการการค้นหาที่เร็วและมีเซสชันจำนวนมาก จุดสำคัญคือเซิร์ฟเวอร์ต้องหาพบและยืนยันเซสชันในทุกคำขอ
เซสชันแบบคุกกี้เหมาะเมื่อคุณต้องการพฤติกรรมการออกจากระบบที่เข้มงวด เช่น แดชบอร์ดของพนักงานหรือพอร์ทัลลูกค้าที่ผู้ดูแลระบบต้องสามารถบังคับออกได้ เมื่อพนักงานลาออก การปิดการใช้งานเซสชันฝั่งเซิร์ฟเวอร์ยุติการเข้าถึงทันที โดยไม่ต้องรอโทเค็นหมดอายุ
JWT access tokens: จุดแข็งและข้อควรระวัง
JWT (JSON Web Token) คือสตริงที่เซ็นไว้ ซึ่งมี claim สั้นๆ เกี่ยวกับผู้ใช้ (เช่น user ID, role, tenant) พร้อมเวลาหมดอายุ API ของคุณตรวจสอบลายเซ็นและเวลาใช้ได้ภายในเซิร์ฟเวอร์โดยไม่ต้องเรียกฐานข้อมูล จากนั้นก็อนุญาตคำขอ
นี่คือเหตุผลที่ JWT เป็นที่นิยมในผลิตภัณฑ์ที่เน้น API, แอปมือถือ, และระบบที่หลายบริการต้องยืนยันตัวเดียวกัน ถ้าคุณมี backend หลายตัว แต่ละตัวสามารถยืนยันโทเค็นเดียวกันและได้คำตอบเดียวกัน
จุดแข็ง
JWT ตรวจสอบได้เร็วและส่งต่อได้ง่าย ถ้า frontend เรียก endpoint หลายตัว access token ที่อายุสั้นจะช่วยให้การไหลของคำขอราบรื่น: ตรวจลายเซ็น อ่าน user ID แล้วไปต่อ
ตัวอย่าง: พอร์ทัลลูกค้าเรียก "List invoices" และ "Update profile" บนบริการแยกกัน JWT สามารถใส่ customer ID และ role เช่น customer เพื่อให้แต่ละบริการสามารถอนุญาตคำขอโดยไม่ต้องค้นหาเซสชันทุกครั้ง
ข้อควรระวัง
เทรดออฟใหญ่คือการเพิกถอน ถ้าโทเค็นใช้ได้หนึ่งชั่วโมง มันมักจะใช้ได้ทุกที่ในชั่วโมงนั้น แม้ผู้ใช้กด "log out" หรือแอดมินปิดบัญชี เว้นแต่คุณจะเพิ่มการตรวจสอบฝั่งเซิร์ฟเวอร์เพิ่มเติม
JWT ก็รั่วไหลได้ในวิธีปกติ จุดบกพร่องที่พบบ่อยรวมถึง localStorage (XSS อ่านได้), หน่วยความจำของเบราว์เซอร์ (ส่วนขยายเป็นอันตราย), logs และรายงานข้อผิดพลาด, พร็อกซีและเครื่องมือวิเคราะห์ที่จับ header, และโทเค็นที่ถูกคัดลอกในแชทสนับสนุนหรือสกรีนช็อต
เพราะเหตุนี้ JWT access token เหมาะกับการเข้าถึงระยะสั้น ไม่ใช่การล็อกอิน "ตลอดไป" เก็บข้อมูลให้สั้น (ไม่ใส่ข้อมูลส่วนตัวที่ละเอียดอ่อน), ตั้งเวลาให้สั้น และสมมติว่าโทเค็นที่ถูกขโมยจะใช้ได้จนกว่าจะหมดอายุ
Refresh tokens: ทำให้การตั้งค่า JWT ใช้งานได้จริง
JWT access token ถูกออกแบบมาให้อายุสั้น นั่นดีสำหรับความปลอดภัย แต่มันสร้างปัญหาทางปฏิบัติ: ผู้ใช้ไม่ควรต้องล็อกอินใหม่ทุกไม่กี่นาที Refresh token แก้ปัญหานั้นโดยให้แอปขอ access token ใหม่เงียบๆ เมื่ออันเก่าหมดอายุ
ที่เก็บ refresh token สำคัญกว่าที่เก็บ access token ในเว็บแอป ค่าเริ่มต้นที่ปลอดภัยที่สุดบนเบราว์เซอร์คือคุกกี้ HttpOnly, Secure เพื่อ JavaScript อ่านไม่ได้ local storage ทำให้ง่ายแต่ก็ง่ายต่อการขโมยถ้ามีบั๊ก XSS หาก threat model ของคุณรวม XSS ให้หลีกเลี่ยงการเก็บความลับระยะยาวในที่ JavaScript อ่านได้
การหมุน (rotation) คือสิ่งที่ทำให้ refresh token ใช้งานได้ในระบบจริง แทนที่จะใช้ refresh token เดิมเป็นสัปดาห์ๆ คุณสลับมันทุกครั้งที่ใช้: ไคลเอนต์ส่ง refresh token A เซิร์ฟเวอร์ออก access token ใหม่บวก refresh token B และ refresh token A ถูกทำให้ใช้ไม่ได้
การตั้งค่าง่ายๆ ของการหมุนมักมีหลักการไม่กี่ข้อ:
- เก็บ access token ให้สั้น (เป็นนาที ไม่ใช่ชั่วโมง)
- เก็บสถานะ refresh token ฝั่งเซิร์ฟเวอร์พร้อมสถานะและเวลาที่ใช้ล่าสุด
- หมุนทุกครั้งที่รีเฟรชและเพิกถอนโทเค็นก่อนหน้า
- ผูก refresh token กับอุปกรณ์หรือเบราว์เซอร์เมื่อเป็นไปได้
- บันทึกเหตุการณ์การรีเฟรชเพื่อสอบสวนการใช้งานที่ผิดปกติ
การตรวจจับการนำกลับมาใช้ซ้ำคือสัญญาณเตือนสำคัญ ถ้า refresh token A ถูกแลกไปแล้ว แต่คุณเห็นอีกครั้ง ให้สมมติว่ามันถูกคัดลอก การตอบสนองที่พบบ่อยคือเพิกถอนทั้งเซสชัน (และบ่อยครั้งทุกเซสชันของผู้ใช้นั้น) และขอให้ล็อกอินใหม่ เพราะคุณไม่รู้ว่าอันไหนคือต้นฉบับ
สำหรับการออกจากระบบ คุณต้องมีสิ่งที่เซิร์ฟเวอร์บังคับได้ ซึ่งมักหมายถึงตารางเซสชัน (หรือรายการเพิกถอน) ที่ทำเครื่องหมายว่า refresh token ถูกเพิกถอน Access token อาจยังใช้งานได้จนกว่าจะหมดอายุ แต่คุณสามารถลดหน้าต่างนั้นโดยตั้ง access token ให้สั้น
ข้อกำหนดการออกจากระบบและสิ่งที่บังคับจริงได้
การออกจากระบบฟังดูง่ายจนกว่าคุณจะนิยามมัน มักมีคำขอสองแบบ: "ลงชื่อออกอุปกรณ์นี้" (เบราว์เซอร์หรือโทรศัพท์เครื่องเดียว) และ "ลงชื่อออกทุกที่" (เซสชันที่ใช้งานอยู่ทั้งหมดทุกอุปกรณ์)
ยังมีคำถามเรื่องเวลาอีกด้วย "ออกจากระบบทันที" หมายความว่าแอปจะหยุดยอมรับสิทธิเลยตอนนี้ ในขณะที่ "ออกจากระบบเมื่อหมดอายุ" หมายถึงแอปจะหยุดยอมรับเมื่อเซสชันหรือโทเค็นหมดอายุเอง
กับเซสชันแบบคุกกี้ การออกจากระบบทันทีทำได้ตรงไปตรงมาด้วยเพราะเซิร์ฟเวอร์เป็นเจ้าของเซสชัน คุณลบคุกกี้ในฝั่งไคลเอนต์และเพิกถอนเร็กคอร์ดเซสชันฝั่งเซิร์ฟเวอร์ได้ หากมีคนคัดลอกค่าคุกกี้ก่อนหน้านั้น การปฏิเสธจากเซิร์ฟเวอร์คือสิ่งที่บังคับการออกจากระบบจริง
กับการพิสูจน์ตัวด้วย JWT เพียงอย่างเดียว (access token แบบ stateless และไม่มีการตรวจสอบฝั่งเซิร์ฟเวอร์) คุณไม่สามารถรับประกันการออกจากระบบทันทีได้ โทเค็นที่ถูกขโมยยังใช้ได้จนกว่าจะหมดอายุ เพราะเซิร์ฟเวอร์ไม่มีที่เช็คว่า "โทเค็นนี้ถูกเพิกถอนหรือยัง?" คุณสามารถเพิ่ม denylist แต่ก็หมายถึงการเก็บ state และตรวจสอบมัน ซึ่งทำให้สูญเสียความเรียบง่ายเดิม
แนวปฏิบัติจริงคือมอง access token เป็นอายุสั้นและบังคับการออกจากระบบผ่าน refresh token Access token อาจยังใช้งานได้อีกไม่กี่นาที แต่ refresh token คือสิ่งที่รักษาเซสชันให้มีชีวิตอยู่ หากโน้ตบุ๊กถูกขโมย การเพิกถอนครอบครัว refresh token จะตัดการเข้าถึงได้อย่างรวดเร็ว
สิ่งที่คุณสามารถสัญญาต่อผู้ใช้ได้จริง:
- ลงชื่อออกอุปกรณ์นี้: เพิกถอนเซสชันหรือ refresh token นั้น และลบคุกกี้หรือ storage ท้องถิ่น
- ลงชื่อออกทุกที่: เพิกถอนทุกเซสชันหรือทุกครอบครัว refresh token ของบัญชี
- ผลทันที: รับประกันได้กับเซสชันฝั่งเซิร์ฟเวอร์, พยายามให้ดีที่สุดกับ access token จนกว่าจะหมดอายุ
- เหตุการณ์บังคับลงชื่อออก: เปลี่ยนรหัสผ่าน ปิดบัญชี หรือการลดบทบาท
สำหรับการเปลี่ยนรหัสผ่านและการปิดบัญชี อย่าเชื่อว่า "ผู้ใช้จะออกจากระบบเอง" เก็บเวอร์ชันเซสชันระดับบัญชี (หรือ timestamp แบบ "token valid after") ในการรีเฟรชแต่ละครั้ง (และบางครั้งในแต่ละคำขอ) ให้เปรียบเทียบ ถ้ามันเปลี่ยน ปฏิเสธและขอให้ล็อกอินใหม่
ทีละขั้นตอน: เลือกแนวทางเซสชันสำหรับแอปของคุณ
ถ้าคุณอยากให้การออกแบบเซสชันเรียบง่าย ตัดสินกฎก่อนแล้วค่อยเลือกวิธีการ ปัญหาส่วนมากเริ่มเมื่อทีมเลือก JWT หรือคุกกี้เพราะมันฮิต ไม่ใช่เพราะมันตรงกับความเสี่ยงและข้อกำหนดการออกจากระบบ
เริ่มจากการระบุทุกที่ที่ผู้ใช้ล็อกอิน แอปเบราเซอร์แตกต่างจากแอปมือถือ native, เครื่องมือแอดมินภายใน, หรือการผนวกของพาร์ทเนอร์ แต่ละแบบเปลี่ยนวิธีเก็บรักษาอย่างปลอดภัย วิธีต่ออายุการล็อกอิน และความหมายของ "ออกจากระบบ"
ลำดับปฏิบัติที่ใช้ได้กับหลายทีม:
- ระบุไคลเอนต์ของคุณ: เว็บ, iOS/Android, เครื่องมือภายใน, การเข้าถึงภายนอก
- เลือก threat model เริ่มต้น: XSS, CSRF, อุปกรณ์ถูกขโมย
- ตัดสินว่าการออกจากระบบต้องรับประกันอะไร: อุปกรณ์นี้, ทุกอุปกรณ์, แอดมินบังคับออก
- เลือกรูปแบบพื้นฐาน: เซสชันแบบคุกกี้ (เซิร์ฟเวอร์จำ) หรือ access token + refresh token
- ตั้งค่า timeout และกฎตอบสนอง: idle vs absolute expiry รวมถึงทำอย่างไรเมื่อเห็นการใช้งานซ้ำที่น่าสงสัย
จากนั้นจงเขียนสัญญาชัดเจนว่าระบบของคุณรับประกันอะไร ตัวอย่าง: "เว็บเซสชันหมดอายุหลังไม่ทำอะไร 30 นาที หรือ 7 วันแบบ absolute. แอดมินสามารถบังคับออกภายใน 60 วินาที. โทรศัพท์หายสามารถปิดการใช้งานจากระยะไกล." ประโยคเหล่านี้สำคัญกว่าลibraries ที่คุณใช้
สุดท้าย เพิ่มการมอนิเตอร์ที่สอดคล้องกับรูปแบบของคุณ สำหรับการตั้งค่าโทเค็น สัญญาณที่แข็งแรงคือการนำ refresh token มาใช้ซ้ำ (same refresh token ถูกใช้ซ้ำ) ให้ปฏิบัติเหมือนการโจรกรรม ยกเลิกครอบครัวเซสชัน และเตือนผู้ใช้
ข้อผิดพลาดที่พบบ่อยที่นำไปสู่อควอนต์แอดเคานต์
เหตุการณ์การยึดบัญชีส่วนมากไม่ใช่ "แฮ็กฉลาด" แต่เป็นชัยชนะง่ายๆ จากข้อผิดพลาดการจัดการเซสชัน การจัดการเซสชันที่ดีส่วนมากคือการไม่ให้ผู้โจมตีทางลัดในการขโมยหรือนำสิทธิซ้ำ
กับดักที่พบบ่อยคือเก็บ access token ใน localStorage และหวังว่าจะไม่มี XSS ถ้ามีสคริปต์ไหนรันบนเพจของคุณ (dependency ที่เป็นอันตราย, widget ถูกฉีด, คอมเมนต์ที่ถูกเก็บ) มันสามารถอ่าน localStorage และส่งโทเค็นออกไปได้ คุกกี้ที่มี HttpOnly ลดความเสี่ยงนี้เพราะ JavaScript อ่านไม่ได้
กับดักอีกอย่างคือทำให้ JWT อยู่ได้นานเพื่อหลีกเลี่ยง refresh token โทเค็นอายุ 7 วันคือหน้าต่างการนำกลับมาใช้ซ้ำ 7 วันถ้ามันรั่ว Access token สั้นๆ บวกกับ refresh token ที่จัดการดีจะยากต่อการนำไปใช้ประโยชน์ โดยเฉพาะเมื่อคุณสามารถตัดการรีเฟรชได้
คุกกี้มีกับดักของตัวเอง: ลืมการป้องกัน CSRF ถ้าแอปของคุณใช้เซสชันคุกกี้และยอมรับคำขอที่เปลี่ยนสถานะโดยไม่มีการป้องกัน CSRF เว็บไซต์อันตรายสามารถหลอกเบราว์เซอร์ที่ล็อกอินให้ส่งคำขอที่ถูกต้องได้
ข้อผิดพลาดอื่นๆ ที่มักเจอในการรีวิวเหตุการณ์:
- Refresh token ไม่เคยหมุน หรือหมุนแต่คุณไม่ตรวจจับการนำกลับมาใช้ซ้ำ
- คุณรองรับวิธีล็อกอินหลายแบบ (เซสชันคุกกี้และ bearer token) แต่กฎ "แบบไหนชนะ" ในเซิร์ฟเวอร์ไม่ชัดเจน
- โทเค็นตกไปอยู่ใน logs (console เบราว์เซอร์, เหตุการณ์วิเคราะห์, log เซิร์ฟเวอร์) ซึ่งถูกคัดลอกและเก็บไว้
ตัวอย่างชัดเจน: เจ้าหน้าที่ซัพพอร์ตวาง "debug log" ลงในตั๋ว บันทึกนั้นมี header Authorization ใครก็ตามที่เข้าถึงตั๋วได้สามารถ replay โทเค็นนั้นและทำงานในนามของเจ้าหน้าที่ได้ ปฏิบัติต่อโทเค็นเหมือนรหัสผ่าน: อย่าพิมพ์พวกมัน อย่าเก็บพวกมัน และให้สั้นเท่าที่จะเป็นไปได้
ตรวจสอบด่วนก่อนเปิดใช้งาน
บั๊กเซสชันส่วนมากไม่ได้เกี่ยวกับคริปโตหรูหรา แต่มาจากธงที่หายไป โทเค็นที่ใช้ชีวิตนานเกินไป หรือตัว endpoint หนึ่งที่ควรต้องการการยืนยันตัวตนนั้นไม่ได้บังคับ
ก่อนปล่อย ให้ทำการตรวจสอบสั้นๆ ที่มุ่งไปที่สิ่งที่ผู้โจมตีทำได้ถ้าได้คุกกี้หรือโทเค็นที่ถูกขโมย นี่เป็นวิธีเร็วที่สุดในการปรับปรุงความปลอดภัยโดยไม่ต้องเขียนระบบ auth ใหม่ทั้งหมด
เช็กลิสต์ก่อนปล่อย
เดินตามเช็คลิสต์เหล่านี้ในสเตจ แล้วทำอีกครั้งในโปรดักชัน:
- เก็บ access token ให้สั้น (นาที) และยืนยันว่า API ปฏิเสธหลังหมดอายุจริง
- ปฏิบัติต่อ refresh token เหมือนรหัสผ่าน: เก็บในที่ JavaScript อ่านไม่ได้ถ้าเป็นไปได้, ส่งเฉพาะไปยัง endpoint การรีเฟรช, และหมุนหลังการใช้ทุกครั้ง
- ถ้าใช้คุกกี้สำหรับการยืนยันตัวตน ให้ยืนยัน flag: เปิด HttpOnly, เปิด Secure, และตั้งค่า SameSite อย่างตั้งใจ ตรวจสอบขอบเขตคุกกี้ (domain และ path) ว่าไม่กว้างเกินจำเป็น
- ถ้าคุกกี้ยืนยันคำขอ ให้เพิ่มการป้องกัน CSRF และยืนยันว่า endpoint ที่เปลี่ยนสถานะล้มเหลวถ้าไม่มีสัญญาณ CSRF
- ทำให้การเพิกถอนเป็นของจริง: หลังรีเซ็ตรหัสผ่านหรือปิดบัญชี เซสชันที่มีอยู่ควรหยุดทำงานเร็ว (ลบเซสชันฝั่งเซิร์ฟเวอร์, เพิกถอน refresh token, หรือตรวจสอบ "session version")
หลังจากนั้น ทดสอบสัญญาการออกจากระบบของคุณ "ออกจากระบบ" มักหมายถึง "ลบเซสชันท้องถิ่น" แต่ผู้ใช้คาดหวังมากกว่า
การทดสอบที่เป็นประโยชน์: ล็อกอินบนโน้ตบุ๊กและโทรศัพท์ จากนั้นเปลี่ยนรหัสผ่าน โน้ตบุ๊กควรถูกบังคับให้หลุดเมื่อมีคำขอครั้งถัดไป ไม่ใช่ชั่วโมงต่อมา ถ้าคุณเสนอ "ออกจากระบบทุกที่" และรายการอุปกรณ์ ให้ยืนยันว่าแต่ละอุปกรณ์แมปกับเซสชันหรือเร็กคอร์ด refresh token ที่แยกกันซึ่งคุณสามารถเพิกถอนได้
ตัวอย่าง: พอร์ทัลลูกค้าพร้อมบัญชีพนักงานและการบังคับออก
ลองนึกภาพธุรกิจเล็กๆ ที่มีพอร์ทัลลูกค้าเว็บ (ลูกค้าตรวจสอบใบแจ้งหนี้ เปิดตั๋ว) และแอปมือถือสำหรับพนักงานภาคสนาม (งาน หมายเหตุ รูปภาพ) พนักงานบางคนทำงานในพื้นที่ไม่มีสัญญาณ ดังนั้นแอปต้องทำงานออฟไลน์ได้บ้าง แอดมินอยากมีปุ่มใหญ่สีแดง: ถ้าแท็บเล็ตหายหรือผู้รับเหมาออก พวกเขาสามารถบังคับให้ล็อกเอาต์
เพิ่มภัยคุกคามสามอย่าง: แท็บเล็ตแชร์กันในรถตู้ (ลืมล็อกเอาต์), ฟิชชิ่ง (พนักงานกรอกข้อมูลในหน้าปลอม), และบั๊ก XSS ในพอร์ทัล (สคริปต์พยายามขโมยข้อมูล)
การตั้งค่าที่ใช้งานได้จริงในที่นี้คือ access token อายุสั้นบวกกับ refresh token ที่หมุนได้ และมีการเพิกถอนฝั่งเซิร์ฟเวอร์ มันให้การเรียก API ที่รวดเร็วและทนต่อการออฟไลน์ได้ พร้อมกับให้แอดมินตัดการเข้าถึงได้
ตัวอย่างการตั้งค่า:
- อายุ access token: 5–15 นาที
- การหมุน refresh token: ทุกการรีเฟรชจะคืน refresh token ใหม่ และทำให้ตัวก่อนหน้านั้นใช้ไม่ได้
- เก็บ refresh token อย่างปลอดภัย: บนเว็บ เก็บในคุกกี้ HttpOnly, Secure; บนมือถือ เก็บใน secure storage ของระบบปฏิบัติการ
- ติดตาม refresh token ฝั่งเซิร์ฟเวอร์: เก็บเร็กคอร์ดโทเค็น (ผู้ใช้, อุปกรณ์, เวลาที่ออก, ใช้ล่าสุด, ธงเพิกถอน) ถ้าโทเค็นที่ถูกหมุนแล้วถูกใช้ซ้ำ ให้ถือว่าเป็นการโจรกรรมและเพิกถอนทั้งเชน
การบังคับออกทำได้: แอดมินเพิกถอนเร็กคอร์ด refresh token สำหรับอุปกรณ์นั้น (หรือทุกอุปกรณ์ของผู้ใช้) อุปกรณ์ที่ถูกขโมยยังใช้งาน access token ปัจจุบันจนกว่าจะหมดอายุ แต่จะขออันใหม่ไม่ได้ ดังนั้นเวลาสูงสุดที่ต้องรอเพื่อตัดการเข้าถึงขึ้นกับอายุของ access token
สำหรับอุปกรณ์หาย ให้กำหนดกฎเป็นภาษาธรรมดา: "ภายใน 10 นาที แอปจะหยุดซิงค์และต้องล็อกอินใหม่" งานออฟไลน์ยังคงอยู่บนอุปกรณ์ แต่การซิงค์ครั้งต่อไปเมื่อออนไลน์จะล้มเหลวจนกว่าผู้ใช้จะลงชื่อเข้าใช้ใหม่
ขั้นตอนต่อไป: ลงมือทำ ทดสอบ และทำให้ง่ายต่อการดูแล
เขียนสิ่งที่ "ออกจากระบบ" หมายถึงเป็นภาษาผลิตภัณฑ์ที่ชัดเจน เช่น: "การออกจากระบบจะยกเลิกการเข้าถึงบนอุปกรณ์นี้" "ออกจากระบบทุกที่จะเตะออกทุกอุปกรณ์ภายใน 1 นาที" หรือ "การเปลี่ยนรหัสผ่านจะล็อกเอาต์เซสชันอื่นๆ" คำสัญญาเหล่านี้ตัดสินว่าคุณต้องมีสถานะเซสชันฝั่งเซิร์ฟเวอร์ รายการเพิกถอน หรือโทเค็นอายุสั้นหรือไม่
แปลงคำสัญญาเป็นแผนทดสอบเล็กๆ บั๊กโทเค็นและเซสชันมักดูดีในทางที่เป็นปกติ แต่ล้มเหลวในชีวิตจริง (โหมดสลีป, เครือข่ายไม่เสถียร, หลายอุปกรณ์)
เช็กลิสต์ทดสอบเชิงปฏิบัติ
รันการทดสอบที่ครอบคลุมกรณียุ่งๆ:
- หมดอายุ: การเข้าถึงหยุดเมื่อ access token หรือเซสชันหมดอายุ แม้เบราว์เซอร์ยังเปิดอยู่
- เพิกถอน: หลัง "ออกจากระบบทุกที่" สิทธิเดิมล้มเหลวในการขอครั้งถัดไป
- การหมุน: การหมุน refresh token ออก refresh token ใหม่และทำให้ของเก่าใช้ไม่ได้
- การตรวจจับการนำกลับมาใช้: การ replay โทเค็น refresh เก่าเปิดใช้นโยบายล็อกดาวน์
- หลายอุปกรณ์: กฎสำหรับ "อุปกรณ์ปัจจุบันเท่านั้น" กับ "ทุกอุปกรณ์" ถูกบังคับและ UI สอดคล้อง
หลังทดสอบ ทำการซ้อมโจมตีง่ายๆ กับทีม เลือกสามเรื่องและเดินผ่านตั้งแต่ต้นจนจบ: บั๊ก XSS ที่อ่านโทเค็นได้, พยายาม CSRF กับเซสชันคุกกี้, และโทรศัพท์ถูกขโมยพร้อมเซสชันที่ยังใช้งานอยู่ คุณกำลังตรวจว่าการออกแบบตรงกับคำสัญญาหรือไม่
ถ้าคุณต้องไปเร็ว ลดการเขียนโค้ดเฉพาะแบบจูนเอง AppMaster (appmaster.io) เป็นทางเลือกหนึ่งเมื่อคุณต้องการ backend ที่สร้างได้พร้อมใช้งานจริงทั้งเว็บและแอป native เพื่อให้คุณรักษากฎอย่าง expiry, rotation, และ forced logout ให้สอดคล้องกันข้ามไคลเอนต์
กำหนดการทบทวนหลังเปิดใช้ ใช้ตั๋วซัพพอร์ตจริงและเหตุการณ์จริงเพื่อปรับเวลา timeout, ขีดจำกัดเซสชัน และพฤติกรรม "ออกจากระบบทุกที่" จากนั้นรันเช็กลิสต์เดิมอีกครั้งเพื่อให้การแก้ไขไม่ถูกรักษาไว้เป็น regression


