
ในโลกดิจิทัลที่ขับเคลื่อนด้วยการเชื่อมต่อและการเข้าถึงข้อมูล การรักษาความปลอดภัยและการยืนยันตัวตน (Authentication) รวมถึงการอนุญาตสิทธิ์ (Authorization) กลายเป็นหัวใจสำคัญที่ไม่อาจมองข้ามได้ครับ นักพัฒนาและผู้ดูแลระบบต่างต้องเผชิญกับความท้าทายในการสร้างระบบที่ทั้งปลอดภัย ใช้งานง่าย และรองรับการขยายตัวได้ บทความนี้ SiamLancard.com จะพาคุณดำดิ่งสู่โลกของสองเทคโนโลยีหลักที่เข้ามาปฏิวัติแนวคิดนี้ นั่นคือ OAuth 2.0 มาตรฐานการมอบสิทธิ์อันทรงพลัง และ JWT (JSON Web Tokens) กลไกการระบุตัวตนแบบไร้สถานะที่ได้รับความนิยมอย่างสูง เราจะมาทำความเข้าใจว่าทั้งสองสิ่งนี้คืออะไร ทำงานอย่างไร มีข้อดีข้อเสียอย่างไร และที่สำคัญที่สุดคือจะนำมาใช้งานร่วมกันเพื่อสร้างระบบ Authentication และ Authorization ที่สมบูรณ์แบบได้อย่างไร พร้อมตัวอย่างโค้ดและแนวปฏิบัติที่ดีที่สุด เพื่อให้คุณมีคู่มือครบถ้วนสำหรับการนำไปประยุกต์ใช้ในโปรเจกต์ของคุณครับ
- บทนำ
- เจาะลึก OAuth 2.0: มาตรฐานการมอบสิทธิ์ที่ทรงพลัง
- ทำความเข้าใจ JWT (JSON Web Tokens): กุญแจสู่การระบุตัวตนแบบไร้สถานะ
- ผสานพลัง: OAuth 2.0 และ JWT ทำงานร่วมกันอย่างไร
- การนำไปใช้งานจริง: แนวปฏิบัติและข้อควรพิจารณา
- ตารางเปรียบเทียบ: JWT Access Token กับ Session-based Authentication
- ตัวอย่างโค้ด: การสร้างและตรวจสอบ JWT
- คำถามที่พบบ่อย (FAQ)
- สรุปและ Call-to-Action
เจาะลึก OAuth 2.0: มาตรฐานการมอบสิทธิ์ที่ทรงพลัง
OAuth 2.0 คืออะไร?
OAuth 2.0 ย่อมาจาก “Open Authorization” เวอร์ชัน 2.0 ครับ มันคือ เฟรมเวิร์กมาตรฐานสำหรับการมอบสิทธิ์ (Authorization Framework) ที่ช่วยให้แอปพลิเคชันหนึ่ง (Client) สามารถเข้าถึงทรัพยากรที่ได้รับการป้องกันบนเซิร์ฟเวอร์อื่น (Resource Server) ในนามของผู้ใช้ (Resource Owner) โดยที่ผู้ใช้ไม่จำเป็นต้องเปิดเผยข้อมูลประจำตัว (Username/Password) ของตนเองให้แก่แอปพลิเคชันนั้นโดยตรงครับ
ลองนึกภาพสถานการณ์ที่คุณต้องการให้แอปพลิเคชันรูปภาพที่คุณชื่นชอบโพสต์รูปภาพลงบน Facebook ของคุณโดยอัตโนมัติ การที่คุณต้องให้ชื่อผู้ใช้และรหัสผ่าน Facebook แก่แอปพลิเคชันรูปภาพนั้นเป็นสิ่งที่ไม่ปลอดภัยอย่างยิ่ง เพราะแอปพลิเคชันนั้นจะสามารถทำอะไรก็ได้บน Facebook ของคุณ ไม่ใช่แค่โพสต์รูปภาพเท่านั้น แต่ยังรวมถึงการอ่านข้อความส่วนตัว การลบข้อมูล หรือแม้แต่เปลี่ยนรหัสผ่านของคุณได้เลยครับ
OAuth 2.0 เข้ามาแก้ปัญหานี้โดยทำหน้าที่เป็น “ตัวกลาง” ในการขอและมอบสิทธิ์ แทนที่จะให้ชื่อผู้ใช้และรหัสผ่าน คุณจะมอบ “ใบอนุญาต” ที่จำกัดขอบเขตการเข้าถึง (Limited Scope) ให้กับแอปพลิเคชันรูปภาพนั้น ซึ่งใบอนุญาตนี้จะอนุญาตให้แอปพลิเคชันสามารถโพสต์รูปภาพได้เท่านั้น ไม่สามารถทำอย่างอื่นได้ครับ
ทำไมต้อง OAuth 2.0?
ก่อนหน้าที่จะมี OAuth ผู้พัฒนาแอปพลิเคชันมักจะใช้วิธีที่เรียกว่า “Delegated Authorization” โดยการให้ผู้ใช้ป้อนชื่อผู้ใช้และรหัสผ่านของบริการภายนอก (เช่น Facebook, Google) ลงในแอปพลิเคชันของตนโดยตรง ซึ่งวิธีนี้มีข้อเสียร้ายแรงหลายประการครับ:
- ความเสี่ยงด้านความปลอดภัยสูง: แอปพลิเคชันที่ได้รับข้อมูลประจำตัวของผู้ใช้สามารถนำไปใช้ในทางที่ผิดได้ ผู้ใช้ไม่มีทางรู้ว่าแอปพลิเคชันนั้นเก็บข้อมูลรหัสผ่านไว้อย่างปลอดภัยหรือไม่ หรืออาจนำไปใช้เพื่อวัตถุประสงค์อื่นนอกเหนือจากที่ตกลงกันไว้ครับ
- การเข้าถึงแบบไม่จำกัด (All-or-Nothing Access): โดยปกติแล้ว การให้ชื่อผู้ใช้และรหัสผ่านจะหมายถึงการอนุญาตให้เข้าถึงทรัพยากรทั้งหมดของผู้ใช้บนบริการนั้นๆ ไม่มีวิธีจำกัดสิทธิ์ให้เข้าถึงแค่บางส่วนได้ครับ
- การเพิกถอนสิทธิ์ที่ยุ่งยาก: หากผู้ใช้ต้องการเพิกถอนสิทธิ์ที่ให้ไป จะต้องเปลี่ยนรหัสผ่านของบริการนั้น ซึ่งจะส่งผลกระทบต่อทุกแอปพลิเคชันที่เคยได้รับสิทธิ์นั้นไปครับ
- การขาดมาตรฐาน: แต่ละบริการอาจมีวิธีการขอสิทธิ์ที่แตกต่างกัน ทำให้การพัฒนาแอปพลิเคชันที่ต้องเชื่อมต่อหลายบริการเป็นเรื่องซับซ้อนครับ
OAuth 2.0 เข้ามาแก้ไขปัญหาเหล่านี้โดย:
- เพิ่มความปลอดภัย: ผู้ใช้ไม่ต้องเปิดเผยข้อมูลประจำตัวให้กับ Client โดยตรงครับ Client จะได้รับเพียง Access Token ซึ่งเป็นเหมือนกุญแจที่ใช้เข้าถึงทรัพยากรตามขอบเขตที่กำหนดไว้เท่านั้น
- ควบคุมการเข้าถึงได้ละเอียด (Granular Control): ผู้ใช้สามารถกำหนดได้ว่า Client แต่ละรายจะเข้าถึงข้อมูลอะไรได้บ้าง ผ่านการกำหนด
scopesครับ เช่น อนุญาตให้ “อ่านโปรไฟล์” เท่านั้น หรือ “โพสต์รูปภาพ” เท่านั้น - การเพิกถอนสิทธิ์ที่ง่ายดาย: ผู้ใช้สามารถเพิกถอนสิทธิ์ของ Client ใดๆ ได้ตลอดเวลาโดยไม่กระทบกับ Client อื่นๆ หรือต้องเปลี่ยนรหัสผ่านครับ
- เป็นมาตรฐานสากล: ช่วยให้นักพัฒนาสามารถสร้างแอปพลิเคชันที่เชื่อมต่อกับบริการต่างๆ ได้ง่ายขึ้น เนื่องจากใช้โปรโตคอลการมอบสิทธิ์ที่เป็นมาตรฐานเดียวกันครับ
บทบาทสำคัญใน OAuth 2.0
ในการทำความเข้าใจ OAuth 2.0 สิ่งสำคัญคือการรู้จักกับบทบาทหลัก 4 บทบาทที่เกี่ยวข้องครับ:
-
Resource Owner (เจ้าของทรัพยากร):
คือผู้ใช้ปลายทาง (End-user) ที่เป็นเจ้าของข้อมูลหรือทรัพยากรที่ได้รับการป้องกันครับ เช่น ตัวคุณเองที่เป็นเจ้าของรูปภาพบน Facebook หรือข้อมูลติดต่อบน Google.
-
Client (ไคลเอนต์):
คือแอปพลิเคชันที่ต้องการเข้าถึงทรัพยากรของ Resource Owner ครับ เช่น แอปพลิเคชันรูปภาพที่คุณใช้ หรือแอปพลิเคชันมือถือที่ต้องการเข้าถึงปฏิทินของคุณ Client จะต้องได้รับการลงทะเบียนกับ Authorization Server ก่อนจึงจะสามารถใช้งาน OAuth ได้ครับ
-
Authorization Server (เซิร์ฟเวอร์ผู้ให้สิทธิ์):
เป็นเซิร์ฟเวอร์ที่ทำหน้าที่ยืนยันตัวตนของ Resource Owner และตรวจสอบว่า Resource Owner ยินยอมที่จะให้สิทธิ์แก่ Client หรือไม่ครับ หาก Resource Owner ยินยอม Authorization Server จะออก Access Token ให้แก่ Client ครับ โดยทั่วไปแล้ว Authorization Server มักจะเป็นส่วนหนึ่งของบริการที่ดูแล Resource Server นั้นๆ ครับ (เช่น Google, Facebook)
-
Resource Server (เซิร์ฟเวอร์ทรัพยากร):
คือเซิร์ฟเวอร์ที่โฮสต์ทรัพยากรที่ได้รับการป้องกันครับ เช่น Facebook Graph API สำหรับรูปภาพ หรือ Google Calendar API สำหรับปฏิทิน Resource Server จะรับ Access Token จาก Client และตรวจสอบกับ Authorization Server หรือตรวจสอบด้วยตัวเองว่า Access Token นั้นถูกต้องและมีสิทธิ์เข้าถึงทรัพยากรที่ร้องขอหรือไม่ครับ
ข้อสังเกต: Authorization Server และ Resource Server อาจเป็นเซิร์ฟเวอร์เดียวกัน หรือแยกกันก็ได้ครับ แต่ตามหลักการแล้ว บทบาทของทั้งสองมีความแตกต่างกันอย่างชัดเจน
ประเภทการให้สิทธิ์ (Grant Types) ของ OAuth 2.0
OAuth 2.0 มีวิธีการหรือ “Flows” หลายประเภทสำหรับการให้สิทธิ์ ซึ่งแต่ละประเภทก็เหมาะกับสถานการณ์การใช้งานที่แตกต่างกันไปครับ
-
Authorization Code Grant (ได้รับความนิยมและปลอดภัยที่สุด):
เป็น Flow ที่แนะนำและใช้กันมากที่สุดสำหรับแอปพลิเคชันบนเว็บ (Web Applications) ที่ทำงานบนเซิร์ฟเวอร์ เพราะมีความปลอดภัยสูงครับ
- ขั้นตอน:
- Client (แอปพลิเคชันของคุณ) ส่งคำขอให้ Resource Owner (ผู้ใช้) อนุญาตสิทธิ์ โดยจะนำผู้ใช้ไปยังหน้า Login/Consent ของ Authorization Server (เช่น Google Login page) ครับ
- Resource Owner ล็อกอินและให้ความยินยอมอนุญาตสิทธิ์แก่ Client ครับ
- Authorization Server จะนำผู้ใช้กลับมายัง Client พร้อมกับส่ง
Authorization Code(เป็นรหัสชั่วคราว) มาให้ครับ - Client (บนฝั่งเซิร์ฟเวอร์) นำ
Authorization Codeที่ได้ไปแลกเป็นAccess Token(และอาจมีRefresh Token) กับ Authorization Server โดยตรงครับ - Client ใช้
Access Tokenที่ได้ในการเข้าถึงทรัพยากรบน Resource Server ครับ
- ข้อดี:
Authorization Codeจะถูกส่งผ่านเบราว์เซอร์ ซึ่งผู้โจมตีอาจดักจับได้ แต่โค้ดนี้ไม่สามารถใช้ได้โดยตรง ต้องใช้client_secretเพื่อแลกเป็น Access Token ซึ่งclient_secretจะเก็บอยู่บนเซิร์ฟเวอร์ของ Client ทำให้ปลอดภัยกว่าครับ - เหมาะสำหรับ: Web applications (server-side), mobile apps (with PKCE extension) ครับ
- ขั้นตอน:
-
Client Credentials Grant:
ใช้สำหรับ Machine-to-Machine Communication หรือแอปพลิเคชันที่ไม่มีผู้ใช้ปลายทางเกี่ยวข้องครับ Client จะได้รับสิทธิ์ในการเข้าถึงทรัพยากรของตัวเอง ไม่ใช่ทรัพยากรของผู้ใช้ครับ
- ขั้นตอน:
- Client ส่ง
client_idและclient_secretไปยัง Authorization Server ครับ - Authorization Server ตรวจสอบข้อมูลและออก
Access Tokenให้แก่ Client ครับ - Client ใช้
Access Tokenในการเข้าถึงทรัพยากรบน Resource Server ครับ
- Client ส่ง
- ข้อดี: ง่าย ไม่ต้องมีขั้นตอนผ่านผู้ใช้ครับ
- เหมาะสำหรับ: ระบบภายใน, microservices ที่ต้องสื่อสารกันเอง, CLI tools ครับ
- ขั้นตอน:
-
Implicit Grant (Deprecated / ไม่แนะนำให้ใช้แล้ว):
เคยใช้สำหรับแอปพลิเคชัน Single-Page Applications (SPAs) หรือ Mobile Apps ที่ไม่มี Back-end Server ครับ Access Token จะถูกส่งกลับไปยัง Client โดยตรงผ่าน URL Fragment ครับ
- ปัญหา: Access Token อาจถูกเปิดเผยผ่านเบราว์เซอร์ประวัติ (Browser History) หรือ Web Logs และไม่สามารถใช้ Refresh Token ได้ ทำให้ต้องขอ Token ใหม่บ่อยๆ ครับ
- ปัจจุบัน: ถูกแทนที่ด้วย Authorization Code Grant with PKCE (Proof Key for Code Exchange) สำหรับแอปพลิเคชันฝั่ง Client ครับ
-
Resource Owner Password Credentials Grant (ไม่แนะนำให้ใช้):
Client ขอชื่อผู้ใช้และรหัสผ่านจาก Resource Owner โดยตรงแล้วส่งต่อไปยัง Authorization Server เพื่อขอ Access Token ครับ
- ปัญหา: Client ต้องเข้าถึงข้อมูลประจำตัวของผู้ใช้ ซึ่งขัดกับวัตถุประสงค์หลักของ OAuth 2.0 ที่ต้องการหลีกเลี่ยงการเปิดเผยข้อมูลนี้ครับ
- เหมาะสำหรับ: ใช้ในสถานการณ์ที่เชื่อถือ Client ได้อย่างเต็มที่เท่านั้น เช่น แอปพลิเคชันที่สร้างโดยผู้ให้บริการ API เอง และไม่มีทางเลือกอื่นที่ดีกว่าครับ
-
Refresh Token:
ไม่ใช่ Grant Type หลัก แต่เป็น Token ประเภทหนึ่งที่ Authorization Server สามารถออกให้พร้อมกับ Access Token ครับ Refresh Token มีอายุการใช้งานยาวนานกว่า Access Token และใช้สำหรับขอ Access Token ใหม่เมื่อ Access Token เดิมหมดอายุ โดยไม่ต้องให้ผู้ใช้ล็อกอินใหม่ครับ
- ข้อควรระวัง: Refresh Token เป็น Token ที่สำคัญมาก ต้องเก็บรักษาอย่างปลอดภัยที่สุดครับ
OAuth 2.0 ไม่ใช่ Authentication แต่…
เป็นสิ่งสำคัญที่ต้องย้ำตรงนี้ครับว่า OAuth 2.0 เป็นเฟรมเวิร์กสำหรับการมอบสิทธิ์ (Authorization) ไม่ใช่การยืนยันตัวตน (Authentication) ครับ
หมายความว่า OAuth 2.0 ช่วยให้ Client ได้รับอนุญาตให้เข้าถึงทรัพยากรบางอย่างของ Resource Owner ได้ แต่ไม่ได้บอกว่า “ใคร” คือ Resource Owner นั้นครับ Authorization Server เพียงแค่รู้ว่าผู้ใช้ (Resource Owner) ได้ล็อกอินและยินยอมให้สิทธิ์แล้ว แต่ไม่ได้ส่งข้อมูลยืนยันตัวตนของผู้ใช้กลับมาให้ Client โดยตรงครับ
แล้วจะทำ Authentication ได้อย่างไร? คำตอบคือ OpenID Connect (OIDC) ครับ
OpenID Connect คือเลเยอร์ (Layer) ที่สร้างขึ้นบน OAuth 2.0 ที่เพิ่มฟังก์ชันการยืนยันตัวตนเข้ามาครับ เมื่อใช้ OIDC ร่วมกับ OAuth 2.0 Authorization Server จะออก ID Token (ซึ่งมักจะเป็น JWT) มาให้ Client ด้วยครับ ID Token นี้จะบรรจุข้อมูลยืนยันตัวตนของผู้ใช้ เช่น ชื่อ, อีเมล, รูปโปรไฟล์ และอื่นๆ ที่ถูกร้องขอและยินยอมโดยผู้ใช้ครับ Client สามารถใช้ ID Token นี้ในการยืนยันตัวตนของผู้ใช้ได้ครับ
ดังนั้น โดยสรุปคือ:
- OAuth 2.0: คุณเป็นเจ้าของทรัพยากรใช่ไหม? คุณยินยอมให้แอปนี้เข้าถึงข้อมูลรูปภาพของคุณได้ไหม? (Authorization)
- OpenID Connect: คุณคือใคร? ชื่ออะไร? อีเมลอะไร? (Authentication)
การเข้าใจความแตกต่างนี้เป็นพื้นฐานสำคัญในการออกแบบระบบที่ใช้ OAuth 2.0 และ JWT ได้อย่างถูกต้องและปลอดภัยครับ อ่านเพิ่มเติมเกี่ยวกับ OpenID Connect
ทำความเข้าใจ JWT (JSON Web Tokens): กุญแจสู่การระบุตัวตนแบบไร้สถานะ
JWT คืออะไร?
JWT ย่อมาจาก JSON Web Token ครับ มันคือมาตรฐาน (RFC 7519) ที่ใช้สำหรับสร้าง Token ที่ปลอดภัยและกะทัดรัด เพื่อส่งข้อมูลระหว่างสองฝ่ายในรูปแบบ JSON ที่สามารถตรวจสอบความถูกต้องได้ครับ JWT มักถูกนำมาใช้สำหรับการยืนยันตัวตน (Authentication) และการอนุญาตสิทธิ์ (Authorization) ในระบบที่ต้องการความ Scalability สูง และเป็นที่นิยมอย่างมากในสถาปัตยกรรมแบบ Microservices และ Single-Page Applications (SPAs) ครับ
คุณสมบัติเด่นของ JWT คือการเป็น “Self-contained” หรือ “มีข้อมูลในตัวเอง” ครับ หมายความว่าข้อมูลที่จำเป็นทั้งหมดเกี่ยวกับการยืนยันตัวตนของผู้ใช้ เช่น ID ผู้ใช้, สิทธิ์การเข้าถึง (roles/permissions), และวันหมดอายุของ Token จะถูกบรรจุอยู่ในตัว Token เอง และถูกเข้ารหัสพร้อมลงลายเซ็นดิจิทัล (Digitally Signed) ทำให้เซิร์ฟเวอร์สามารถตรวจสอบความถูกต้องของ Token ได้โดยไม่ต้องสอบถามฐานข้อมูลหรือเซิร์ฟเวอร์อื่นเพิ่มเติมครับ
โครงสร้างของ JWT
JWT ประกอบด้วยสามส่วนหลักๆ ที่คั่นด้วยเครื่องหมายจุด (.) ครับ:
Header.Payload.Signature
แต่ละส่วนจะถูกเข้ารหัสด้วย Base64Url-encoded ซึ่งทำให้สามารถส่งผ่าน URL ได้อย่างปลอดภัยครับ มาดูกันทีละส่วนครับ
-
Header (ส่วนหัว):
เป็นส่วนที่ระบุประเภทของ Token (
typ) ซึ่งมักจะเป็น “JWT” และอัลกอริทึมการเข้ารหัส (alg) ที่ใช้ในการสร้าง Signature ครับ เช่น HS256 (HMAC SHA-256) หรือ RS256 (RSA SHA-256) ครับ{ "alg": "HS256", "typ": "JWT" } -
Payload (ส่วนข้อมูล):
เป็นส่วนที่บรรจุ “Claims” หรือข้อมูลที่คุณต้องการส่งครับ Claims เป็นข้อความ JSON ที่ระบุเอนทิตี (โดยปกติคือผู้ใช้) และแอตทริบิวต์เพิ่มเติม รวมถึงวันหมดอายุของ Token ครับ
{ "sub": "1234567890", "name": "SiamLancard User", "admin": true, "iat": 1516239022, "exp": 1516242622 }ในตัวอย่างนี้
sub(subject) คือ ID ผู้ใช้,nameคือชื่อผู้ใช้,adminคือสถานะแอดมิน,iat(issued at) คือเวลาที่ Token ถูกสร้าง, และexp(expiration time) คือเวลาที่ Token หมดอายุครับ -
Signature (ส่วนลายเซ็น):
เป็นส่วนที่ใช้ในการตรวจสอบความถูกต้องของ Token ครับ Signature ถูกสร้างขึ้นโดยการนำ Base64Url-encoded ของ Header และ Payload มารวมกัน แล้วนำไปเข้ารหัสด้วยอัลกอริทึมที่ระบุใน Header (เช่น HS256) และ Secret Key (หรือ Private Key ในกรณีของ RS256) ครับ
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )Signature นี้มีความสำคัญอย่างยิ่ง เพราะหากมีการเปลี่ยนแปลงข้อมูลใน Header หรือ Payload แม้แต่น้อย Signature ที่คำนวณใหม่จะไม่ตรงกับ Signature เดิม ทำให้เซิร์ฟเวอร์สามารถตรวจจับการปลอมแปลง Token ได้ทันทีครับ
เมื่อทั้งสามส่วนถูกเข้ารหัสและรวมกัน จะได้ JWT ที่มีลักษณะดังนี้ครับ:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNpYW1MYW5jYXJkIFVzZXIiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.jM1M_0yA5m5Xw7h0M_zQ8Q2Q8Q2Q8Q2Q8Q2Q8Q2Q8Q2Q
ประเภทของ Claims ใน JWT
Claims คือข้อมูลที่บรรจุอยู่ใน Payload ของ JWT ครับ แบ่งออกเป็น 3 ประเภทหลักๆ ดังนี้ครับ:
-
Registered Claims (Claims ที่ถูกลงทะเบียน):
เป็น Claims ที่มีชื่อและวัตถุประสงค์ที่กำหนดไว้ในมาตรฐาน JWT (RFC 7519) เพื่อให้เกิดความสอดคล้องกันในการใช้งานครับ Claims เหล่านี้เป็นทางเลือก (optional) แต่แนะนำให้ใช้เพื่อประโยชน์ในการทำงานร่วมกันครับ
iss(Issuer): ผู้ออก Token (เช่นyourdomain.com)sub(Subject): เจ้าของ Token (เช่น User ID)aud(Audience): ผู้รับ Token (เช่น ชื่อแอปพลิเคชัน)exp(Expiration Time): เวลาที่ Token หมดอายุ (เป็น Unix timestamp)nbf(Not Before): เวลาที่ Token จะเริ่มใช้งานได้ (เป็น Unix timestamp)iat(Issued At): เวลาที่ Token ถูกสร้างขึ้น (เป็น Unix timestamp)jti(JWT ID): ID เฉพาะสำหรับ Token นี้ (ใช้ในการป้องกันการโจมตีแบบ Replay Attack)
-
Public Claims (Claims สาธารณะ):
เป็น Claims ที่กำหนดโดยนักพัฒนาเอง แต่ควรลงทะเบียนใน IANA JSON Web Token Registry หรือใช้ชื่อที่ป้องกันการชนกัน (Collision-Resistant Namespaces) เพื่อหลีกเลี่ยงความสับสนครับ เช่น URI ที่อ้างอิงถึงข้อมูลที่กำหนดไว้สาธารณะครับ
-
Private Claims (Claims ส่วนตัว):
เป็น Claims ที่กำหนดขึ้นมาเองโดยนักพัฒนาเพื่อใช้ในแอปพลิเคชันของตนเองครับ ไม่ได้มีการลงทะเบียนในมาตรฐานใดๆ และไม่มีการตรวจสอบการชนกันของชื่อครับ เช่น
"userId": "abc-123"หรือ"role": "admin"ครับ ควรใช้ชื่อที่ยาวและเฉพาะเจาะจงเพื่อหลีกเลี่ยงการชนกับ Registered Claims หรือ Public Claims ครับ
ข้อดีของ JWT
-
ไร้สถานะ (Statelessness) และ Scalability:
เนื่องจาก JWT มีข้อมูลที่จำเป็นทั้งหมดอยู่ในตัวมันเอง Resource Server ไม่จำเป็นต้องเก็บข้อมูล Session ไว้ในหน่วยความจำหรือฐานข้อมูล ทำให้สามารถขยายระบบได้ง่ายขึ้น (Horizontal Scaling) โดยไม่ต้องกังวลเรื่องการแบ่งปัน Session ระหว่าง Server ครับ
-
กะทัดรัด (Compact):
JWT มีขนาดเล็ก ทำให้สามารถส่งผ่าน HTTP Header ได้อย่างรวดเร็ว และลด Overhead ในการส่งข้อมูลครับ
-
ปลอดภัย (Secure):
Signature ของ JWT รับประกันว่าข้อมูลที่อยู่ใน Token ไม่ได้ถูกแก้ไขระหว่างทาง และผู้รับสามารถตรวจสอบแหล่งที่มาของ Token ได้ครับ
-
รองรับ Cross-Domain:
JWT สามารถใช้กับบริการหรือโดเมนที่แตกต่างกันได้ง่าย ทำให้เหมาะสำหรับ Microservices และ Single Sign-On (SSO) ครับ
-
รองรับการใช้งานบนมือถือ:
Client แอปพลิเคชันบนมือถือสามารถเก็บ JWT ไว้และส่งไปกับทุกๆ คำขอได้อย่างสะดวกครับ
ข้อควรระวังและข้อจำกัดของ JWT
-
ไม่มีกลไกการเพิกถอน (No Built-in Revocation):
เมื่อ JWT ถูกออกไปแล้ว ตราบใดที่ยังไม่หมดอายุ มันยังคงถูกต้องและใช้งานได้ครับ การเพิกถอน JWT ก่อนหมดอายุต้องใช้วิธีเพิ่มเติม เช่น การทำ Blacklist Token หรือ Short-lived Token ร่วมกับ Refresh Token ครับ
-
ขนาดของ Token:
แม้จะกะทัดรัด แต่หากใส่ Claims จำนวนมากเกินไป จะทำให้ Token มีขนาดใหญ่ขึ้น ส่งผลต่อประสิทธิภาพในการส่งข้อมูลได้ครับ
-
ความปลอดภัยของ Secret Key:
Secret Key ที่ใช้ในการสร้าง Signature เป็นสิ่งสำคัญที่สุด หาก Key นี้รั่วไหล ผู้โจมตีสามารถสร้าง JWT ปลอมได้ครับ
-
การจัดเก็บ Token บน Client-side:
การเก็บ JWT ไว้ใน Local Storage หรือ Session Storage ของเบราว์เซอร์มีความเสี่ยงต่อการโจมตีแบบ XSS (Cross-Site Scripting) ได้ครับ การใช้ HttpOnly cookies อาจเป็นทางเลือกที่ปลอดภัยกว่าสำหรับ Access Token ครับ
-
Sensitive Data ใน Payload:
ข้อมูลใน Payload ของ JWT ไม่ได้ถูกเข้ารหัส (Encrypted) แค่ถูก Base64Url-encoded ซึ่งใครก็สามารถถอดรหัสได้ครับ ดังนั้น ไม่ควรใส่ข้อมูลที่อ่อนไหวมากๆ ลงใน Payload ครับ หากต้องการส่งข้อมูลที่อ่อนไหว ต้องใช้ JWE (JSON Web Encryption) ซึ่งเป็นอีกมาตรฐานหนึ่งครับ
ผสานพลัง: OAuth 2.0 และ JWT ทำงานร่วมกันอย่างไร
บ่อยครั้งที่นักพัฒนาใหม่ๆ มักจะสับสนระหว่าง OAuth 2.0 และ JWT ว่าเป็นสิ่งเดียวกัน หรือใช้แทนกันได้ ซึ่งจริงๆ แล้วไม่ใช่ครับ ทั้งสองมีบทบาทที่แตกต่างกัน แต่สามารถทำงานร่วมกันได้อย่างมีประสิทธิภาพเพื่อสร้างระบบ Authentication และ Authorization ที่แข็งแกร่งและ Scalable ครับ
บทบาทที่แตกต่างแต่ส่งเสริมกัน
- OAuth 2.0: ทำหน้าที่เป็น ผู้จัดการการมอบสิทธิ์ (Authorization Manager) ครับ มันเป็นโปรโตคอลที่กำหนดขั้นตอนและกลไกในการ “ขอ” และ “ให้” สิทธิ์การเข้าถึงทรัพยากรครับ ผลลัพธ์หลักของ OAuth 2.0 คือการที่ Client ได้รับ Access Token ครับ
- JWT: ทำหน้าที่เป็น รูปแบบของ Token (Token Format) ครับ โดยทั่วไปแล้ว Access Token ที่ OAuth 2.0 ออกให้นั้น มักจะเป็น JWT นั่นเองครับ JWT จะเป็น “ภาชนะ” ที่บรรจุข้อมูลยืนยันตัวตนและสิทธิ์การเข้าถึงไว้ในตัวเอง ทำให้ Resource Server สามารถตรวจสอบ Token ได้อย่างรวดเร็วและเป็นอิสระครับ
กล่าวคือ OAuth 2.0 คือ “กระบวนการ” ในการได้มาซึ่งกุญแจ (Access Token) ส่วน JWT คือ “รูปแบบของกุญแจ” นั้นครับ
ขั้นตอนการทำงานโดยละเอียด
ลองพิจารณา Flow การทำงานร่วมกันระหว่าง OAuth 2.0 และ JWT โดยใช้ Authorization Code Grant Type ซึ่งเป็น Flow ที่ได้รับความนิยมและปลอดภัยที่สุดครับ
-
Client เริ่มต้นคำขอสิทธิ์:
แอปพลิเคชัน Client (เช่น เว็บไซต์ของคุณ) ต้องการเข้าถึงข้อมูลของผู้ใช้ (Resource Owner) บน Resource Server (เช่น Google Photos) ครับ Client จะนำ Resource Owner ไปยังหน้า Login/Consent ของ Authorization Server (Google Identity Platform) ครับ
URL อาจมีลักษณะดังนี้:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=YOUR_CLIENT_ID&scope=email%20profile&redirect_uri=YOUR_REDIRECT_URI&state=RANDOM_STRING -
Resource Owner ให้ความยินยอม:
Resource Owner ล็อกอินเข้าสู่ Authorization Server และจะเห็นหน้าจอขอความยินยอมว่า Client ต้องการเข้าถึงข้อมูลอะไรบ้าง (ตามที่ระบุใน
scopeเช่นemail,profile) หาก Resource Owner ยินยอม Authorization Server จะส่งAuthorization Codeกลับไปยัง Client ผ่านredirect_uriที่ระบุไว้ครับURL ที่ถูก Redirect กลับมา:
https://your-app.com/callback?code=AN_AUTHORIZATION_CODE&state=RANDOM_STRING -
Client แลกเปลี่ยน Authorization Code เป็น Access Token:
Client (บนฝั่งเซิร์ฟเวอร์) จะได้รับ
Authorization Codeครับ จากนั้น Client จะส่งAuthorization Codeนี้ พร้อมกับclient_idและclient_secretไปยัง Authorization Server เพื่อขอแลกเปลี่ยนเป็นAccess TokenครับAuthorization Server จะตรวจสอบความถูกต้องของ
Authorization Codeและclient_id/secretหากถูกต้อง จะออกAccess Token(ซึ่งเป็น JWT) และRefresh Token(ถ้ามี) กลับมาให้ Client ครับตัวอย่าง Response จาก Authorization Server:
{ "access_token": "eyJhbGciOiJIUzI1NiI...", // นี่คือ JWT! "token_type": "Bearer", "expires_in": 3600, // Access Token มีอายุ 1 ชั่วโมง "refresh_token": "some_long_refresh_token_string", "scope": "email profile" } -
Client ใช้ Access Token เพื่อเข้าถึง Resource Server:
เมื่อ Client ได้รับ
Access Token(ซึ่งเป็น JWT) แล้ว Client จะสามารถใช้ Token นี้ในการส่งคำขอไปยัง Resource Server เพื่อเข้าถึงทรัพยากรที่ได้รับอนุญาตได้ครับ Client จะส่ง Access Token ไปใน HTTP Header ในรูปแบบAuthorization: Bearer <ACCESS_TOKEN>ครับตัวอย่าง HTTP Request:
GET /api/user/profile HTTP/1.1 Host: api.google.com Authorization: Bearer eyJhbGciOiJIUzI1NiI... -
Resource Server ตรวจสอบ Access Token:
เมื่อ Resource Server ได้รับคำขอพร้อม Access Token (JWT) Resource Server จะทำการตรวจสอบความถูกต้องของ JWT ครับ
- ตรวจสอบ Signature ของ JWT ด้วย Secret Key หรือ Public Key ที่รู้จัก
- ตรวจสอบว่า Token หมดอายุแล้วหรือไม่ (
expclaim) - ตรวจสอบว่า Token ถูกออกโดย Issuer ที่ถูกต้องหรือไม่ (
issclaim) - ตรวจสอบว่า Token ถูกสร้างสำหรับ Audience นี้หรือไม่ (
audclaim) - ตรวจสอบ Claims อื่นๆ ที่จำเป็น เช่น
scopeหรือrolesเพื่อยืนยันสิทธิ์ในการเข้าถึงทรัพยากรที่ร้องขอ
หาก JWT ถูกต้องและมีสิทธิ์ Resource Server จะประมวลผลคำขอและส่งข้อมูลกลับไปยัง Client ครับ
-
การใช้ Refresh Token (เมื่อ Access Token หมดอายุ):
เมื่อ
Access Tokenหมดอายุ Client จะไม่สามารถเข้าถึง Resource Server ได้อีกต่อไปครับ แทนที่จะให้ผู้ใช้ล็อกอินใหม่ Client สามารถใช้Refresh Tokenที่ได้รับมาในขั้นตอนที่ 3 เพื่อขอAccess Tokenใหม่จาก Authorization Server ได้โดยตรงครับClient ส่ง
refresh_tokenพร้อมclient_idและclient_secretไปยัง Authorization Server ครับ Authorization Server ตรวจสอบและออกAccess Tokenใหม่ให้ครับ
ตัวอย่างสถานการณ์การใช้งานจริง
สมมติว่าคุณกำลังพัฒนาแอปพลิเคชันจัดการโปรเจกต์ (Client) และต้องการให้ผู้ใช้สามารถเชื่อมต่อกับบริการเก็บไฟล์ (Resource Server) เช่น Google Drive เพื่อแนบไฟล์เข้ากับโปรเจกต์ได้ครับ
- ผู้ใช้คลิกปุ่ม “เชื่อมต่อ Google Drive” ในแอปพลิเคชันจัดการโปรเจกต์ของคุณ
- แอปพลิเคชันของคุณนำผู้ใช้ไปยังหน้า Login/Consent ของ Google (Authorization Server)
- ผู้ใช้ล็อกอินเข้า Google และอนุญาตให้แอปพลิเคชันของคุณ “ดูและจัดการไฟล์ใน Google Drive ของฉัน” (
scope: https://www.googleapis.com/auth/drive) - Google ส่ง
Authorization Codeกลับมายัง Back-end ของแอปพลิเคชันคุณ - Back-end ของแอปพลิเคชันคุณใช้
Authorization Codeพร้อมclient_idและclient_secretแลกเป็นAccess Token(JWT) และRefresh Tokenกับ Google - Back-end ของแอปพลิเคชันคุณเก็บ
Refresh Tokenไว้ในฐานข้อมูลอย่างปลอดภัย และส่งAccess Token(JWT) กลับไปยัง Front-end ของแอปพลิเคชันคุณ - เมื่อผู้ใช้ต้องการแนบไฟล์ Front-end จะส่ง
Access Token(JWT) ไปยัง Back-end ซึ่ง Back-end จะใช้Access Tokenนั้นในการเรียก Google Drive API (Resource Server) เพื่อแสดงรายการไฟล์ให้ผู้ใช้เลือกครับ - Google Drive API ตรวจสอบ JWT (Access Token) ว่าถูกต้องและมีสิทธิ์ในการเข้าถึงไฟล์ของผู้ใช้หรือไม่ หากถูกต้อง ก็จะส่งรายการไฟล์กลับมา
ในสถานการณ์นี้ JWT คือกุญแจที่แอปพลิเคชันของคุณใช้เพื่อพิสูจน์สิทธิ์ในการเข้าถึง Google Drive ในนามของผู้ใช้ โดยที่ผู้ใช้ไม่ต้องบอกรหัสผ่าน Google ของตนเองให้แอปพลิเคชันคุณเลยครับ
การนำไปใช้งานจริง: แนวปฏิบัติและข้อควรพิจารณา
การนำ OAuth 2.0 และ JWT มาใช้งานจริงนั้น จำเป็นต้องคำนึงถึงแนวทางปฏิบัติที่ดีที่สุด (Best Practices) เพื่อให้ระบบมีความปลอดภัยและมีประสิทธิภาพสูงสุดครับ
ความปลอดภัยของ JWT
-
ใช้ HTTPS/SSL เสมอ:
JWTs ควรถูกส่งผ่านช่องทางที่เข้ารหัสเสมอ เพื่อป้องกันการดักจับ (Man-in-the-Middle Attack) ครับ
-
ปกป้อง Secret Key / Private Key:
Key ที่ใช้ในการ Sign JWT ต้องเก็บเป็นความลับสูงสุดครับ การรั่วไหลของ Key นี้จะทำให้ผู้โจมตีสามารถสร้าง JWT ปลอมขึ้นมาได้ครับ หากใช้ RS256 (Asymmetric Signature) Private Key ต้องเป็นความลับ และ Public Key ใช้สำหรับตรวจสอบครับ
-
กำหนดวันหมดอายุ (Expiration Time) ให้ Access Token สั้นๆ:
Access Token ควรมีอายุสั้นๆ (เช่น 15 นาทีถึง 1 ชั่วโมง) เพื่อลดความเสี่ยงหาก Token ถูกขโมยไปครับ หากหมดอายุแล้ว Client สามารถใช้ Refresh Token เพื่อขอ Access Token ใหม่ได้ครับ
-
อย่าเก็บ Sensitive Data ใน Payload:
จำไว้ว่า Payload ของ JWT ไม่ได้ถูกเข้ารหัส เพียงแค่ Base64Url-encoded ซึ่งถอดรหัสได้ง่าย ไม่ควรใส่ข้อมูลส่วนตัวที่อ่อนไหว เช่น รหัสผ่าน, ข้อมูลบัตรเครดิต ลงใน Payload ครับ หากจำเป็นจริงๆ ควรใช้ JWE (JSON Web Encryption) แทนครับ
-
ใช้ Claims ที่จำเป็นเท่านั้น:
ใส่ข้อมูลใน Payload เท่าที่จำเป็นจริงๆ เพื่อให้ Token มีขนาดเล็กและลดโอกาสในการเปิดเผยข้อมูลที่ไม่จำเป็นครับ
-
ตรวจสอบ Claims เสมอ:
เมื่อ Resource Server ได้รับ JWT ต้องตรวจสอบ Claims ต่างๆ เสมอครับ ได้แก่:
Signature: สำคัญที่สุด ตรวจสอบว่า Token ไม่ถูกแก้ไขครับexp(Expiration Time): ตรวจสอบว่า Token ยังไม่หมดอายุครับnbf(Not Before): ตรวจสอบว่า Token เริ่มใช้งานได้แล้วครับiss(Issuer): ตรวจสอบว่า Token ถูกออกโดย Authorization Server ที่ถูกต้องครับaud(Audience): ตรวจสอบว่า Token ถูกสร้างขึ้นมาสำหรับ Client นี้ครับjti(JWT ID): ใช้สำหรับป้องกัน Replay Attack (หากมีการนำ JWT เดิมมาใช้ซ้ำ) ครับ
การจัดการ Refresh Token อย่างปลอดภัย
Refresh Token เป็นกุญแจสำคัญในการรักษาการเชื่อมต่อของผู้ใช้โดยไม่ต้องให้ล็อกอินใหม่ แต่ก็เป็นจุดอ่อนที่ต้องระวังเป็นพิเศษครับ
-
เก็บ Refresh Token ไว้บนเซิร์ฟเวอร์เท่านั้น:
Refresh Token ไม่ควรถูกส่งไปยัง Front-end ของ Client ครับ ควรเก็บไว้ในฐานข้อมูลที่ปลอดภัยบน Back-end ของคุณ และส่งไปพร้อมกับ
client_secretเมื่อขอ Access Token ใหม่ครับ -
ใช้ HttpOnly Cookies สำหรับ Refresh Token:
หากจำเป็นต้องส่ง Refresh Token ไปยัง Client (เช่น สำหรับแอปพลิเคชัน SPA ที่ไม่มี Back-end ของตัวเอง) ควรเก็บไว้ใน HttpOnly Secure Cookie ครับ ซึ่งจะป้องกันไม่ให้ JavaScript บนหน้าเว็บเข้าถึง Token ได้ ลดความเสี่ยงจาก XSS ครับ
-
กำหนดอายุของ Refresh Token:
แม้จะยาวกว่า Access Token แต่ Refresh Token ก็ควรมีวันหมดอายุเช่นกันครับ (เช่น 7 วัน, 30 วัน หรือ 1 ปี) เพื่อจำกัดช่วงเวลาที่ผู้โจมตีสามารถใช้มันได้หากถูกขโมยไปครับ
-
Implement Refresh Token Rotation:
ทุกครั้งที่ Client ใช้ Refresh Token เพื่อขอ Access Token ใหม่ Authorization Server ควรออก Refresh Token ตัวใหม่ให้พร้อมกัน และเพิกถอน Refresh Token ตัวเก่าทันทีครับ วิธีนี้ช่วยลดความเสี่ยงหาก Refresh Token ถูกขโมยไป ผู้โจมตีจะมี Refresh Token ที่ใช้ได้เพียงครั้งเดียวเท่านั้นครับ
การเพิกถอน Token (Revocation)
เนื่องจาก JWT ไม่มีกลไกการเพิกถอนในตัว การจัดการเรื่องนี้จึงต้องใช้เทคนิคเพิ่มเติมครับ
-
สำหรับ Access Token (JWT):
เนื่องจากมีอายุสั้น การเพิกถอนโดยตรงอาจไม่จำเป็นเท่าไหร่ครับ หาก Token หมดอายุ ก็จะใช้ไม่ได้เองครับ แต่ถ้าต้องการเพิกถอนทันที (เช่น ผู้ใช้ออกจากระบบ หรือเปลี่ยนรหัสผ่าน) สามารถทำได้โดย:
- Blacklisting: สร้างรายการ (Blacklist) ของ JWT ที่ถูกเพิกถอนในฐานข้อมูล หรือ Redis Cache ครับ เมื่อ Resource Server ได้รับ JWT ใดๆ ก็จะต้องตรวจสอบกับ Blacklist ก่อนที่จะทำการตรวจสอบ Signature และ Claims อื่นๆ ครับ
- Session Management: ในบางกรณี (เช่น กรณีต้องการให้ผู้ใช้ออกจากระบบจากทุกอุปกรณ์พร้อมกัน) อาจใช้การจัดการ Session ID ร่วมกับ JWT โดยที่ JWT มี Session ID เป็น Claim และเมื่อผู้ใช้ออกจากระบบ ก็จะเพิกถอน Session ID นั้นครับ
-
สำหรับ Refresh Token:
Refresh Token มีอายุยาวนานกว่า จึงจำเป็นต้องมีกลไกการเพิกถอนที่ชัดเจนครับ:
- Revocation List: เก็บรายการ Refresh Token ที่ถูกเพิกถอนในฐานข้อมูลครับ เมื่อผู้ใช้ออกจากระบบ Authorization Server จะเพิ่ม Refresh Token ของผู้ใช้นั้นลงในรายการเพิกถอนครับ
- Refresh Token Rotation: ตามที่กล่าวไปข้างต้น การออก Refresh Token ใหม่ทุกครั้งที่ใช้ และเพิกถอนตัวเก่า จะช่วยจำกัดความเสี่ยงได้ดีครับ
การใช้ Scopes ใน OAuth 2.0
Scopes เป็นส่วนสำคัญของ OAuth 2.0 ที่ช่วยให้ Resource Owner สามารถควบคุมได้ว่า Client จะเข้าถึงข้อมูลอะไรได้บ้างครับ
-
กำหนด Scopes ให้ชัดเจนและละเอียด:
ควรมี Scopes ที่หลากหลายและมีความหมายที่ชัดเจนครับ เช่น
read:profile,write:posts,delete:filesแทนที่จะเป็นfull_accessครับ -
ขอ Scopes เท่าที่จำเป็น:
Client ควรขอ Scopes เท่าที่จำเป็นต่อการทำงานของแอปพลิเคชันเท่านั้นครับ การขอ Scopes มากเกินไปอาจทำให้ผู้ใช้ไม่ไว้วางใจและไม่ยินยอมให้สิทธิ์ครับ
-
ตรวจสอบ Scopes บน Resource Server:
Resource Server ต้องตรวจสอบ
scopeClaim ใน Access Token (JWT) ทุกครั้งที่ได้รับคำขอ เพื่อให้แน่ใจว่า Client มีสิทธิ์เพียงพอที่จะดำเนินการตามคำขอนั้นๆ ครับ
OpenID Connect (OIDC) เพื่อการยืนยันตัวตน
หากคุณต้องการยืนยันตัวตนของผู้ใช้ด้วย OAuth 2.0 คุณควรใช้ OpenID Connect ครับ
-
ใช้ ID Token สำหรับ Authentication:
OIDC จะออก
ID Token(ซึ่งเป็น JWT) ที่มีข้อมูลยืนยันตัวตนของผู้ใช้ (เช่นsub,name,email) ครับ Client ควรตรวจสอบ Signature และ Claims ของ ID Token เพื่อยืนยันตัวตนของผู้ใช้ครับ -
แยก ID Token และ Access Token:
ID Token ใช้สำหรับ Authentication (ใครคือผู้ใช้) ส่วน Access Token ใช้สำหรับ Authorization (ผู้ใช้มีสิทธิ์ทำอะไรบ้าง) ครับ
การปฏิบัติตามแนวทางเหล่านี้จะช่วยให้คุณสร้างระบบที่ใช้ OAuth 2.0 และ JWT ได้อย่างปลอดภัย มีประสิทธิภาพ และ Scalable ครับ เรียนรู้เพิ่มเติมเกี่ยวกับแนวทางปฏิบัติเพื่อความปลอดภัยของ JWT
ตารางเปรียบเทียบ: JWT Access Token กับ Session-based Authentication
เพื่อความเข้าใจที่ชัดเจนยิ่งขึ้น ลองมาดูตารางเปรียบเทียบระหว่างการใช้ JWT Access Token กับ Session-based Authentication ที่เป็นวิธีการแบบดั้งเดิมกันครับ
| คุณสมบัติ | JWT Access Token (Stateless) | Session-based Authentication (Stateful) |
|---|---|---|
| แนวคิดหลัก | Token มีข้อมูลในตัวเอง (Self-contained), ไร้สถานะ (Stateless) | เซิร์ฟเวอร์เก็บสถานะของผู้ใช้ (Session State) |
| การจัดการสถานะ | ไม่มีสถานะที่เซิร์ฟเวอร์ต้องเก็บ (Server does not store session state) | เซิร์ฟเวอร์ต้องเก็บ Session ID และข้อมูล Session ในหน่วยความจำ/ฐานข้อมูล |
| Scalability | สูงมาก (Horizontal Scaling ง่าย), เหมาะสำหรับ Microservices และ Load Balancing | ทำ Scalability ยากกว่า (ต้องใช้ Sticky Sessions หรือ Distributed Sessions) |
| Token Format | JSON Web Token (JWT) | Random string (Session ID) |
| การตรวจสอบ Token | ตรวจสอบ Signature และ Claims ภายใน Token ได้โดยตรงบน Resource Server | ต้องสอบถามจาก Session Store (ฐานข้อมูล/Cache) ทุกครั้ง |
| Cross-Domain / SSO | ทำได้ง่ายกว่า (Token สามารถส่งข้ามโดเมนได้) | ทำได้ยากกว่า (ต้องจัดการ Cookie Domain หรือใช้ Proxy) |
| การเพิกถอน Token | ไม่มีกลไกในตัว ต้องใช้ Blacklisting หรือ Short-lived Token + Refresh Token | ง่าย (ลบ Session ID ออกจาก Session Store) |
| การจัดเก็บ Client-side | Local Storage, Session Storage, หรือ HttpOnly Cookie (ต้องระวัง XSS) | HttpOnly Cookie (ปลอดภัยจาก XSS มากกว่า) |
| ขนาดของ Token | ค่อนข้างเล็ก (แต่ใหญ่ขึ้นได้ถ้ามี Claims เยอะ) | เล็กมาก (เป็นแค่ Session ID) |
| ความปลอดภัยของข้อมูล | Payload ไม่ได้เข้ารหัส (Base64Url-encoded), ไม่ควรเก็บข้อมูลอ่อนไหว | ข้อมูล Session เก็บอยู่บนเซิร์ฟเวอร์ ปลอดภัยกว่า |
| เหมาะสำหรับ | API-driven apps, SPAs, Mobile apps, Microservices | Traditional server-side rendered web apps |
จากตารางจะเห็นว่า JWT และ Session-based Authentication มีจุดแข็งและจุดอ่อนที่แตกต่างกันครับ การเลือกใช้ขึ้นอยู่กับสถาปัตยกรรมของแอปพลิเคชัน ข้อกำหนดด้าน Scalability และความต้องการด้านความปลอดภัยครับ ในปัจจุบัน JWT มักเป็นทางเลือกที่ได้รับความนิยมสำหรับแอปพลิเคชันยุคใหม่ที่เน้น API-first และการขยายตัวแบบไร้สถานะครับ
ตัวอย่างโค้ด: การสร้างและตรวจสอบ JWT
เพื่อความเข้าใจที่ชัดเจนยิ่งขึ้น ลองมาดูตัวอย่างโค้ดง่ายๆ ในการสร้างและตรวจสอบ JWT โดยใช้ Node.js ร่วมกับไลบรารี jsonwebtoken ซึ่งเป็นไลบรารีที่ได้รับความนิยมกันครับ
ตัวอย่าง JWT ที่ถอดรหัสแล้ว
ก่อนอื่น มาดูตัวอย่าง JWT ที่เราจะใช้เป็นฐานกันก่อนครับ สมมติว่านี่คือ Payload ของ JWT ที่เราต้องการจะสร้าง:
{
"userId": "user-123",
"email": "[email protected]",
"role": "member",
"iat": 1678886400, // Issued At: 2023-03-15 00:00:00 GMT
"exp": 1678890000 // Expiration Time: 2023-03-15 01:00:00 GMT
}
เมื่อเข้ารหัสแล้ว จะได้ JWT ที่มีลักษณะดังที่ได้กล่าวไปแล้วครับ
ตัวอย่างการสร้างและตรวจสอบ JWT ด้วย Node.js
ก่อนอื่น คุณต้องติดตั้งไลบรารี jsonwebtoken ครับ
npm install jsonwebtoken
จากนั้น คุณสามารถใช้โค้ดต่อไปนี้:
// server.js หรือ authService.js
const jwt = require('jsonwebtoken');
// ----------------------------------------------------
// ส่วนที่ 1: การกำหนด Secret Key และ Options
// ----------------------------------------------------
const JWT_SECRET = 'your_super_secret_key_that_should_be_long_and_complex'; // ต้องเก็บเป็นความลับสูงสุด!
const JWT_EXPIRES_IN = '1h'; // Access Token มีอายุ 1 ชั่วโมง
// ----------------------------------------------------
// ส่วนที่ 2: ฟังก์ชันสำหรับสร้าง JWT
// ----------------------------------------------------
function generateAccessToken(userPayload) {
// สร้าง Payload สำหรับ JWT
const payload = {
userId: userPayload.id,
email: userPayload.email,
role: userPayload.role
};
// ลงลายเซ็น (Sign) JWT ด้วย Secret Key และกำหนดวันหมดอายุ
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
console.log('Generated Access Token:', token);
return token;
}
// ----------------------------------------------------
// ส่วนที่ 3: ฟังก์ชันสำหรับตรวจสอบ JWT
// ----------------------------------------------------
function verifyAccessToken(token) {
try {
// ตรวจสอบ JWT ด้วย Secret Key
const decoded = jwt.verify(token, JWT_SECRET);
console.log('Decoded Token:', decoded);
console.log('Token is valid. User ID:', decoded.userId);
return decoded;
} catch (error) {
if (error.name === 'TokenExpiredError') {
console.error('Token expired:', error.message);
return { error: 'TokenExpiredError', message: 'Access Token หมดอายุแล้วครับ' };
} else if (error.name === 'JsonWebTokenError') {
console.error('Invalid Token:', error.message);
return { error: 'InvalidTokenError', message: 'Access Token ไม่ถูกต้องครับ' };
} else {
console.error('Unknown JWT error:', error.message);
return { error: 'UnknownError', message: 'เกิดข้อผิดพลาดในการตรวจสอบ Access Token ครับ' };
}
}
}
// ----------------------------------------------------
// ส่วนที่ 4: ตัวอย่างการใช้งานจริง
// ----------------------------------------------------
// 1. จำลองข้อมูลผู้ใช้ที่ล็อกอินสำเร็จ
const mockUser = {
id: 'user-456',
email: '[email protected]',
role: 'admin'
};
// 2. สร้าง Access Token สำหรับผู้ใช้
const accessToken = generateAccessToken(mockUser);
// 3. จำลองการส่ง Access Token ไปยัง Resource Server
console.log('\n--- จำลองการตรวจสอบ Token บน Resource Server ---');
// 4. ตรวจสอบ Access Token ที่ได้รับ
const verificationResult = verifyAccessToken(accessToken);
// 5. ตัวอย่างการตรวจสอบ Token ที่ไม่ถูกต้อง (เช่น เปลี่ยนแปลง Signature)
console.log('\n--- จำลอง Token ที่ไม่ถูกต้อง (Signature ถูกเปลี่ยน) ---');
const tamperedToken = accessToken.substring(0, accessToken.length - 5) + 'xxxxx'; // เปลี่ยน Signature ให้ผิด
const tamperedVerificationResult = verifyAccessToken(tamperedToken);
// 6. ตัวอย่าง Token ที่หมดอายุ (สำหรับการทดสอบ คุณอาจจะตั้ง expiresIn เป็น '1s' แล้วรอ)
// หากต้องการทดสอบ Token หมดอายุ:
// const shortLivedToken = jwt.sign({ userId: 'test' }, JWT_SECRET, { expiresIn: '1s' });
// console.log('\nGenerated short-lived token:', shortLivedToken);
// setTimeout(() => {
// console.log('--- ตรวจสอบ Token ที่หมดอายุ ---');
// verifyAccessToken(shortLivedToken);
// }, 2000); // รอ 2 วินาทีเพื่อให้ Token หมดอายุ
โค้ดข้างต้นนี้แสดงให้เห็นถึง:
- การสร้าง JWT โดยใช้
jwt.sign()โดยมี Payload, Secret Key และ Options (เช่นexpiresIn) ครับ - การตรวจสอบ JWT โดยใช้
jwt.verify()ซึ่งจะทำการตรวจสอบ Signature,expและ Claims อื่นๆ โดยอัตโนมัติ และจะโยน Error ออกมาหาก Token ไม่ถูกต้องหรือหมดอายุครับ - การจัดการกับ Error ที่อาจเกิดขึ้น เช่น
TokenExpiredErrorหรือJsonWebTokenErrorครับ
โค้ดนี้เป็นเพียงตัวอย่างพื้นฐานสำหรับการสร้างและตรวจสอบ JWT ครับ ในระบบจริง คุณจะต้องบูรณาการมันเข้ากับระบบ OAuth 2.0 ของคุณ โดย Authorization Server จะเป็นผู้สร้าง Access Token (JWT) และ Resource Server จะเป็นผู้ตรวจสอบ Access Token นั้นครับ
คำถามที่พบบ่อย (FAQ)
Q1: OAuth 2.0 กับ JWT ต่างกันอย่างไรครับ?
A1: OAuth 2.0 คือ เฟรมเวิร์กการมอบสิทธิ์ (Authorization Framework) ที่กำหนดกระบวนการในการให้แอปพลิเคชัน (Client) เข้าถึงทรัพยากรของผู้ใช้ (Resource Owner) บนเซิร์ฟเวอร์อื่นได้ โดยไม่ต้องเปิดเผยรหัสผ่านของผู้ใช้ครับ ผลลัพธ์ของกระบวนการนี้คือ Client จะได้รับ Access Token ครับ
ส่วน JWT (JSON Web Token) คือ รูปแบบของ Token (Token Format) ครับ โดยทั่วไปแล้ว Access Token ที่ OAuth 2.0 ออกให้นั้นมักจะเป็น JWT นั่นเองครับ JWT เป็นเหมือน “ภาชนะ” ที่บรรจุข้อมูลยืนยันตัวตนและสิทธิ์การเข้าถึงไว้ในตัวมันเอง ทำให้ Resource Server สามารถตรวจสอบ Token ได้โดยไม่ต้องสอบถามจากฐานข้อมูลครับ
สรุปคือ OAuth 2.0 คือ “วิธีการ” ในการได้มาซึ่ง “กุญแจ” (Access Token) ส่วน JWT คือ “รูปแบบ” ของ “กุญแจ” นั้นครับ
Q2: JWT ปลอดภัยจริงหรือครับ? มีข้อควรระวังอะไรบ้าง?
A2: JWT ปลอดภัยในแง่ที่ว่ามันป้องกันการเปลี่ยนแปลงข้อมูลใน Token ได้ด้วย Signature ครับ หากข้อมูลใน Header หรือ Payload ถูกแก้ไข Signature จะไม่ตรงกัน ทำให้เซิร์ฟเวอร์ตรวจจับการปลอมแปลงได้ครับ
อย่างไรก็ตาม JWT ก็มีข้อควรระวังสำคัญครับ:
- Secret Key รั่วไหล: หาก Key ที่ใช้ในการสร้าง Signature รั่วไหล ผู้โจมตีสามารถสร้าง JWT ปลอมได้ครับ
- ไม่มีกลไกเพิกถอนในตัว: เมื่อ JWT ถูกออกไปแล้ว ตราบใดที่ยังไม่หมดอายุ มันยังคงใช้งานได้ครับ การเพิกถอนต้องใช้วิธี Blacklisting หรือการใช้ Refresh Token ร่วมกันครับ
- ข้อมูลใน Payload ไม่ได้เข้ารหัส: ข้อมูลใน Payload ของ JWT สามารถถอดรหัสได้ง่ายด้วย Base64Url-decode ครับ ดังนั้นไม่ควรใส่ข้อมูลที่อ่อนไหวมากๆ ลงไปครับ
- XSS Attack: หากเก็บ JWT ไว้ใน Local Storage บนเบราว์เซอร์ มีความเสี่ยงที่จะถูกโจมตีแบบ XSS และขโมย Token ไปได้ครับ การใช้ HttpOnly Secure Cookie เป็นทางเลือกที่ปลอดภัยกว่าสำหรับ Access Token ครับ
Q3: ควรเก็บ JWT (Access Token) ไว้ที่ไหนใน Client-side ครับ?
A3: คำถามนี้เป็นประเด็นถกเถียงกันในวงการครับ ไม่มีคำตอบที่สมบูรณ์แบบ แต่มีแนวทางดังนี้:
- HttpOnly Secure Cookie: ถือเป็นวิธีที่ปลอดภัยที่สุดจาก XSS (Cross-Site Scripting) ครับ เพราะ JavaScript ไม่สามารถเข้าถึง Cookie ได้โดยตรง แต่มีข้อจำกัดเรื่อง CORS (Cross-Origin Resource Sharing) และการจัดการ CSRF (Cross-Site Request Forgery) Token ครับ
- Local Storage/Session Storage: ใช้งานง่ายสำหรับนักพัฒนา เพราะเข้าถึงได้ด้วย JavaScript แต่มีความเสี่ยงสูงต่อ XSS ครับ หากเว็บไซต์ของคุณมีช่องโหว่ XSS ผู้โจมตีสามารถรันสคริปต์เพื่อขโมย JWT ได้ครับ
สำหรับ Access Token ที่มีอายุสั้นๆ (เช่น 15-30 นาที) การเก็บในหน่วยความจำของแอปพลิเคชัน (In-memory) หรือ Local Storage อาจเป็นที่ยอมรับได้หากแอปพลิเคชันของคุณมีมาตรการป้องกัน XSS ที่เข้มงวดครับ สำหรับ Refresh Token ที่มีอายุยาวนานกว่า ควรเก็บไว้ใน HttpOnly Secure Cookie หรือบน Back-end ของคุณอย่างปลอดภัยครับ
Q4: JWT สามารถเพิกถอนได้หรือไม่ครับ?
A4: โดยพื้นฐานแล้ว JWT ไม่มีกลไกการเพิกถอน (Revocation) ในตัวครับ เมื่อ JWT ถูกออกไปแล้ว ตราบใดที่ยังไม่หมดอายุ (exp claim) มันก็ยังถือว่าถูกต้องและใช้งานได้ครับ
แต่เราสามารถ “จำลอง” การเพิกถอนได้ด้วยวิธีต่างๆ ครับ:
- Blacklisting: สร้างรายการของ JWT ที่ถูกเพิกถอนแล้วเก็บไว้ในฐานข้อมูลหรือ Cache (เช่น Redis) ครับ ทุกครั้งที่เซิร์ฟเวอร์ได้รับ JWT ก็จะตรวจสอบกับ Blacklist ก่อนว่า Token นี้ถูกเพิกถอนไปแล้วหรือไม่ครับ
- Short-lived Access Tokens + Refresh Tokens: กำหนดให้ Access Token มีอายุสั้นมากๆ (เช่น 15 นาที) ครับ เมื่อผู้ใช้ออกจากระบบ ก็จะเพิกถอน Refresh Token ทันที ทำให้ Access Token ที่มีอยู่จะหมดอายุไปเองในไม่ช้า และไม่สามารถขอ Access Token ใหม่ได้อีกครับ
- Session Management: บรรจุ Session ID ลงใน JWT และเมื่อต้องการเพิกถอน ก็แค่ลบ Session ID นั้นออกจากฐานข้อมูลครับ
Q5: OpenID Connect (OIDC) เกี่ยวข้องกับ OAuth 2.0 และ JWT อย่างไรครับ?
A5: OpenID Connect (OIDC) คือ เลเยอร์การยืนยันตัวตน (Authentication Layer) ที่สร้างขึ้นบน OAuth 2.0 ครับ
- OAuth 2.0: มุ่งเน้นไปที่การ มอบสิทธิ์ (Authorization) ให้ Client เข้าถึงทรัพยากรครับ มันบอกว่า “แอปพลิเคชันนี้ได้รับอนุญาตให้ทำอะไรได้บ้างในนามของผู้ใช้” แต่ไม่ได้บอกว่า “ใครคือผู้ใช้นั้น” ครับ
- OIDC: เพิ่มความสามารถในการ ยืนยันตัวตน (Authentication) เข้ามาครับ เมื่อใช้ OIDC ร่วมกับ OAuth 2.0 Authorization Server จะออก
ID Token(ซึ่งเป็น JWT) มาให้ Client ด้วยครับ ID Token นี้จะบรรจุข้อมูลพื้นฐานของผู้ใช้ เช่น User ID, ชื่อ, อีเมล และอื่นๆ ที่ผ่านการยืนยันตัวตนแล้ว ทำให้ Client ทราบว่า “ใครคือผู้ใช้ที่กำลังใช้งานอยู่” ครับ
ดังนั้น โดยรวมแล้ว OAuth 2.0 จัดการเรื่อง “สิทธิ์ในการเข้าถึง” ส่วน OIDC ใช้ OAuth 2.0 เป็นกลไกพื้นฐานเพื่อเพิ่มความสามารถในการ “ยืนยันตัวตน” และส่งข้อมูลยืนยันตัวตนในรูปแบบ JWT (ID Token) กลับมาให้ Client ครับ
Q6: Scopes ใน OAuth 2.0 คืออะไรครับ?
A6: Scopes คือ ชุดของสิทธิ์หรือขอบเขตการเข้าถึง ที่ Client ร้องขอจาก Resource Owner ครับ มันเป็นวิธีที่ละเอียดในการระบุว่า Client ต้องการเข้าถึงข้อมูลประเภทใด หรือทำอะไรได้บ้างบน Resource Server ครับ
ตัวอย่างเช่น:
email: ขอสิทธิ์เข้าถึงที่อยู่อีเมลของผู้ใช้profile: ขอสิทธิ์เข้าถึงข้อมูลโปรไฟล์พื้นฐานของผู้ใช้https://www.googleapis.com/auth/drive.file: ขอสิทธิ์เข้าถึงไฟล์ที่สร้างหรือเปิดด้วยแอปพลิเคชันของคุณบน Google Driveread:posts: ขอสิทธิ์อ่านโพสต์write:posts: ขอสิทธิ์เขียนโพสต์
Resource Owner จะเห็น Scopes เหล่านี้บนหน้าจอขอความยินยอม และสามารถเลือกได้ว่าจะอนุญาตหรือไม่ครับ การใช้ Scopes ที่เหมาะสมช่วยเพิ่มความโปร่งใสและควบคุมการเข้าถึงข้อมูลได้ดียิ่งขึ้นครับ Resource Server จะต้องตรวจสอบ Scope ใน Access Token (JWT) เพื่อยืนยันว่า Client มี