จากตอน EP ที่แล้วเราได้ทำการเก็บ Data ของ API ด้วย Gorm Library ลงใน SQLite Database เป็นทีเรียบร้อยแล้ว

ในวันนี้เราจะมาทำการทำ Authorization ด้วย JSON Web Token หรือว่า JWT และใช้ Middleware ใน Gin Framework บนภาษา Go กัน

สำหรับใครที่ยังไม่เคยเขียน API ด้วยภาษา Go หรือไม่เคยใช้ Gin Framework มาก่อน ผมแนะนำให้ลองอ่านบทความ EP. 1 ของเราที่นี่ก่อนได้เลยครับ

แต่ถ้าพร้อมแล้ว เรามาเริ่มจากการทำ Authorization แบบง่ายๆ กันก่อนเลยดีกว่า โดยเริ่มจากใน listBooksHandler ของเราเช่นเดิม

เราได้เพิ่ม Logic เกี่ยวกับการทำ Authorization เข้ามาในบรรทัดที่ 2 ถึงบรรทัดที่ 9 โดยเริ่มจากดึง Token ออกมาจาก Header ที่ชื่อ Authorization ใน HTTP Request ด้วยคำสั่ง c.Request.Header.Get("Authorization")

ต่อมาเราทำการตัดมาเฉพาะส่วนที่เป็น token จริงๆ โดยการตัดคำว่า Bearer ออก (เนื่องจากว่าหน้าตาของ Token จะเป็น Bearer MY_TOKEN_STRING )

หลังจากที่เราได้ Token มาแล้ว เราก็ทำการเช็ค token ว่าเป็น token ที่ถูกต้องหรือไม่ด้วยฟังก์ชัน validateToken ซึ่งจะเป็นฟังก์ชันที่เราจะมา implement กันต่อจากนี้ ซึ่งเมื่อฟังก์ชัน validateToken ของเรามี error เกิดขึ้น เราก็จะทำการส่ง status 401 Unauthorized กลับไปให้กับ client ของเรา

ต่อจากนั้นเราลองมา implement ฟังก์ชัน validateToken แบบง่ายๆ กันดูก่อน

โค้ดข้างบนคือการเช็ค token แบบง่ายๆ เลยก็คือแค่ส่งเข้ามาก็ถือว่า valid ไม่ว่า token จะเป็นอะไรก็ตาม

และเราก็มาลองทดสอบ Request กันแบบง่ายๆ ด้วย REST Client ของเรากันเหมือนเดิม โดยที่เราจะยังไม่ได้ใส่ Authorization Header เข้าไปก่อน

GET http://localhost:8080/books

ผลลัพธ์ที่เราได้ก็จะเป็นแบบนี้

ซึ่งผลลัพธ์ก็จะเป็นตามที่คาดนั่นก็คือเราจะได้ status 401 Unauthorized เพราะว่าเราไม่ได้ส่ง Token ให้กับ Server ของเรา

ทีนี้เรามาลองยิง request กันอีกรอบ แต่คราวนี้เราจะส่ง Token เข้าไปด้วยแบบนี้

GET http://localhost:8080/books
Authorization: Bearer abcdefghijk

ผลลัพธ์ที่ได้ก็จะเป็นแบบนี้

ซึ่งเราก็จะสามารถเข้าถึงข้อมูลของ API ได้ตามปกติเหมือนก่อนหน้านี้ที่เราเคยทดสอบ API ไว้ใน EP ก่อนๆ

ทีนี้ถ้าสมมติว่าเราอยากจะนำ Logic นี้ไปใช้กับ route อื่นๆ อย่างเช่น POST และ DELETE ด้วย เราอาจจะสามารถทำได้แบบนี้

ซึ่งสังเกตว่า เราจะต้องมีโค้ดการเช็ค Token อยู่ตามทุก Route ของเราเลย ซึ่งทำให้มี Duplicated Code จำนวนมากตามจำนวน Handler ที่เรามีเลย ซึ่งจะทำให้มีปัญหาการ Maintain โค้ดต่อในอนาคต เพราะถ้าหากต้องการที่จะแก้ Logic Authorization อันนี้ เราก็จะต้องมาไล่แก้กับทุกๆ Handler

ซึ่งปัญหานี้เราสามารถแก้ได้ด้วยการใช้ Middleware

รู้จักกับ Middleware

ตัว Middleware ใน Gin Framework นั้นถูกสร้างมาเพื่อให้มีการใส่โค้ดบางอย่างสำหรับหลายๆ route ที่เราต้องการได้ เพื่อแก้ปัญหาความซ้ำซ้อนของโค้ดอย่างแบบเมื่อกี้นี้เราเจอกัน

ทีนี้เราลองมา Implement ตัว Middleware ของเรากันดูเลยดีกว่า

โดยเริ่มจากการแยก Logic ของการเช็ค Authorization Token มาเป็น Middleware Function ก่อนแบบนี้

หลังจากนั้นก็ทำการสร้าง route group ใน main function และเปลี่ยนการ register route เดิมมาใช้ route group แทนแบบนี้ (บรรทัดที่ 14 ถึง 18)

ทีนี้เราก็สามารถที่จะทำ Logic การ Authorization ไว้ในที่เดียวใน authorizationMiddleware เรียบร้อย ซึ่งทำให้เราสามารถ Maintain โค้ดของเราได้ง่ายขึ้นมากๆ เพราะ Logic จะถูกเก็บไว้อยู่ในที่เดียวใน Middleware

สร้าง Route เพื่อ Generate Token

โดยปกติแล้ว การทำ Authorization นั้น จะต้องมี route เพื่อที่จะ Generate Token ให้กับ Client เพื่อที่ให้ Client สามารถ Access เข้า API ต่างๆ ซึ่ง route นั้นมักจะเป็น route POST /login (ซึ่งใน Tutorial นี้เราจะทำ Route นี้แบบง่ายๆ โดยที่จะไม่ได้มีการเช็ค Logic เรื่องการ Login แต่จะเน้นเรื่อง Middleware และ JWT เป็นหลัก)

โดยเรามาเริ่มการสร้าง Route POST /login

และก็ทำการ Register Route Login บน router แบบปกติที่เราทำกันใน main function (บรรทัดที่ 14)

และเราก็ทำการเปลี่ยน Logic ในฟังก์ชัน validateToken ของเราเป็นแบบนี้แทน เพื่อให้สอดคล้องกันกับ loginHandler ที่เราสร้างขึ้นเมื่อกี้นี้

และเราลองทดสอบกันเช่นเดิม โดยเริ่มจาก Login Route เพื่อเอา Token มาก่อน

POST http://localhost:8080/login

ซึ่งเราก็จะได้ Token ออกมาแบบนี้

และนำ Token ที่ได้มาใช้ในการเข้าถึง API โดยใส่เข้าไปใน Authorization Header

GET http://localhost:8080/books
Authorization: Bearer ACCESS_TOKEN

ซึ่งถ้าเราลองเปลี่ยน Token เป็นแบบอื่นๆ ดู

GET http://localhost:8080/books
Authorization: Bearer ANOTHER_TOKEN

ก็จะเป็น 401 Unauthorized ตามที่เราคาดไว้

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

ซึ่งถ้าเรามีสิ่งที่เป็นเหมือนลายเซ็นของ Server แล้วเราสามารถ “เซ็น” ลายเซ็นเหล่านี้ลงบน Token นี้ได้เพื่อที่เราจะสามารถรู้ได้ว่า Token นี้เป็นของ Server เราจริงๆ ก็น่าจะดี และหนึ่งในวิธีการทำสิ่งนี้ก็คือการทำ JWT หรือ JSON Web Token นั่นเอง

ใช้ JWT ในการทำ Authorization Token

JSON Web Token หรือ JWT เป็นหนึ่งรูปแบบของ Token ที่เรานำมาใช้ในการเก็บข้อมูลในการส่งเพื่อทำการ Authentication และ Authorization
(สำหรับคนที่ยังไม่รู้จักว่า JWT คืออะไร ใช้งานยังไง ผมแนะนำให้ศึกษาเพิ่มเติมเกี่ยวกับ JWT ได้ที่นี่เลยครับ https://jwt.io/introduction)

ซึ่งในภาษา Go เองก็มี library เกี่ยวกับการทำ JWT อยู่ใน Repository https://github.com/golang-jwt/jwt

โดยเราจะแก้ loginHandler ของเราให้ Generate ตัว JWT Token เป็นแบบนี้

ซึ่งในโค้ดนี้ เราจะสร้าง token ด้วยคำสั่ง jwt.NewWithClaims และทำการระบุ signing algorithm ของเราเข้าไป พร้อมกับการใส่ Claim เข้าไป ซึ่งในทีนี้เราจะใส่เพียงแค่เวลาที่หมดอายุของ Token นี้

หลังจากนั้นเราก็จะทำการ “เซ็น” ด้วยลายเซ็นหรือ Signature ของ Server เรา (ซึ่ง Signature นี้จะต้องเก็บไว้เป็นความลับ ไม่ควรมีใครรู้) ซึ่งในทีนี้เราจะใช้เป็น string ("MySignature") ที่แปลงเป็น []byte

และถ้าไม่ได้มี error อะไรจากการที่ทำการ sign signature นั้น เราก็สามารถที่จะคืน token ของเรากลับไปได้ตามปกติ

ทีนี้เราลองมาเทส Route Login เพื่อดู Token ของเรากันใหม่ดีกว่า

POST http://localhost:8080/login

ผลลัพธ์ที่ได้ก็จะเป็นแบบนี้

เราก็จะได้ JSON Web Token ที่ถูก Generate ออกมาเรียบร้อย

ซึ่งถ้าเรานำ Token อันนี้ไปลองใส่ดูใน https://jwt.io/ ดูก็จะสามารถเห็นข้อมูลต่างๆ ที่เราสร้างมาไว้กันเมื่อกี้นี้ได้

ขั้นตอนต่อไปคือเราจะไปแก้ Logic การตรวจสอบ token ที่ฟังก์ชัน validateToken

เราสามารถอ่าน token ได้ด้วยคำสั่ง jwt.Parse โดยเราใส่ token เข้าไป และ function ที่เอาไว้ return signature ที่เราทำการ sign ตัว JWT ไว้ ซึ่งนั้นก็คือ []byte("MySignature") นั่นเอง

นอกจากนี้ภายใน function เราก็มีการเช็คว่า signing method นั้นตรงกับที่เราได้ทำการ signed ไว้ตอนแรกหรือไม่ ซึ่งถ้าไม่ตรงเราก็จะทำการคืนค่า error กลับไป

เมื่อเสร็จเรียบร้อยเราก็ทำการทดสอบ API ของเราด้วย JWT ที่เรา Generate ออกมากันเมื่อกี้นี้

GET http://localhost:8080/books
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDAxNjU0MTZ9.fInd5mOn7nd92-fV30oLTatx13Cun3TeceiY6r0A2E8

ผลลัพธ์ที่เราได้ก็จะเป็นแบบนี้

ซึ่งตรงนี้ถ้าหากว่าใครเกิดเจอว่าขึ้นเป็น 401 Unauthorized ก็อาจจะเป็นเพราะว่าตัว JWT ที่เรา Generate ขึ้นมาในตอนแรกนั้น ได้หมดอายุหรือ expire ไปแล้ว ซึ่งเราก็สามารถแก้ไขได้ง่ายๆ โดยการ Generate Token อันใหม่ที่ POST /login เช่นเดิม

Code ฉบับเต็ม

เสร็จเรียบร้อย! เท่านี้เราก็สามารถใช้ JWT คู่กับ Middleware ใน Gin Framework ในการทำ Authorization ตัว API ของเราได้เรียบร้อยแล้ว

More in:Technology

Comments are closed.