ข้ามไปยังเนื้อหา
กลับไปหน้าบทความ
การพัฒนาซอฟต์แวร์

JWT กับ Session Cookie ควรใช้แบบไหน

Session cookie เก็บ state ไว้ที่ server ส่วน JWT เก็บ claim ไว้ในตัว token เทียบเรื่อง revocation ที่เก็บใน browser XSS กับ CSRF และ cookie attribute ที่ควรตั้ง

ปานกลาง7 นาทีในการอ่าน

ในบทความนี้

ตอบก่อนเลยครับ session cookie คือ cookie ที่เก็บ session ID แบบสุ่ม ส่วนข้อมูลจริงของ session อยู่ที่ server ทุก request server ต้องเอา ID ไปค้นใน session store JWT คือ token ที่มี claim อยู่ในตัวและมีลายเซ็น server ตรวจลายเซ็นกับวันหมดอายุได้เองโดยไม่ต้องค้นอะไร

สองอย่างนี้ไม่ได้อยู่คนละฝั่งกันเสียทีเดียว JWT ก็ใส่ใน cookie ได้ คำถามที่ต้องตอบจริง ๆ จึงมีสองข้อ คือ เมื่อผู้ใช้ logout หรือ token รั่ว เราต้องยกเลิกได้ทันทีหรือไม่ และ token ถูกเก็บไว้ที่ไหนใน browser ถ้าเป็นเว็บแอปที่มี backend ตัวเดียว ผมเริ่มจาก session cookie ก่อน ส่วนเหตุผลอยู่ในหัวข้อข้างล่างครับ

หลัง logout แบบ session cookie server ลบแถว session ทิ้ง request ถัดไปจึงได้ 401 ส่วน JWT ที่ถูกคัดลอกไว้ยังผ่านการตรวจจนถึงเวลา exp

server สร้าง ID แบบสุ่มหนึ่งค่า เก็บข้อมูลของผู้ใช้ไว้ฝั่งตัวเองโดยใช้ ID นั้นเป็น key แล้วส่ง ID ให้ browser ใน cookie RFC 6265 อธิบายแบบนี้ว่า แทนที่จะเก็บข้อมูล session ไว้ใน cookie ตรง ๆ server มักเก็บ nonce หรือ session identifier ไว้ใน cookie แล้วใช้ค่านั้นเป็น key ค้นข้อมูล (หัวข้อ 8.4)

ตอน login สำเร็จ response จะมี header ประมาณนี้

HTTP/1.1 200 OK
Set-Cookie: __Host-sid=7f3a9c...; Path=/; Secure; HttpOnly; SameSite=Lax

หลังจากนั้น browser แนบ Cookie: __Host-sid=7f3a9c... ไปกับทุก request ที่ไปยัง host นี้เอง server ค้น ID ใน store ถ้าเจอก็รู้ว่าเป็นใคร

ข้อดีที่ RFC 6265 ชี้คือ ถ้า cookie รั่ว ID นั้นใช้ได้แค่กับ server ตัวนั้น ไม่มีข้อมูลผู้ใช้อยู่ในตัว เงื่อนไขคือ ID ต้องเดาไม่ได้ OWASP กำหนดว่า session ID ต้องมี entropy อย่างน้อย 64 bit และต้องสร้าง ID ใหม่ทุกครั้งที่ระดับสิทธิ์เปลี่ยน โดยเฉพาะตอน login เพื่อกัน session fixation ซึ่ง RFC 6265 ก็เตือนไว้ด้วย

JWT ทำงานอย่างไร

JWT ตาม RFC 7519 คือวิธีส่ง claim ระหว่างสองฝ่ายแบบกะทัดรัดและใส่ใน URL ได้ claim เป็น JSON object ที่อยู่ใน JWS (มีลายเซ็นหรือ MAC) หรือใน JWE (เข้ารหัส) แบบที่เจอบ่อยที่สุดคือ JWS ซึ่งมีสามส่วนคั่นด้วยจุด คือ header, payload และ signature แต่ละส่วนเข้ารหัสแบบ base64url

header กับ payload ที่ decode แล้วหน้าตาประมาณนี้

{"alg": "ES256", "typ": "JWT"}
{"sub": "42", "aud": "api.example.com", "exp": 1762160400, "jti": "a81f..."}

claim ที่เกี่ยวกับเรื่องนี้โดยตรงมีสองตัว exp คือเวลาที่หลังจากนั้น JWT ต้องไม่ถูกรับ และ jti คือID เฉพาะของ token ซึ่งใช้กัน replay ได้

อะไรที่คนมักเข้าใจผิดเกี่ยวกับ JWT

Logout แล้ว token ถูกยกเลิกทันทีหรือไม่

ถ้าเป็น session cookie ใช่ครับ server ลบแถว session ออกจาก store request ถัดไปที่ส่ง ID เดิมมาจะค้นไม่เจอ ID นั้นไม่มีความหมายอีกต่อไปทันที ไม่ว่าจะมีใครคัดลอกเก็บไว้หรือไม่

ถ้าเป็น JWT ไม่ใช่ OWASP สรุปว่าเป็นข้อจำกัดในตัว JWT คือ token จะหมดสภาพก็ต่อเมื่อหมดอายุ การลบ token ออกจาก browser ตอน logout แค่ทำให้แท็บนั้นไม่มี token แต่ถ้า token ถูกคัดลอกไปแล้ว API ที่ตรวจแค่ลายเซ็นกับ exp ยังรับต่อจนถึงเวลา exp

วิธีแก้ที่ OWASP ยกไว้คือทำ denylist เก็บdigest ของ token พร้อมวันที่ยกเลิก และเก็บไว้อย่างน้อยจนถึงวันหมดอายุของ token ซึ่งใช้งานได้ แต่ถึงตอนนั้น API ต้องค้น denylist ทุก request ซึ่งก็คือการกลับไปมี state ฝั่ง server เหมือน session ส่วนอีกทางคือตั้ง exp ให้สั้น แล้วยอมรับว่าในช่วงนั้นยังยกเลิกไม่ได้

ระยะเวลาที่ใช้เทียบได้คือ timeout ของ session OWASP ระบุช่วงที่พบบ่อยไว้ว่า idle timeout 2-5 นาทีสำหรับแอปที่มีมูลค่าสูง และ 15-30 นาทีสำหรับแอปความเสี่ยงต่ำ ถ้า JWT มีอายุยาวกว่านี้มาก ก็หมายความว่า token ที่รั่วใช้ได้นานกว่า session ที่ถูก logout ไปแล้ว

ควรเก็บ token ไว้ที่ไหนใน browser

มีสามที่ที่ใช้กันจริง และแต่ละที่มีความเสี่ยงต่างกัน

ที่เก็บ JavaScript อ่านได้ไหม browser ส่งให้เองไหม หลังปิด browser
cookie ที่ตั้ง HttpOnly ไม่ได้ ส่งทุก request ที่ตรงเงื่อนไข หายถ้าไม่มี Expires/Max-Age
localStorage ได้ ทุก script ใน origin เดียวกัน ไม่ส่ง ต้องใส่ header เอง ยังอยู่
sessionStorage หรือตัวแปรใน JavaScript ได้ ไม่ส่ง หายเมื่อปิดแท็บ

ข้อมูลในตารางมาจากหลายที่ HttpOnly สั่งให้ browser ไม่เปิด cookie ให้ API ที่ไม่ใช่ HTTP เช่น API ของ browser ที่ให้ script อ่าน cookie แต่ cookie นั้นยังถูกส่งไปกับ request ที่ JavaScript สร้าง เช่น fetch() ข้อมูลใน localStorage อ่านได้จากทุกหน้าที่โหลดจาก origin เดียวกัน และยังอยู่ข้ามการเปิดปิด browser ส่วน cookie ที่ไม่ระบุ Expires หรือ Max-Age เป็น session cookie ที่ถูกลบเมื่อ client ปิด (แม้ MDN จะเตือนว่าฟีเจอร์ session restore ของ browser อาจกู้มันกลับมา)

XSS กับ CSRF กระทบแต่ละแบบอย่างไร

ทั้งสองแบบโดนคนละจุด XSS คือ script ของผู้โจมตีรันอยู่ในหน้าเว็บของเรา CSRF คือเว็บอื่นทำให้ browser ของผู้ใช้ส่ง request มาหาเราพร้อม cookie

ถ้าเก็บใน cookie

ปัญหาหลักคือ CSRF RFC 6265 อธิบายว่า browser แนบ cookie ไปด้วยแม้ฝ่ายที่สั่งให้ส่ง request จะไม่รู้ค่าของ cookie (หัวข้อ 8.2) เว็บอื่นจึงสร้าง form ที่ POST มาที่ API ของเราแล้วได้สิทธิ์ของผู้ใช้ไปด้วย

ตัวช่วยหลักคือ attribute SameSite ณ ตอนนี้ยังอยู่ในร่าง RFC 6265bis (draft -21, กันยายน 2568) ยังไม่ออกเป็น RFC ร่างนี้กำหนดว่า Strict ส่ง cookie เฉพาะ request แบบ same-site Lax ส่งกับ same-site และกับ top-level navigation ข้ามไซต์ ส่วน None ส่งทุกกรณี ร่างเดียวกันเขียนว่า SameSite ไม่ควรเป็นการป้องกัน CSRF เพียงชั้นเดียว (หัวข้อ 8.8.1) ถ้าไม่ตั้ง SameSite เลย ค่าเริ่มต้นขึ้นกับ browser MDN ระบุว่าบาง browser ใช้ Lax จึงควรตั้งเองให้ชัด

ส่วน XSS HttpOnly กันไม่ให้ script ขโมยค่า cookie ไปใช้ที่อื่น OWASP ถือว่าจำเป็นสำหรับ session ID แต่ก็เขียนไว้ด้วยว่าถ้า XSS ส่ง request จากหน้านั้นเอง browser ก็ยังแนบ session cookie ไปให้ HttpOnly จึงกันการขโมย ไม่ได้กันการใช้งานในหน้านั้น

ถ้าเก็บใน JavaScript แล้วส่งเป็น header

ถ้าแอปเก็บ JWT ไว้ฝั่ง JavaScript แล้วส่งเป็น Authorization: Bearer ... เอง browser จะไม่แนบให้ request ที่เว็บอื่นสร้าง CSRF แบบเดิมจึงใช้ไม่ได้ แต่ XSS อ่าน token ได้ตรง ๆ และเอาไปใช้ที่เครื่องอื่นต่อได้จนกว่า token จะหมดอายุ

OWASP เวอร์ชันกลางปี 2568 แนะนำทางผสมไว้ คือเก็บ JWT ใน sessionStorage หรือตัวแปรใน closure แล้วผูก token กับ "user context" ซึ่งเป็นค่าสุ่มที่ส่งให้ browser ใน cookie แบบ HttpOnly + Secure และเก็บ SHA-256 ของค่านั้นไว้ใน token token ที่ถูกขโมยไปโดยไม่มี cookie นั้นจะใช้ไม่ได้ ทางนี้ได้ผล แต่ก็ต้องดูแลทั้ง token และ cookie ไปพร้อมกัน

ถ้าใช้ cookie ไม่ว่าจะเก็บ session ID หรือ JWT ผมตั้งตามรายการนี้

ควรเลือกแบบไหน

OWASP เองเขียนไว้ว่าถ้าแอปไม่จำเป็นต้อง stateless ทั้งหมด ให้พิจารณาใช้ระบบ session ที่ web framework มีให้ ผมใช้เกณฑ์ประมาณนี้ครับ

เลือก session cookie เมื่อ

  • เป็นเว็บแอปที่ browser คุยกับ backend ของเราเองโดยตรง
  • ต้อง logout หรือระงับบัญชีแล้วมีผลทันที
  • มี session store อยู่แล้ว หรือเพิ่มได้ไม่ยาก

เลือก JWT เมื่อ

  • token ต้องถูกตรวจโดยหลาย service ที่ไม่ได้ใช้ store ร่วมกัน
  • ยอมรับได้ว่า token ใช้ได้จนถึง exp หรือมีวิธียกเลิกรองรับแล้ว
  • ทีมตรวจ alg, aud, exp ครบ และไม่ใส่ข้อมูลลับใน payload

สองแบบใช้ร่วมกันได้ด้วย เช่น browser ใช้ session cookie กับ backend ของแอป แล้ว backend ใช้ token อายุสั้นคุยกับ service อื่นต่อ วิธีนี้ทำให้ token ไม่ต้องอยู่ใน browser เลยครับ

แหล่งอ้างอิง

อ่านต่อ