บทช่วยสอน JSON Web Token: ตัวอย่างใน Laravel และ AngularJS

เผยแพร่แล้ว: 2022-03-11

ด้วยความนิยมที่เพิ่มขึ้นของแอปพลิเคชันหน้าเดียว แอปพลิเคชันมือถือ และบริการ RESTful API วิธีที่นักพัฒนาเว็บเขียนโค้ดส่วนหลังจึงเปลี่ยนไปอย่างมาก ด้วยเทคโนโลยีเช่น AngularJS และ BackboneJS เราไม่ใช้เวลามากในการสร้างมาร์กอัปอีกต่อไป แต่เรากำลังสร้าง API ที่แอปพลิเคชันส่วนหน้าของเราใช้ แบ็คเอนด์ของเราเป็นเรื่องเกี่ยวกับตรรกะทางธุรกิจและข้อมูลมากขึ้น ในขณะที่ตรรกะของการนำเสนอจะถูกย้ายไปยังแอปพลิเคชันส่วนหน้าหรือบนมือถือเท่านั้น การเปลี่ยนแปลงเหล่านี้นำไปสู่แนวทางใหม่ในการนำการพิสูจน์ตัวตนไปใช้ในแอพพลิเคชั่นสมัยใหม่

การรับรองความถูกต้องเป็นส่วนที่สำคัญที่สุดของเว็บแอปพลิเคชันใดๆ เป็นเวลาหลายทศวรรษที่คุกกี้และการตรวจสอบสิทธิ์บนเซิร์ฟเวอร์เป็นวิธีที่ง่ายที่สุด อย่างไรก็ตาม การจัดการการรับรองความถูกต้องในแอปพลิเคชั่นมือถือและหน้าเดียวที่ทันสมัยอาจเป็นเรื่องยาก และต้องการแนวทางที่ดีกว่า วิธีแก้ปัญหาที่เป็นที่รู้จักดีที่สุดสำหรับปัญหาการพิสูจน์ตัวตนสำหรับ API คือ OAuth 2.0 และ JSON Web Token (JWT)

ก่อนที่เราจะเข้าสู่บทช่วยสอน JSON Web Token นี้ JWT คืออะไรกันแน่?

JSON Web Token คืออะไร?

JSON Web Token ใช้เพื่อส่งข้อมูลที่สามารถตรวจสอบและเชื่อถือได้โดยใช้ลายเซ็นดิจิทัล ประกอบด้วยอ็อบเจ็กต์ JSON ขนาดกะทัดรัดและปลอดภัยต่อ URL ซึ่งลงนามด้วยการเข้ารหัสเพื่อตรวจสอบความถูกต้อง และสามารถเข้ารหัสได้หากเพย์โหลดมีข้อมูลที่ละเอียดอ่อน

เนื่องจากโครงสร้างที่กะทัดรัด JWT จึงมักใช้ในส่วนหัว HTTP Authorization หรือพารามิเตอร์การสืบค้น URL

โครงสร้างของ JSON Web Token

JWT จะแสดงเป็นลำดับของค่าที่เข้ารหัส base64url ที่คั่นด้วยอักขระจุด

ตัวอย่างโทเค็นเว็บ JSON ใน laravel และ angularjs

นี่คือตัวอย่างโทเค็น JWT:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0 . yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

หัวข้อ

ส่วนหัวมีข้อมูลเมตาสำหรับโทเค็นและมีประเภทของลายเซ็นและอัลกอริทึมการเข้ารหัสเพียงเล็กน้อย (คุณสามารถใช้เครื่องมือจัดรูปแบบ JSON เพื่อทำให้วัตถุ JSON สวยงาม)

ตัวอย่างส่วนหัว

 { "alg": "HS256", "typ": "JWT" }

ส่วนหัวของตัวอย่าง JWT นี้ประกาศว่าอ็อบเจ็กต์ที่เข้ารหัสคือ JSON Web Token และมีการเซ็นชื่อโดยใช้อัลกอริทึม HMAC SHA-256

เมื่อเข้ารหัสด้วย base64 แล้ว เราก็มีส่วนแรกของ JWT ของเรา

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

เพย์โหลด (การเรียกร้อง)

ในบริบทของ JWT การอ้างสิทธิ์สามารถกำหนดเป็นคำสั่งเกี่ยวกับเอนทิตี (โดยทั่วไปคือผู้ใช้) เช่นเดียวกับข้อมูลเมตาเพิ่มเติมเกี่ยวกับโทเค็นเอง การอ้างสิทธิ์ประกอบด้วยข้อมูลที่เราต้องการส่ง และเซิร์ฟเวอร์สามารถใช้เพื่อจัดการการตรวจสอบสิทธิ์ JSON Web Token ได้อย่างถูกต้อง มีการอ้างสิทธิ์หลายอย่างที่เราสามารถให้ได้ ซึ่งรวมถึงชื่อการเรียกร้องที่ลงทะเบียน ชื่อการเรียกร้องสาธารณะ และชื่อการเรียกร้องส่วนตัว

การเรียกร้อง JWT ที่ลงทะเบียน

นี่คือการอ้างสิทธิ์ที่ลงทะเบียนในรีจิสทรี IANA JSON Web Token Claims การอ้างสิทธิ์ JWT เหล่านี้ไม่ได้มุ่งหมายให้เป็นข้อบังคับแต่เพื่อให้เป็นจุดเริ่มต้นสำหรับชุดของการอ้างสิทธิ์ที่เป็นประโยชน์และทำงานร่วมกันได้

ซึ่งรวมถึง:

  • iss : ผู้ออกโทเค็น
  • sub : เรื่องของโทเค็น
  • aud : ผู้ชมของโทเค็น
  • exp : เวลาหมดอายุ JWT ที่กำหนดไว้ใน Unix time
  • nbf : เวลา "ไม่ก่อน" ที่ระบุเวลาก่อนหน้าที่ JWT จะต้องไม่ได้รับการยอมรับสำหรับการประมวลผล
  • iat : เวลา "ออกเมื่อ" ในเวลา Unix ที่ออกโทเค็น
  • jti : การอ้างสิทธิ์ JWT ID ให้ตัวระบุเฉพาะสำหรับ JWT

เรียกร้องสาธารณะ

การอ้างสิทธิ์สาธารณะจำเป็นต้องมีชื่อที่ป้องกันการชนกัน โดยการตั้งชื่อเป็น URI หรือ URN การชนกันของการตั้งชื่อจะหลีกเลี่ยงได้สำหรับ JWT โดยที่ผู้ส่งและผู้รับไม่ได้เป็นส่วนหนึ่งของเครือข่ายแบบปิด

ตัวอย่างของชื่อการอ้างสิทธิ์สาธารณะอาจเป็น https://www.toptal.com/jwt_claims/is_admin และแนวทางปฏิบัติที่ดีที่สุดคือการวางไฟล์ไว้ที่ตำแหน่งนั้นที่อธิบายการอ้างสิทธิ์เพื่อให้สามารถยกเลิกการอ้างอิงสำหรับเอกสารได้

เรียกร้องส่วนตัว

ชื่อการอ้างสิทธิ์ส่วนบุคคลอาจใช้ในสถานที่ที่มีการแลกเปลี่ยน JWT ในสภาพแวดล้อมแบบปิดระหว่างระบบที่รู้จักเท่านั้น เช่น ภายในองค์กร นี่เป็นคำกล่าวอ้างที่เราสามารถกำหนดตัวเราเองได้ เช่น ID ผู้ใช้ บทบาทของผู้ใช้ หรือข้อมูลอื่นๆ

การใช้ชื่อการอ้างสิทธิ์ที่อาจมีความหมายเชิงความหมายที่ขัดแย้งกันนอกระบบปิดหรือระบบส่วนตัวอาจเกิดการขัดแย้งกันได้ ดังนั้นโปรดใช้ด้วยความระมัดระวัง

สิ่งสำคัญที่ควรทราบคือ เราต้องการเก็บโทเค็นของเว็บให้เล็กที่สุดเท่าที่จะเป็นไปได้ ดังนั้นให้ใช้เฉพาะข้อมูลที่จำเป็นในการอ้างสิทธิ์แบบสาธารณะและส่วนตัว

น้ำหนักบรรทุกตัวอย่าง JWT

 { "iss": "toptal.com", "exp": 1426420800, "https://www.toptal.com/jwt_claims/is_admin": true, "company": "Toptal", "awesome": true }

เพย์โหลดตัวอย่างนี้มีการอ้างสิทธิ์ที่ลงทะเบียนไว้สองรายการ การอ้างสิทธิ์สาธารณะหนึ่งรายการ และการอ้างสิทธิ์ส่วนตัวสองรายการ เมื่อเข้ารหัส base64 แล้ว เราก็มีส่วนที่สองของ JWT ของเรา

 eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0

ลายเซ็น

มาตรฐาน JWT เป็นไปตามข้อกำหนด JSON Web Signature (JWS) เพื่อสร้างโทเค็นที่ลงนามในขั้นสุดท้าย มันถูกสร้างขึ้นโดยการรวม JWT Header ที่เข้ารหัสและ JWT Payload ที่เข้ารหัส แล้วเซ็นชื่อโดยใช้อัลกอริธึมการเข้ารหัสที่รัดกุม เช่น HMAC SHA-256 รหัสลับของลายเซ็นถูกเก็บไว้โดยเซิร์ฟเวอร์ ดังนั้นจึงสามารถตรวจสอบโทเค็นที่มีอยู่และเซ็นชื่อใหม่ได้

 $encodedContent = base64UrlEncode(header) + "." + base64UrlEncode(payload); $signature = hashHmacSHA256($encodedContent);

นี่ทำให้เราเป็นส่วนสุดท้ายของ JWT ของเรา

 yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw

ความปลอดภัยและการเข้ารหัส JWT

การใช้ TLS/SSL ร่วมกับ JWT เป็นสิ่งสำคัญอย่างยิ่ง เพื่อป้องกันการโจมตีจากคนกลาง ในกรณีส่วนใหญ่ วิธีนี้จะเพียงพอที่จะเข้ารหัสเพย์โหลด JWT หากมีข้อมูลที่ละเอียดอ่อน อย่างไรก็ตาม หากเราต้องการเพิ่มชั้นการป้องกันเพิ่มเติม เราสามารถเข้ารหัสข้อมูล JWT ได้เองโดยใช้ข้อกำหนด JSON Web Encryption (JWE)

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

ทำไมต้องมี Web Tokens?

ก่อนที่เราจะเห็นประโยชน์ทั้งหมดของการใช้การพิสูจน์ตัวตน JWT เราต้องดูวิธีการพิสูจน์ตัวตนในอดีตก่อน

การรับรองความถูกต้องตามเซิร์ฟเวอร์

การรับรองความถูกต้องตามเซิร์ฟเวอร์

เนื่องจากโปรโตคอล HTTP เป็นแบบไร้สัญชาติ จึงจำเป็นต้องมีกลไกสำหรับการจัดเก็บข้อมูลผู้ใช้และวิธีตรวจสอบสิทธิ์ผู้ใช้ในคำขอที่ตามมาทุกครั้งหลังจากเข้าสู่ระบบ เว็บไซต์ส่วนใหญ่ใช้คุกกี้เพื่อจัดเก็บ ID เซสชันของผู้ใช้

มันทำงานอย่างไร

เบราว์เซอร์ส่งคำขอ POST ไปยังเซิร์ฟเวอร์ที่มีการระบุตัวตนและรหัสผ่านของผู้ใช้ เซิร์ฟเวอร์ตอบสนองด้วยคุกกี้ซึ่งตั้งค่าไว้บนเบราว์เซอร์ของผู้ใช้ และรวม ID เซสชันเพื่อระบุผู้ใช้

ในทุกคำขอที่ตามมา เซิร์ฟเวอร์จำเป็นต้องค้นหาเซสชันนั้นและยกเลิกการซีเรียลของเซสชันนั้น เนื่องจากข้อมูลผู้ใช้ถูกเก็บไว้บนเซิร์ฟเวอร์

ข้อเสียของการรับรองความถูกต้องบนเซิร์ฟเวอร์

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

  • การแชร์คำขอข้ามต้นทาง (CORS) : เมื่อใช้การเรียก AJAX เพื่อดึงทรัพยากรจากโดเมนอื่น ("ข้ามต้นทาง") เราอาจประสบปัญหากับคำขอที่ต้องห้าม เนื่องจากโดยค่าเริ่มต้น คำขอ HTTP จะไม่รวมคุกกี้ข้าม คำขอต้นทาง

  • การเชื่อมต่อกับเฟรมเวิร์กของเว็บ : เมื่อใช้การพิสูจน์ตัวตนบนเซิร์ฟเวอร์ เราจะเชื่อมโยงกับโครงร่างการตรวจสอบสิทธิ์ของเฟรมเวิร์ก เป็นเรื่องยากหรือเป็นไปไม่ได้เลยที่จะแบ่งปันข้อมูลเซสชันระหว่างเฟรมเวิร์กเว็บต่างๆ ที่เขียนด้วยภาษาโปรแกรมต่างๆ

การตรวจสอบสิทธิ์โดยใช้โทเค็น

การตรวจสอบสิทธิ์โดยใช้โทเค็น

การตรวจสอบสิทธิ์แบบใช้โทเค็น/JWT ไม่มีสถานะ ดังนั้นจึงไม่จำเป็นต้องจัดเก็บข้อมูลผู้ใช้ในเซสชัน สิ่งนี้ทำให้เราสามารถปรับขนาดแอปพลิเคชันของเราโดยไม่ต้องกังวลว่าผู้ใช้จะเข้าสู่ระบบที่ใด เราสามารถใช้โทเค็นเดียวกันเพื่อดึงทรัพยากรที่ปลอดภัยจากโดเมนอื่นที่ไม่ใช่โดเมนที่เราเข้าสู่ระบบได้อย่างง่ายดาย

JSON Web Token ทำงานอย่างไร

เบราว์เซอร์หรือไคลเอ็นต์มือถือส่งคำขอไปยังเซิร์ฟเวอร์การตรวจสอบสิทธิ์ซึ่งมีข้อมูลการเข้าสู่ระบบของผู้ใช้ เซิร์ฟเวอร์การตรวจสอบความถูกต้องสร้างโทเค็นการเข้าถึง JWT ใหม่และส่งคืนไปยังไคลเอนต์ ในทุกคำขอไปยังทรัพยากรที่ถูกจำกัด ไคลเอนต์จะส่งโทเค็นการเข้าถึงในสตริงการสืบค้นหรือส่วนหัว Authorization จากนั้นเซิร์ฟเวอร์จะตรวจสอบความถูกต้องของโทเค็น และหากถูกต้อง ให้ส่งคืนทรัพยากรที่ปลอดภัยไปยังไคลเอ็นต์

เซิร์ฟเวอร์การตรวจสอบสิทธิ์สามารถลงนามในโทเค็นโดยใช้วิธีการลายเซ็นที่ปลอดภัย ตัวอย่างเช่น สามารถใช้อัลกอริธึมคีย์สมมาตร เช่น HMAC SHA-256 ได้หากมีช่องทางที่ปลอดภัยในการแบ่งปันคีย์ลับระหว่างทุกฝ่าย หรืออาจใช้ระบบกุญแจสาธารณะแบบอสมมาตร เช่น RSA ได้เช่นกัน โดยไม่จำเป็นต้องแชร์คีย์เพิ่มเติม

ข้อดีของการรับรองความถูกต้องด้วยโทเค็น

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

การนำกลับมาใช้ใหม่ ได้ : เราสามารถมีเซิร์ฟเวอร์แยกกันได้จำนวนมาก ทำงานบนหลายแพลตฟอร์มและหลายโดเมน นำโทเค็นเดียวกันกลับมาใช้ใหม่เพื่อตรวจสอบสิทธิ์ผู้ใช้ ง่ายต่อการสร้างแอปพลิเคชันที่แชร์การอนุญาตกับแอปพลิเคชันอื่น

JWT Security : เนื่องจากเราไม่ได้ใช้คุกกี้ เราจึงไม่จำเป็นต้องป้องกันการโจมตี cross-site request forgery (CSRF) เราควรยังคงเข้ารหัสโทเค็นของเราโดยใช้ JWE หากเราต้องใส่ข้อมูลที่ละเอียดอ่อนลงในโทเค็น และส่งโทเค็นของเราผ่าน HTTPS เพื่อป้องกันการโจมตีจากคนกลาง

ประสิทธิภาพ : ไม่มีการค้นหาฝั่งเซิร์ฟเวอร์เพื่อค้นหาและดีซีเรียลไลซ์เซสชันในแต่ละคำขอ สิ่งเดียวที่เราต้องทำคือคำนวณ HMAC SHA-256 เพื่อตรวจสอบความถูกต้องของโทเค็นและแยกวิเคราะห์เนื้อหา

ตัวอย่างโทเค็นเว็บ JSON โดยใช้ Laravel 5 และ AngularJS

ในบทช่วยสอน JWT นี้ ฉันจะสาธิตวิธีการใช้การพิสูจน์ตัวตนพื้นฐานโดยใช้ JSON Web Tokens ในสองเทคโนโลยีเว็บยอดนิยม: Laravel 5 สำหรับโค้ดแบ็กเอนด์ และ AngularJS สำหรับตัวอย่าง Frontend Single Page Application (SPA) (คุณสามารถค้นหาการสาธิตทั้งหมดได้ที่นี่ และซอร์สโค้ดในที่เก็บ GitHub นี้ เพื่อให้คุณสามารถปฏิบัติตามบทช่วยสอนได้)

ตัวอย่างโทเค็นเว็บ JSON นี้จะไม่ใช้การเข้ารหัสใดๆ เพื่อรับรองการรักษาความลับของข้อมูลที่ส่งในการอ้างสิทธิ์ ในทางปฏิบัติ เป็นเรื่องปกติเพราะ TLS/SSL เข้ารหัสคำขอ อย่างไรก็ตาม หากโทเค็นจะมีข้อมูลที่ละเอียดอ่อน เช่น หมายเลขประกันสังคมของผู้ใช้ ก็ควรเข้ารหัสโดยใช้ JWE ด้วย

ตัวอย่างแบ็กเอนด์ Laravel

เราจะใช้ Laravel เพื่อจัดการการลงทะเบียนผู้ใช้ ยืนยันข้อมูลผู้ใช้ไปยังฐานข้อมูล และให้ข้อมูลที่ถูกจำกัดบางอย่างซึ่งต้องมีการตรวจสอบสิทธิ์เพื่อให้แอป Angular ใช้งานได้ เราจะสร้างตัวอย่างโดเมนย่อย API เพื่อจำลองการแชร์ทรัพยากรข้ามต้นทาง (CORS) เช่นกัน

การติดตั้งและการบูตโปรเจ็กต์

ในการใช้ Laravel เราต้องติดตั้ง Composer package manager บนเครื่องของเรา เมื่อพัฒนาใน Laravel ฉันขอแนะนำให้ใช้ "กล่อง" ที่บรรจุไว้ล่วงหน้าของ Laravel Homestead ของ Vagrant มันให้สภาพแวดล้อมการพัฒนาที่สมบูรณ์แก่เราโดยไม่คำนึงถึงระบบปฏิบัติการของเรา

วิธีที่ง่ายที่สุดในการเริ่มต้นแอปพลิเคชัน JWT Laravel ของเราคือการใช้แพ็คเกจ Composer Laravel Installer

 composer global require "laravel/installer=~1.1"

ตอนนี้เราทุกคนพร้อมที่จะสร้างโครงการ Laravel ใหม่โดยเรียกใช้ laravel new jwt

สำหรับคำถามใดๆ เกี่ยวกับกระบวนการนี้ โปรดดูเอกสารทางการของ Laravel

หลังจากที่เราสร้างแอปพลิเคชัน Laravel 5 พื้นฐานแล้ว เราจำเป็นต้องตั้งค่า Homestead.yaml ซึ่งจะกำหนดค่าการแมปโฟลเดอร์และการกำหนดค่าโดเมนสำหรับสภาพแวดล้อมภายในเครื่องของเรา

ตัวอย่างของไฟล์ Homestead.yaml :

 --- ip: "192.168.10.10" memory: 2048 cpus: 1 authorize: /Users/ttkalec/.ssh/public.psk keys: - /Users/ttkalec/.ssh/private.ppk folders: - map: /coding/jwt to: /home/vagrant/coding/jwt sites: - map: jwt.dev to: /home/vagrant/coding/jwt/public - map: api.jwt.dev to: /home/vagrant/coding/jwt/public variables: - key: APP_ENV value: local

หลังจากที่เราบูตกล่อง Vagrant ด้วยคำสั่ง vagrant up และเข้าสู่ระบบโดยใช้ vagrant ssh เราจะไปยังไดเร็กทอรีโครงการที่กำหนดไว้ก่อนหน้านี้ ในตัวอย่างด้านบนนี้จะเป็น /home/vagrant/coding/jwt ตอนนี้เราสามารถเรียกใช้คำสั่ง php artisan migrate เพื่อสร้างตารางผู้ใช้ที่จำเป็นในฐานข้อมูลของเรา

การติดตั้ง Composer Dependencies

โชคดีที่มีชุมชนนักพัฒนาซอฟต์แวร์ที่ทำงานบน Laravel และดูแลแพ็คเกจที่ยอดเยี่ยมมากมายที่เราสามารถนำมาใช้ซ้ำและขยายแอปพลิเคชันของเราได้ ในตัวอย่างนี้ เราจะใช้ tymon/jwt-auth โดย Sean Tymon สำหรับจัดการโทเค็นทางฝั่งเซิร์ฟเวอร์ และ barryvdh/laravel-cors โดย Barry vd Heuvel สำหรับการจัดการ CORS

jwt-auth

ต้องการ tymon/jwt-auth ใน composer.json ของเรา และอัปเดตการพึ่งพาของเรา

 composer require tymon/jwt-auth 0.5.*

เพิ่ม JWTAuthServiceProvider ลงในอาร์เรย์ผู้ให้บริการ app/config/app.php ของเรา

 'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'

ต่อไป ในไฟล์ app/config/app.php ภายใต้อาร์เรย์ aliases เราเพิ่มส่วนหน้า JWTAuth

 'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'

สุดท้าย เราจะต้องการเผยแพร่การกำหนดค่าแพ็คเกจโดยใช้คำสั่งต่อไปนี้: php artisan config:publish tymon/jwt-auth

โทเค็นเว็บ JSON ถูกเข้ารหัสโดยใช้รหัสลับ เราสามารถสร้างคีย์นั้นได้โดยใช้คำสั่ง php artisan jwt:generate มันจะถูกวางไว้ในไฟล์ config/jwt.php ของเรา อย่างไรก็ตาม ในสภาพแวดล้อมการใช้งานจริง เราไม่ต้องการให้มีรหัสผ่านหรือคีย์ API ของเราในไฟล์การกำหนดค่า เราควรวางไว้ในตัวแปรสภาพแวดล้อมเซิร์ฟเวอร์และอ้างอิงในไฟล์การกำหนดค่าด้วยฟังก์ชัน env ตัวอย่างเช่น:

 'secret' => env('JWT_SECRET')

เราสามารถหาข้อมูลเพิ่มเติมเกี่ยวกับแพ็คเกจนี้และการตั้งค่าคอนฟิกทั้งหมดบน Github

laravel-cors

ต้องการ barryvdh/laravel-cors ใน composer.json ของเราและอัปเดตการพึ่งพาของเรา

 composer require barryvdh/laravel-cors 0.4.x@dev

เพิ่ม CorsServiceProvider ลงในอาร์เรย์ผู้ให้บริการ app/config/app.php ของเรา

 'Barryvdh\Cors\CorsServiceProvider'

จากนั้นเพิ่มมิดเดิลแวร์ใน app/Http/Kernel.php ของเรา

 'Barryvdh\Cors\Middleware\HandleCors'

เผยแพร่การกำหนดค่าไปยังไฟล์ config/cors.php ในเครื่องโดยใช้คำสั่ง php artisan vendor:publish

ตัวอย่างการกำหนดค่าไฟล์ cors.php :

 return [ 'defaults' => [ 'supportsCredentials' => false, 'allowedOrigins' => [], 'allowedHeaders' => [], 'allowedMethods' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [], ], 'paths' => [ 'v1/*' => [ 'allowedOrigins' => ['*'], 'allowedHeaders' => ['*'], 'allowedMethods' => ['*'], 'maxAge' => 3600, ], ], ];

การกำหนดเส้นทางและการจัดการคำขอ HTTP

เพื่อความกระชับ ฉันจะใส่รหัสทั้งหมดของฉันไว้ในไฟล์ routes.php ที่รับผิดชอบการกำหนดเส้นทาง Laravel และการมอบหมายคำขอไปยังตัวควบคุม โดยปกติเราจะสร้างตัวควบคุมเฉพาะสำหรับจัดการคำขอ HTTP ทั้งหมดของเรา และทำให้โค้ดของเราเป็นแบบโมดูลาร์และสะอาด

เราจะโหลดมุมมอง AngularJS SPA ของเราโดยใช้

 Route::get('/', function () { return view('spa'); });

การลงทะเบียนผู้ใช้

เมื่อเราส่งคำขอ POST เพื่อ /signup ด้วยชื่อผู้ใช้และรหัสผ่าน เราจะพยายามสร้างผู้ใช้ใหม่และบันทึกลงในฐานข้อมูล หลังจากสร้างผู้ใช้แล้ว JWT จะถูกสร้างขึ้นและส่งคืนผ่านการตอบกลับ JSON

 Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); });

เข้าสู่ระบบผู้ใช้

เมื่อเราส่งคำขอ POST ไปยัง /signin ด้วยชื่อผู้ใช้และรหัสผ่าน เราจะตรวจสอบว่าผู้ใช้มีอยู่จริงและส่งคืน JWT ผ่านการตอบกลับ JSON

 Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if ( ! $token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); });

การดึงทรัพยากรที่ถูกจำกัดบนโดเมนเดียวกัน

เมื่อผู้ใช้ลงชื่อเข้าใช้แล้ว เราสามารถดึงทรัพยากรที่ถูกจำกัดได้ ฉันได้สร้างเส้นทาง /restricted ที่จำลองทรัพยากรที่ต้องการผู้ใช้ที่รับรองความถูกต้องแล้ว ในการดำเนินการนี้ ส่วนหัว Authorization คำขอหรือสตริงการสืบค้นต้องระบุ JWT สำหรับแบ็กเอนด์เพื่อยืนยัน

 Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]);

ในตัวอย่างนี้ ฉันใช้ jwt-auth ที่ให้มาใน jwt-auth โดยใช้ 'before' => 'jwt-auth' มิดเดิลแวร์นี้ใช้เพื่อกรองคำขอและตรวจสอบโทเค็น JWT หากโทเค็นไม่ถูกต้อง ไม่มีอยู่ หรือหมดอายุ มิดเดิลแวร์จะส่งข้อยกเว้นที่เราสามารถตรวจจับได้

ใน Laravel 5 เราสามารถตรวจจับข้อยกเว้นโดยใช้ไฟล์ app/Exceptions/Handler.php การใช้ฟังก์ชันการ render เราสามารถสร้างการตอบสนอง HTTP ตามข้อยกเว้นที่ส่งออกมา

 public function render($request, Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) { return response(['Token is invalid'], 401); } if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) { return response(['Token has expired'], 401); } return parent::render($request, $e); }

หากผู้ใช้ได้รับการตรวจสอบสิทธิ์และโทเค็นถูกต้อง เราสามารถส่งคืนข้อมูลที่จำกัดไปยังส่วนหน้าผ่าน JSON ได้อย่างปลอดภัย

กำลังดึงทรัพยากรที่ถูกจำกัดจากโดเมนย่อย API

ในตัวอย่างโทเค็นเว็บ JSON ถัดไป เราจะใช้แนวทางอื่นในการตรวจสอบความถูกต้องของโทเค็น แทนที่จะใช้ jwt-auth เราจะจัดการกับข้อยกเว้นด้วยตนเอง เมื่อเราส่งคำขอ POST ไปยังเซิร์ฟเวอร์ API api.jwt.dev/v1/restricted จำกัด เรากำลังส่งคำขอข้ามต้นทาง และต้องเปิดใช้งาน CORS ในแบ็กเอนด์ โชคดีที่เราได้กำหนดค่า CORS ในไฟล์ config/cors.php แล้ว

 Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });

ตัวอย่างส่วนหน้าของ AngularJS

เรากำลังใช้ AngularJS เป็น front-end โดยอาศัยการเรียก API ไปยังเซิร์ฟเวอร์การตรวจสอบสิทธิ์ส่วนหลังของ Laravel สำหรับการตรวจสอบสิทธิ์ผู้ใช้และข้อมูลตัวอย่าง บวกกับเซิร์ฟเวอร์ API สำหรับข้อมูลตัวอย่างข้ามต้นทาง เมื่อเราไปที่หน้าแรกของโครงการ แบ็กเอนด์จะให้บริการ resources/views/spa.blade.php ที่จะบูตแอปพลิเคชัน Angular

นี่คือโครงสร้างโฟลเดอร์ของแอป Angular:

 public/ |-- css/ `-- bootstrap.superhero.min.css |-- lib/ |-- loading-bar.css |-- loading-bar.js `-- ngStorage.js |-- partials/ |-- home.html |-- restricted.html |-- signin.html `-- signup.html `-- scripts/ |-- app.js |-- controllers.js `-- services.js

Bootstrapping แอปพลิเคชันเชิงมุม

spa.blade.php มีสิ่งจำเป็นที่จำเป็นในการรันแอปพลิเคชัน เราจะใช้ Twitter Bootstrap เพื่อจัดสไตล์ พร้อมด้วยธีมที่กำหนดเองจาก Bootswatch เพื่อให้มีการตอบสนองด้วยภาพเมื่อทำการโทร AJAX เราจะใช้สคริปต์ angular-loading-bar ซึ่งจะสกัดกั้นคำขอ XHR และสร้างแถบการโหลด ในส่วนหัว เรามีสไตล์ชีตต่อไปนี้:

 <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css">

ส่วนท้ายของมาร์กอัปของเรามีการอ้างอิงถึงไลบรารี เช่นเดียวกับสคริปต์ที่กำหนดเองของเราสำหรับโมดูล Angular ตัวควบคุม และบริการ

 <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>

เรากำลังใช้ไลบรารี ngStorage สำหรับ AngularJS เพื่อบันทึกโทเค็นลงในที่จัดเก็บในเครื่องของเบราว์เซอร์ เพื่อให้เราสามารถส่งในแต่ละคำขอผ่านส่วนหัว Authorization

ในสภาพแวดล้อมการใช้งานจริง เราจะย่อขนาดและรวมไฟล์สคริปต์และสไตล์ชีตทั้งหมดของเราเข้าด้วยกันเพื่อปรับปรุงประสิทธิภาพ

ฉันได้สร้างแถบนำทางโดยใช้ Bootstrap ที่จะเปลี่ยนการมองเห็นของลิงก์ที่เหมาะสม ขึ้นอยู่กับสถานะการลงชื่อเข้าใช้ของผู้ใช้ สถานะการลงชื่อเข้าใช้ถูกกำหนดโดยการมีอยู่ของตัวแปร token ในขอบเขตของตัวควบคุม

 <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li> <li data-ng-show="token"><a ng-click="logout()">Logout</a></li> </ul> </div>

การกำหนดเส้นทาง

เรามีไฟล์ชื่อ app.js ซึ่งมีหน้าที่ในการกำหนดค่าเส้นทางส่วนหน้าทั้งหมดของเรา

 angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { BASE: 'http://jwt.dev:8000', BASE_API: 'http://api.jwt.dev:8000/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' });

ที่นี่เราจะเห็นว่าเราได้กำหนดเส้นทางสี่เส้นทางที่จัดการโดย HomeController หรือ RestrictedController ทุกเส้นทางสอดคล้องกับมุมมอง HTML บางส่วน นอกจากนี้เรายังได้กำหนดค่าคงที่สองค่าที่มี URL สำหรับคำขอ HTTP ของเราไปยังแบ็กเอนด์

ขอตัวสกัดกั้น

บริการ $http ของ AngularJS ช่วยให้เราสามารถสื่อสารกับแบ็กเอนด์และสร้างคำขอ HTTP ในกรณีของเรา เราต้องการสกัดกั้นทุกคำขอ HTTP และฉีดด้วยส่วนหัว Authorization ที่มี JWT ของเราหากผู้ใช้ได้รับการตรวจสอบสิทธิ์ เรายังสามารถใช้ตัวสกัดกั้นเพื่อสร้างตัวจัดการข้อผิดพลาด HTTP ส่วนกลาง นี่คือตัวอย่างของ interceptor ของเราที่ฉีด token หากมีอยู่ในที่จัดเก็บในเครื่องของเบราว์เซอร์

 $httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) { return { 'request': function (config) { config.headers = config.headers || {}; if ($localStorage.token) { config.headers.Authorization = 'Bearer ' + $localStorage.token; } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { $location.path('/signin'); } return $q.reject(response); } }; }]);

คอนโทรลเลอร์

ในไฟล์ controllers.js เราได้กำหนดตัวควบคุมสองตัวสำหรับแอปพลิเคชันของเรา: HomeController และ RestrictedController HomeController จัดการฟังก์ชันการลงชื่อเข้าใช้ ลงชื่อสมัครใช้ และออกจากระบบ โดยจะส่งข้อมูลชื่อผู้ใช้และรหัสผ่านจากแบบฟอร์มลงชื่อเข้าใช้และลงชื่อสมัครใช้ไปยังบริการตรวจสอบสิทธิ์ ซึ่งจะส่งคำขอ Auth ไปยังแบ็กเอนด์ จากนั้นจะบันทึกโทเค็นไปยังที่จัดเก็บในตัวเครื่อง หรือแสดงข้อความแสดงข้อผิดพลาด ขึ้นอยู่กับการตอบสนองจากแบ็กเอนด์

 angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) }; $scope.signup = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signup(formData, successAuth, function () { $rootScope.error = 'Failed to signup'; }) }; $scope.logout = function () { Auth.logout(function () { window.location = "/" }); }; $scope.token = $localStorage.token; $scope.tokenClaims = Auth.getTokenClaims(); }])

RestrictedController ทำงานในลักษณะเดียวกัน เพียงดึงข้อมูลโดยใช้ฟังก์ชัน getRestrictedData และ getApiData บนบริการ Data

 .controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) { Data.getRestrictedData(function (res) { $scope.data = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted content.'; }); Data.getApiData(function (res) { $scope.api = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted API content.'; }); }]);

แบ็กเอนด์มีหน้าที่ให้บริการข้อมูลที่จำกัดก็ต่อเมื่อผู้ใช้ได้รับการตรวจสอบสิทธิ์แล้วเท่านั้น ซึ่งหมายความว่าเพื่อตอบสนองต่อข้อมูลที่จำกัด คำขอสำหรับข้อมูลนั้นจำเป็นต้องมี JWT ที่ถูกต้องภายในส่วนหัว Authorization หรือสตริงการสืบค้น หากไม่เป็นเช่นนั้น เซิร์ฟเวอร์จะตอบสนองด้วยรหัสสถานะข้อผิดพลาด 401 Unauthorized

บริการตรวจสอบสิทธิ์

บริการตรวจสอบสิทธิ์มีหน้าที่ในการลงชื่อเข้าใช้และลงทะเบียนคำขอ HTTP ไปยังแบ็กเอนด์ หากคำขอสำเร็จ การตอบสนองจะมีโทเค็นที่ลงนาม ซึ่งจากนั้นจะถอดรหัส base64 และข้อมูลการอ้างสิทธิ์โทเค็นที่ปิดไว้จะถูกบันทึกลงในตัวแปร tokenClaims สิ่งนี้ถูกส่งไปยังคอนโทรลเลอร์ผ่านฟังก์ชัน getTokenClaims

 angular.module('app') .factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) { function urlBase64Decode(str) { var output = str.replace('-', '+').replace('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!'; } return window.atob(output); } function getClaimsFromToken() { var token = $localStorage.token; var user = {}; if (typeof token !== 'undefined') { var encoded = token.split('.')[1]; user = JSON.parse(urlBase64Decode(encoded)); } return user; } var tokenClaims = getClaimsFromToken(); return { signup: function (data, success, error) { $http.post(urls.BASE + '/signup', data).success(success).error(error) }, signin: function (data, success, error) { $http.post(urls.BASE + '/signin', data).success(success).error(error) }, logout: function (success) { tokenClaims = {}; delete $localStorage.token; success(); }, getTokenClaims: function () { return tokenClaims; } }; } ]);

บริการข้อมูล

นี่เป็นบริการง่ายๆ ที่ส่งคำขอไปยังเซิร์ฟเวอร์การตรวจสอบสิทธิ์ เช่นเดียวกับเซิร์ฟเวอร์ API สำหรับข้อมูลจำลองที่จำกัดการจำลอง มันทำการร้องขอและมอบหมายการเรียกกลับที่ประสบความสำเร็จและข้อผิดพลาดไปยังคอนโทรลเลอร์

 angular.module('app') .factory('Data', ['$http', 'urls', function ($http, urls) { return { getRestrictedData: function (success, error) { $http.get(urls.BASE + '/restricted').success(success).error(error) }, getApiData: function (success, error) { $http.get(urls.BASE_API + '/restricted').success(success).error(error) } }; } ]);

นอกเหนือจากบทช่วยสอน JSON Web Token นี้

การรับรองความถูกต้องโดยใช้โทเค็นช่วยให้เราสามารถสร้างระบบที่แยกจากกันซึ่งไม่ได้เชื่อมโยงกับรูปแบบการตรวจสอบสิทธิ์เฉพาะ โทเค็นอาจถูกสร้างขึ้นที่ใดก็ได้และใช้บนระบบใด ๆ ที่ใช้รหัสลับเดียวกันสำหรับการเซ็นชื่อโทเค็น พวกเขาพร้อมสำหรับมือถือและไม่ต้องการให้เราใช้คุกกี้

JSON Web Tokens ใช้งานได้กับภาษาโปรแกรมยอดนิยมทั้งหมดและกำลังได้รับความนิยมอย่างรวดเร็ว ได้รับการสนับสนุนจากบริษัทต่างๆ เช่น Google, Microsoft และ Zendesk ข้อกำหนดมาตรฐานโดย Internet Engineering Task Force (IETF) ยังคงอยู่ในเวอร์ชันร่างและอาจเปลี่ยนแปลงเล็กน้อยในอนาคต

ยังมีอีกมากที่จะกล่าวถึง JWT เช่น วิธีจัดการรายละเอียดความปลอดภัย และการรีเฟรชโทเค็นเมื่อหมดอายุ แต่บทแนะนำ JSON Web Token ควรแสดงให้เห็นถึงการใช้งานพื้นฐานและที่สำคัญกว่านั้นคือข้อดีของการใช้ JWT

อ่านเพิ่มเติมในบล็อก Toptal Engineering:

  • การสร้าง Node.js/TypeScript REST API ส่วนที่ 3: MongoDB การตรวจสอบสิทธิ์ และการทดสอบอัตโนมัติ