JWT กับ Session Cookie ควรใช้แบบไหน
Session cookie เก็บ state ไว้ที่ server ส่วน JWT เก็บ claim ไว้ในตัว token เทียบเรื่อง revocation ที่เก็บใน browser XSS กับ CSRF และ cookie attribute ที่ควรตั้ง
ปานกลาง7 นาทีในการอ่าน
SESSION COOKIE the state lives on the server ┌─ server ─────────┐ ┌────────┐ Cookie: sid=7f3a9c │ session store ● │ │ browser│ ──────────────────────────────── │ │ └────────┘ │ 7f3a9c user 42 │ │ │ └──────────────────┘ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ JWT the state lives inside the token ┌─ API ────────────┐ ┌────────┐ Bearer eyJhbGciOi... │ [ ] signature │ │ browser│ ──────────────────────────────── │ [ ] exp, aud │ └────────┘ │ no lookup │ │ exp 10:15 │ └──────────────────┘ logout ends a session at once; a JWT lives until exp
ในบทความนี้
ตอบก่อนเลยครับ 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 ก่อน ส่วนเหตุผลอยู่ในหัวข้อข้างล่างครับ
SESSION COOKIE the state lives on the server ┌─ server ─────────┐ ┌────────┐ Cookie: sid=7f3a9c │ session store ● │ │ browser│ ──────────────────────────────── │ │ └────────┘ │ 7f3a9c user 42 │ │ │ └──────────────────┘ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ JWT the state lives inside the token ┌─ API ────────────┐ ┌────────┐ Bearer eyJhbGciOi... │ [ ] signature │ │ browser│ ──────────────────────────────── │ [ ] exp, aud │ └────────┘ │ no lookup │ │ exp 10:15 │ └──────────────────┘ logout ends a session at once; a JWT lives until exp
Session cookie ทำงานอย่างไร
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
- ลายเซ็นไม่ได้ทำให้อ่านไม่ออก OWASP เขียนไว้ว่าเนื้อหาของ JWT แค่ encode เป็น base64 ไม่ได้เข้ารหัสโดยค่าเริ่มต้น ใครได้ token ไปก็อ่าน role หรืออีเมลข้างในได้ ไม่ควรใส่ข้อมูลที่ไม่อยากให้ใครเห็น
- header ถูกแก้ได้ RFC 8725 บันทึกการโจมตีที่เกิดขึ้นจริง เช่นเปลี่ยน
algเป็น"none"แล้ว library บางตัวยอมรับโดยไม่ตรวจลายเซ็น หรือเปลี่ยนRS256เป็นHS256จน library เอา RSA public key ไปใช้เป็น HMAC secret ข้อแนะนำคือ library ต้องให้ผู้เรียกกำหนดชุด algorithm ที่รับ และต้องไม่ใช้ algorithm อื่น - ต้องตรวจ
audถ้า issuer เดียวออก token ให้หลายแอป token ต้องมีaudและแต่ละแอปต้องตรวจว่าเป็นของตัวเอง ไม่อย่างนั้น token ของแอปหนึ่งเอาไปใช้กับอีกแอปได้
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 attribute ตัวไหนควรตั้ง
ถ้าใช้ cookie ไม่ว่าจะเก็บ session ID หรือ JWT ผมตั้งตามรายการนี้
-
Secureส่ง cookie เฉพาะช่องทางที่ปลอดภัย ซึ่งปกติคือ HTTP over TLS -
HttpOnlyไม่ให้ script อ่านค่า -
SameSite=LaxหรือStrictตั้งเอง อย่ารอค่าเริ่มต้นของ browser - ชื่อขึ้นต้นด้วย
__Host-ร่าง 6265bis กำหนดว่า cookie ชื่อนี้ต้องตั้งพร้อมSecure,Path=/และไม่มีDomaincookie จึงผูกกับ host เดียว subdomain อื่นมาเขียนทับไม่ได้ -
Max-Ageไม่ยาวเกินอายุ session หรือ token ถ้าตั้งทั้งExpiresและMax-AgeMax-Ageจะมีผลก่อน - ขนาด RFC 6265 กำหนดให้ browser รองรับอย่างน้อย 4096 byte ต่อ cookie และ cookie ถูกส่งไปกับทุก request JWT ที่มี claim เยอะจึงทำให้ทุก request ใหญ่ขึ้น
ควรเลือกแบบไหน
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 เลยครับ
แหล่งอ้างอิง
- RFC 7519: JSON Web Token (JWT)
- RFC 8725: JSON Web Token Best Current Practices
- RFC 6265: HTTP State Management Mechanism
- draft-ietf-httpbis-rfc6265bis-21: Cookies: HTTP State Management Mechanism (กันยายน 2025)
- OWASP Session Management Cheat Sheet
- OWASP JSON Web Token Cheat Sheet for Java (ฉบับพฤษภาคม 2025)
- MDN: Set-Cookie header
อ่านต่อ
บทความที่เกี่ยวข้อง
การพัฒนาซอฟต์แวร์
RPC กับ REST แตกต่างกันอย่างไร และควรเลือกใช้แบบไหน
RPC เรียก procedure บน server ด้วยชื่อและพารามิเตอร์ ส่วน REST ส่งคำขอไปที่ resource ผ่าน interface กลางของ HTTP เทียบทีละประเด็นพร้อมตัวอย่าง gRPC และ JSON-RPC อิงจาก Fielding, RFC 9110 และ spec ต้นทาง
เริ่มต้น10 นาที
- API
- RPC
- REST
- gRPC
- JSON-RPC
- HTTP
การพัฒนาซอฟต์แวร์
WebSocket SSE และ Polling ต่างกันอย่างไร ควรเลือกแบบไหน
Polling ส่ง request ใหม่ทุกรอบ SSE เปิด response ค้างให้ server ส่ง event ทางเดียว WebSocket upgrade แล้วส่ง frame ได้สองทาง พร้อมเรื่อง reconnect และ proxy
ปานกลาง9 นาที
- WebSocket
- Server-Sent Events
- SSE
- Long Polling
- HTTP
- Reverse Proxy
- nginx
การพัฒนาซอฟต์แวร์
OAuth 2.0 กับ OpenID Connect ต่างกันอย่างไร
OAuth 2.0 ให้แอปได้ access token ไปเรียก API แทนผู้ใช้ ส่วน OpenID Connect เพิ่ม ID token ที่บอกว่าใคร sign in ไล่ authorization code flow กับ PKCE ทีละ request
ปานกลาง7 นาที
- OAuth 2.0
- OpenID Connect
- PKCE
- Authentication
- Authorization
- Tokens