สอนทำเว็บไซต์ด้วย Node.js, Express และ MongoDB ตอนที่ 9 - ทำระบบ Login ด้วย Passport.js
สวัสดีครับ มาต่อกันที่ตอนที่ 9 กันนะครับ สำหรับตอนนี้เราจะมาเริ่มทำส่วนการ Login ด้วยการใช้ Middleware และการใช้ตัวข่วยอย่าง Passport.js กันนะครับ
โดยตัวอย่างนี้ จะเป็นกึ่งๆ Workshop นิดๆ จากเนื้อหา ตอนที่ 1-8 มารวมเป็นบทความนี้ครับ (อาจจะไปเร็วนึงนึง เพราะ assume ว่าผู้อ่าน ได้อ่าน และทำความเข้าใจเนื้อหาตอนก่อนๆมาแล้วนะครับ)
- ใช้ Express Generator ในสร้างโปรเจ็ค
- ใช้ Pug ในการทำ Template มีหน้า Login / Register และหน้า Home เพื่อแสดงข้อมูลถ้า Login เรียบร้อยแล้ว
- เก็บข้อมูล User ไว้ใน MongoDB
- ใช้งาน Passport.js (Middleware ที่ช่วยในการทำ Authentication)
- มีการเก็บข้อมูล Session (จะได้เรียนรู้ในบทความนี้)
โดยในตัวอย่างนี้ จะเป็น Web Application แบบทั่วๆไปนะครับ คือเป็น Server Side Rendering ไม่ใช่เป็น Single Page Application นะครับ
ตัวอย่างเว็บ หลังจากทำเว็บ จะได้หน้าตาประมาณนี้นะครับ http://example-web.now.sh
ส่วนเรื่อง Single Page Application (SPA) และการทำ RESTful API สำหรับส่วน Backend เดี๋ยวจะพูดถึงในบทถัดๆไปครับ
เนื้อหาบทเรียน
- ตอนที่ 1 - NodeJS คืออะไร + ทำการติดตั้ง Node.js และ Node.js เบื้องต้น
- ตอนที่ 2 - ทบทวนพื้นฐาน JavaScript และ Modern JavaScript ES6, ES7+
- ตอนที่ 3 - ว่าด้วยพื้นฐาน Node.js / Callback / Sync และ Async
- ตอนที่ 4 - เริ่มต้นทำเว็บด้วย Node.js และ Express.js
- ตอนที่ 5 - ลองหัดใช้ Template Engine ชื่อ Pug
- ตอนที่ 6 - เริ่มต้นกับ MongoDB
- ตอนที่ 7 - ทำ Backend API ด้วย Node.js และ MongoDB กันดีกว่า
- ตอนที่ 8 - Express Generator / Middleware
- ตอนที่ 9 - ทำระบบ Login ด้วย Passport.js (บทความนี้)
- ตอนที่ 10 - การ Hosting และ Deploy Production
Step 1 - Create Project
เริ่มต้นเราไม่รอช้า ทำการสร้างโปรเจ็คใหม่ด้วย Express Generator เลยครับ
เราก็จะได้โปรเจ็ค Express ขึ้นมา โดยมี Pug เป็น Template ครับ (สำหรับตัวอย่างนี้ผมจะพยายามข้ามเรื่อง Style ของเว็บนะครับ ไม่ได้เน้นความสวยงาม เน้น Functionality ที่มันทำงานได้) โดยตัว UI ผมใช้เป็น Bootstrap ธรรมดาเลยนะครับ
ต่อมาที่ไฟล์ views/layout.pug
ผมเพิ่ม Bootstrap css ลงไปครับ
ทำการสั่ง Start server
จะได้ Server รันบน Localhost ครับ http://localhost:3000
Step 2 - สร้างหน้า Login / Register
ต่อมาเราจะทำหน้า Form สำหรับ Login และ Register ครับ เป็นแบบง่ายๆ เลยคือ HTML มี input สำหรับใส่ username และ password ครับ โดย Register จะมี Optional ให้ใส่ name ตอนลงทะเบียนเพิ่มไปด้วย
สร้างไฟล์ views/register.pug
ขึ้นมา
โดยเมื่อ Submit form มันจะไปเรียก POST /auth/register
นะครับ
ต่อมาสร้าง views/login.pug
เหมือนกัน คล้ายๆกับ Register เลย เพียงแค่ลบ input name ออก และเปลี่ยน method เป็น POST /auth/login
ครับ
ต่อมาใส่ CSS นิดนึง ตรงไฟล์ public/stylesheets/style.css
ทีนี้เราจะเพิ่ม Router ของ Express กันครับ ตัว Generator มันสร้างไว้ให้แล้ว ทีนี้ผมก็จะทำจากไฟล์เดิมเลยคือ routes/index.js
ไฟล์ index
นี้ คือ เพิ่ม /register
และ /login
ให้มัน render ไฟล์ pug template ที่เราเพิ่งสร้างด้านบนครับ
ทีนี้ลองเข้าเว็บ http://localhost:3000/register และ http://localhost:3000/login เราก็จะได้หน้า Form สำหรับ Login และ Register แล้วครับ
Step 3 - รับข้อมูลจาก Form
ต่อมาเราจะทำการรับข้อมูลจาก Form ที่กรอกเข้ามาจากหน้า Login และ Register นะครับ โดยใช้ routes ที่ตัว Express Generator นั้น gen มาให้แล้ว คือ routes/users.js
แต่ผมจะเปลี่ยนชื่อใหม่เป็น routes/auth.js
ละกันครับ
โดย กำหนดเป็นแบบ .post()
ครับ เพื่อรับค่าจาก Form ทีนี้ เราลอง console.log ดูค่า req.body
ดูครับ
แก้ไข app.js
นิดหน่อย เนื่องจากผมเปลี่ยนชื่อไฟล์ บรรทัด 6-7
และบรรทัด 22 23
ทีนี้เวลาเรา Submit จะตอน Login หรือ Register ค่าก็จะถูกส่งมา เข้า route router.post('/register')
เราก็จะเห็นค่า req.body
ครับ ทีนี้ส่ิงที่เราจะต้องทำคือ save ค่าที่ user ส่งมา เนี่ย ลง database
จริงๆแล้ว การใช้งาน Production เราต้องมีการ validate ค่าต่างๆ ด้วยนะครับ เช่น express-validation, joi, yup เป็นต้น
Step 4 - เชื่อมต่อ MongoDB
ต่อมาเราจะทำการเชื่อมต่อ MongoDB โดยใช้ Mongoose ครับ เพื่อเก็บข้อมูล username / password ในระบบ นั่นเอง
ติดตั้ง Mongoose
ต่อมาสร้างไฟล์ db.js
เพื่อไว้ connect mongodb
ชื่อ Database แล้วแต่เพื่อนๆ จะตั้งเลยนะครับ ใครจะเป็นอะไรก็ได้ ไม่จำเป็นต้อง
ahoy-node-passport
เหมือนในบทความ
ต่อมาเพิ่มตรงนี้ลงไปในไฟล์ app.js
เพื่อ import ไฟล์ที่เรา connect mongodb ไว้
ต่อมา สร้าง User Model ขึ้นมาที่ไฟล์ models/User.js
เพื่อเอาไว้ใช้ตอน insert หรือ query
ทีนี้กลับไปที่ routes/auth.js
ตอน login และ register เราก็รับค่า req.body
มา แล้ว save ลง database เลยครับ
โดยสำหรับ Register เราก็จะรับ req.body
มาเซฟลง Database และ Login เราก็จะรับ input มาเป้น criteria ในการ findOne()
หา User ในระบบ ถ้าเจอ และ username และ password ถูก ก็แสดงว่า user login เข้าสู่ระบบได้สำเร็จ
แต่ๆ ระบบเรายังมีช่องโหว่ และมีสิ่งที่ไม่ควรทำอย่างยิ่ง!! แม้ว่าจะเป็นแค่ Development หรือทำระบบเล็กๆ หรือทำอะไรขึ้นมาเล่นๆ นั่นก็คือ Password ไม่ควรเก็บเป็น Plain Text
Step 4 - Hash Password ซะ
ข้อนี้เป็นสิ่งที่จำเป็นที่สุด !!! เน้นย้ำเลยครับ Password ที่เราเก็บในระบบ ห้ามเก็บแบบ Plain Text เด็ดขาด เราจะไม่สามารถรู้ได้เลยว่า Password ที่เก็บคืออะไร ไม่สามารถเปลี่ยน Password ให้คนอื่นได้ ทำได้อย่างเดียว คือการ Reset Password ครับ
และถ้าเราไปใช้ระบบ หรือบริการไหน ที่เราทำการขอ Forgot Password แล้วระบบส่ง Password เรากลับมาในอีเมล์ นั้นแสดงว่าระบบหรือบริการนั้นเก็บ Password เราแบบ Plain text สามารถเห็น Password เราและของคนอื่นๆในระบบได้หมดเลย เรียกได้ว่าไม่ปลอดภัยมากๆ และควรเปลี่ยน Password โดยด่วน (ไม่ว่าจะเป็น Developers, Admin หรือใครก็ตาม ก็ไม่มีสิทธิ์ หรือไม่ควรจะรู้ Password คนอื่นได้ครับ)
ส่วนการ Hash นั้น คือการเอา Password ปกติมาเข้ารหัสทงเดียว โดยได้ผลลัพธ์ที่เราไม่สามารถรู้ได้ว่า Password เราคืออะไร ส่วนความปลอดภัย ขึ้นอยู่กับ Algorithm และจำนวนการ hash ครับ
เราก็ไม่สามารถรู้ได้ว่า Password คืออะไร วิธีการที่ใช้ในการเช็คว่า Password ถูกต้องมั้ย คือการเปรียบเทียบ สิ่งที่ Input มากับค่า Password ที่มันถูก hash แล้ว ถ้ามันตรงกัน แสดงว่า Password ถูกครับ
การ Hash เราจะใช้ Library ที่ชื่อว่า bcrypt
ครับ ทำการติดตั้งลงไป
ทีนี้การ Hash Password เราจะทำได้โดยการเรียก function
ส่วนการ Compare ก็จะใช้แบบนี้
ซึ่งทั้งคู่ เป็นแบบ callback base เราสามารถใช้ hashSync
และ compareSync
เพื่อให้มัน hash/compare แบบ synchronus ได้เช่นกันครับ
ทีนี้กลับมาที่ส่วนรับค่า req.body
จาก User ตอนสมัครและลงทะเบียน เราต้องทำการ hash ค่า password ของ User ก่อน แบบนี้ครับ routes/auth.js
ส่วนของ Login ก็ทำแบบเดียวกันครับ เราจะ findOne()
โดยใช้ Username เราจะได้ค่า password
ที่ hash แล้วใน Database จากนั้นค่อยเอาค่าที่ hash ที่เราเก็บไว้ มาเปรียบเทียบกับที่ User ส่งมาจาก form login
ทีนี้ ข้อมูลที่เราเก็บส่วนที่เป็น Password ก็จะถูก Hash เรียบร้อยแล้ว ลองทำการ Register และ Login ใหม่ รวมถึงลองเช็คใน Mongo Database ดูครับ ว่าข้อมูลเราจะไม่ใช่ Plain Text แล้ว
Step 5 - เก็บ Session เมื่อ Login
ต่อมา กรณีที่เรา Login หรือ Register จะทำยังไงให้ระบบรู้ว่าเรายังอยู่ในระบบ ยังไม่ได้ Logout ไปไหน ทุกครั้งที่ Refresh หรือ ปิดแท้ป แล้วเปิดใหม่ ไม่จำเป็นต้อง Login แต่ต้องสามารถเข้าได้เลย เพราะว่ามี Session อยู่
วิธีที่ง่ายที่สุดคือ ทำ Middleware ขึ้นมาใช้เองครับ
อย่างที่รู้จากบทความตอนก่อนว่า Middleware คือ function ของ Express ที่มี request ,response และ next เราสามารถ implement อะไรก็ได้ ตามที่เราต้องการ
เราเพียงแค่สร้าง Function ขึ้นมาครับ เป็น Middleware ที่เอาไว้เช็คว่ามี req.user
มั้ย? ถ้ามีแสดงว่า User นั้น Login หรือมี Session อยู่
ต่อมา ก็แค่เอาไปใส่ใน routes/index.js
แบบนี้
ทีนี้ พอเข้าหน้า http://localhost:3000 จะถูกเด้งไปหน้า /login
ทุกครั้ง เนื่องจากไม่มี req.user
ต่อมา เราเพิ่ม req.user
ตอนที่เค้า Login เรียบร้อยแล้ว หรือ Register เรียบร้อยแล้ว ที่ไฟล์ routes/auth.js
assign ค่า user จาก database ไปใส่ object req
ก่อนที่จะ render index ครับ ทีนี้เวลาเรา ยังไม่ได้ login มันก็จะเด้งไปหน้า login แต่ถ้า login เรียบร้อยแล้ว มันจะ redirect ไป index แล้วก็ไม่ถูก redirect มา login แล้ว เพราะเรามี req.user
ครับ
ทีนี้ปัญหาก็ยังมีคือ เรา refresh มันไม่ได้จำ req.user
ครับ
วิธีแก้ไขคือ ต้องพึ่ง Session ครับ วิธีง่ายๆ คือใช้ express-session
ทำการติดตั้ง
จากนั้นที่ไฟล์ app.js
เพิ่มนี้ลงไป
ตัว Middleware ของ Session จะสร้าง req.session
ขึ้นมาครับ ทีนี้เราก็สามารถ assign ค่าไปที่ object นี้ได้
ทีนี้ส่วนที่ Login และ Register ที่ใช้ req.user
ก็เปลี่ยนเป็น
แล้วส่วน isLoggedIn
ก็เปลี่ยนเป็นแบบนี้
ทีนี้เราก็จะได้ Session แล้วครับ ที่เป็น built-in และมันจะ clear ก็ต่อเมื่อเรา restart server ครับ
Step 6 - ใช้ Passport.js มาช่วยในการ Authentication
ต่อมา เราไม่อยากเขียน Middleware และ Session เอง เราสามารถใช้ Passport.js ซึ่งเป็น Library ที่นิยมมาทำ Authentication ตัวนึงของ Node.js เลยครับ ด้วยความที่เค้า Provide function และพวก Helper ต่างๆ มาให้ ทำให้มันค่อนข้างง่ายครับ
ซึ่งจริงๆแล้ว Passport.js สามารถทำ Authentication ผ่าน Social Network อื่นๆ เช่น Twitter, Facebook, Google, Github ได้หมดเลยครับ แต่สำหรับบทความนี้ ขอเป็นตัวอย่างเฉพาะ Username / Password ละกันเนาะ
และใน Passport.js เราจะเรียกมันว่า Strategy ครับ โดยใช้ passport-local ครับ
ทำการติดตั้ง
ที่ไฟล์ app.js
เพิ่มนี้ลงไป ค่อนข้างเยอะนิดนึง เดี๋ยวจะพยายามอธิบายครับ
ส่วนต่อมาคือ ไฟล์ routes/auth.js
จะใช้ Passport มาเป็น Middleware ดัง syntax แบบนี้
ก็จะได้เป็น
แถม Logic ที่เรา implement ก็ไม่ต้องใช้แล้ว เพราะมัน handle ที่ Passport LocalStrategy ที่ไฟล์ app.js
เรียบร้อยครับ
สุดท้าย ฟังค์ชั่น isLoggedIn
ที่เราใช้ตอน session เราก็เปลี่ยนมา handle req.isAuthenticated()
ซึ่งเป็น helper ของ Passport กรณีที่มี session มันก็จะ return true ได้แบบนี้ ไฟล์ routes/index.js
สุดท้าย ท้ายสุด ลืมส่วน Logout ครับ เรามีปุ่ม Logout และ request มาที่ /logout
เราก็เพิ่ม router ส่วนนี้เลย ที่ไฟล์ routes/index.js
ตัว Passport มี req.logout()
ที่จัดการพวก session และ logout ให้เรา จากนั้นก็ redirect กลับไป /
เป็นอันเรียบร้อยครบ flow การทำงาน
สรุป
ก็บทความนี้ เป็น Flow การทำ Authentication ตั้งแต่เริ่มต้น ใช้แบบธรรมดา จนมี Session ทำ Middleware เอง และสุดท้ายใช้ Library อย่าง Passport.js เข้ามาช่วย ทำให้ประหยัดเวลา ของเราไปได้มากทีเดียว
ก็หวังว่าบทความนี้เพื่อนๆ จะได้ไอเดียนำไปต่อยอด ได้รู้ว่า Session มันทำงานยังไง ได้รู้ว่า Passport มันทำงานยังไง Serialize / Deserialize เอาอะไรไปเก็บใน Session พวกนี้
และตัวอย่าง มันก็ไม่ได้สมบูรณ์นะครับ อาจจะมีบ้าง ที่ไม่ครบ เป้าหมายของบทความนี้คือเน้นให้เห็นไอเดีย การใช้งานครับ หากขาดตกบกพร่องตรงไหนไป ขออภัยด้วยครับ และหากใครติดปัญหา หรือไม่เข้าใจตรงไหน สามารถสอบถามได้ครับ หรือหากใครเจอข้อผิดพลาด สามารถแนะนำได้เช่นกันครับ
—
ส่วน Source Code (อยู่ part9) สามารถเข้าไปดาวน์โหลด หรือ clone ผ่าน Github ได้เลย หากใครไม่รู้จัก Git สามารถอ่านบทความนี้เพิ่มได้ครับ Git คืออะไร ? + พร้อมสอนใช้งาน Git และ Github
ขอบคุณครับ
❤️ Happy Coding
- Authors
-
Chai Phonbopit
เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust