จากตอน 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 ของเราได้เรียบร้อยแล้ว