สร้างระบบสมาชิกแบบง่าย ๆ🚀
เขียนเว็บระบบสมาชิกด้วย Pug, Express, JWT
ข้อควรรู้
เนื่องจากเราจะใช้ Ecosystem ของ JavaScript ในการพัฒนาระบบนี้ ผมขอแนะนำให้ทำความคุ้นเคย JavaScript ES6 ไว้ก่อน รวมถึง HTTP, Cookies เบื้องต้น เพื่อความเข้าใจอย่างถ่องแท้ครับ 📕
โครงสร้าง (Architecture)
เราจะใช้ token ในการยืนยันตัวตนของผู้ใช้บริการ โดยสามารถได้รับจากการเข้าสู่ระบบหรือสมัครสมาชิก ดังภาพ
token จะถูกเก็บไว้ใน cookies ซึ่งจะถูกส่งมาพร้อมกับ request ทุกครั้ง ทำให้ server สามารถยืนยันตัวตนของผู้ใช้บริการได้
วิธีการนี้เรียกว่า Stateless สามารถอ่านเพิ่มเติมได้ที่นี้
Node.js Library
Library ที่เราจะใช้กันหลัก ๆ มี 3 อย่างคือ
- Express
- Pug
- JWT
Express คือ Web Framework ที่หลาย ๆ คนรู้จักกันดี
Pug คือ Templating Engine ทำให้เราสามารถ render HTML ตามข้อมูลที่เราต้องการได้ รวมถึงทำให้เขียน HTML ได้ง่ายและกระชับขึ้นอีกด้วย
JWT หรือ JSON Web Token คือ Token ที่ผู้ให้บริการจะออกให้กับฝั่งผู้ใช้งาน โดยผู้ใช้งานจะใช้ Token นี้ในการยืนยันตัวตน
โครงสร้างโปรเจค🔥
เริ่มจากขั้นแรกคือสร้างโปรเจคและติดตั้ง dependencies ทั้งหมด
$ mkdir authentication && cd authentication
$ npm init -y
$ npm install express cookie-parser body-parser jsonwebtoken pug
สร้างไฟล์ index.js โค้ดมีดังนี้
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const jwt = require("jsonwebtoken");const SECRET = "TOKEN_SECRET";let users = [];
const apiRoute = express.Router();const app = express();
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use("/api", apiRoute);app.listen(8080, () => {
console.log("Listening on :8080");
});
โอเค เรียบง่ายมาก โค้ดนี้จะสร้างเซิร์ฟเวอร์ด้วย express และรอรับ connection อยู่บนพอร์ต 8080
ค่าคงที่ SECRET จะกำหนดเป็นอะไรก็ได้ แต่ต้องปลอดภัย คาดเดาไม่ได้ และห้ามเผยแพร่ให้ใครเห็นเด็ดขาด ใช้สำหรับเข้ารหัสและถอดรหัส token
ส่วน users จะใช้สำหรับเก็บข้อมูลผู้ใช้งาน จริง ๆ แล้วเราจำเป็นต้องใช้ฐานข้อมูล แต่เพื่อความเรียบง่ายของบทความนี้ เลยตัดสินใจใช้แบบนี้แทนครับผม 😅
apiRoute ใช้สำหรับรวมกลุ่ม API route เพื่อให้ง่ายต่อการจัดการ API 🚀
API — Application Programming Interface
สำหรับ API ที่เราต้องสร้าง จะมีด้วยกัน 3 route คือ
- POST /register — สมัครสมาชิก
- POST /login — เข้าสู่ระบบ
- GET /logout — ออกจากระบบ
สมัครสมาชิก (POST /register)
apiRoute.post("/register", (req, res) => {
const { name, username, password } = req.body; if (!name || !username || !password)
return res.redirect("/?error=missing credentials"); if (users.some((user) => username === user.username))
return res.redirect("/?error=username already exists"); let user = {
name,
username,
password,
}; users = [user, ...users]; const token = jwt.sign({ data: { name } }, SECRET);
res.cookie("token", token, { httpOnly: true });
res.redirect("/");
});
Route นี้ อยู่บน /register และประมวลดังนี้
- เช็ค body (Form data ชื่อ name, username, password)
- เช็คว่า username ถูกใช้ไปหรือยังใน users ที่ประกาศตั้งแต่แรก
- สร้างและเพิ่มผู้ใช้ใหม่ใน users
- สร้าง token ด้วย SECRET ที่กำหนดไว้ จากนั้น set cookie พร้อม httpOnly (สำคัญมาก เพื่อความปลอดภัย)
- redirect กลับไปที่ / (หน้าหลักสำหรับผู้ใช้งาน ซึ่งเราจะมาสร้างภายหลัง)
เราพึ่งได้สร้าง logic สำหรับสมัครสมาชิกมาแล้ว โดยถ้าหากเกิด error ขึ้นมาก็จะทำการ redirect กลับไปยัง / พร้อม query ชื่อว่า error เพื่อให้ทราบว่ามีข้อผิดพลาดอะไรเกิดขึ้น 🚧
หากการสมัครสมาชิกสำเร็จด้วยดี ตอนนี้ฝั่งผู้ใช้งานจะได้รับ token อยู่ใน cookie เรียบร้อย หลังจากนี้ cookie จะสามารถใช้เพื่อแสดงถึงตัวตนได้ 🎉
เข้าสู่ระบบ (POST /login)
apiRoute.post("/login", (req, res) => {
const { username, password } = req.body; if (!username || !password)
return res.redirect("/?error=missing credentials"); const user = users.find(
(user) => username === user.username && password === user.password
); if (!user) return res.redirect("/?error=invalid credentials"); const token = jwt.sign({ data: { name: user.name } }, SECRET);
res.cookie("token", token, { httpOnly: true });
res.redirect("/");
});
Route นี้จะอยู่บน /login โดยจะประมวลผลดังนี้
- เช็คว่ามี username และ password ส่งมาไหม
- ตรวจสอบว่า มีข้อมูลที่ตรงกันไหม
- สร้าง token ด้วย SECRET และ set cookie ด้วย httpOnly เหมือนเดิม
- redirect กลับไปยัง /
เรียบง่าย ตรงไปตรงมาดี 🌻
ออกจากระบบ (GET /logout)
apiRoute.get("/logout", (req, res) => {
res.clearCookie("token");
res.redirect("/");
});
สำหรับ /logout จะทำการลบ cookie ออกและ redirect กลับไปยัง /
API route ทั้งสามเสร็จแล้ว 🎉 โดยจะใช้งานได้ผ่าน prefix /api
ส่วนผู้ใช้งาน 💻
ได้เวลาสร้างเพจสำหรับผู้ใช้งาน ที่สามารถ สมัครสมาชิก, เข้าสู่ระบบ และออกจากระบบได้ ตัวอย่างผลลัพท์จะประมาณนี้
- ก่อนเข้าสู่ระบบ
- หลังเข้าสู่ระบบ
ทั้งหมดนี้เขียนด้วย Pug ซึ่งมี syntax เฉพาะตัว แนะนำให้อ่าน official document หากต้องการดูรายละเอียดทั้งหมด 📔
Pug Time 🐶
สร้างโฟลเดอร์ views และสร้างไฟล์ด้านในชื่อ index.pug โค้ดมีดังนี้
doctype html
html(lang="en")
head
meta(charset='UTF-8')
title Authentication
style.
body {
text-align: center;
}
form {
display: inline-flex;
flex-direction: column;
margin: 0 20px;
}
label {
display: flex;
justify-content: space-between;
}
.error {
color: red;
}
if name
h1 Welcome, #{name} 🔥
span
a(href="/api/logout")
button Logout
else
h1 Not authenticated 🚧if error
h2.error= errorform(action='/api/register' method='post')
h3 Register
label
span Name:
input(required='' type='text' name='name' placeholder='Name')
label
span Username:
input(required='' type='text' name='username' placeholder='Username')
label
span Password:
input(required='' type='password' name='password' placeholder='Password')
input(type='submit' value='Register')form(action='/api/login' method='post')
h3 Login
label
span Username:
input(required='' type='text' name='username' placeholder='Username')
label
span Password:
input(required='' type='password' name='password' placeholder='Password')
input(type='submit' value='Login')
สังเกตที่โค้ดส่วนนี้
if name
h1 Welcome, #{name} 🔥
span
a(href="/api/logout")
button Logout
else
h1 Not authenticated 🚧if error
h2.error= error
Pug สามารถสร้าง logic การ render ได้ ในทีนี้เราตรวจสอบว่ามีการส่งชื่อ (name) มาไหม หากใช่ให้แสดงชื่อและปุ่มออกจากระบบ และหากไม่ใช่ ก็ให้แสดงคำว่า “Not authenticated” แทน
มีการตรวจสอบ error ด้วย เพื่อแสดงข้อผิดพลาด เช่น ข้อมูลที่กรอกไม่ถูกต้อง
Finish it up 🎯
ตอนนี้เซิร์ฟเวอร์เรายังไม่ได้ implement การ render ด้วย templating engine (Pug) เพราะฉะนั้นต้องกลับมายังไฟล์ index.js อีกครั้ง
ในไฟล์ index.js เพิ่มโค้ดนี้เข้าไป
function authMiddleware(req, res, next) {
const token = req.cookies.token; if (token)
jwt.verify(token, SECRET, (err, decoded) => {
req.user = err || decoded.data;
}); next();
}app.use(authMiddleware);
เราสร้าง Middleware ขึ้นมา ซึ่งจะอ่านค่า cookie แล้วยืนยัน token (JWT) ว่าถูกต้องไหมด้วย SECRET หากถูกต้องจะทำการแนบข้อมูลของผู้ใช้ที่ถอดรหัสได้ไว้ที่ req
app.set("view engine", "pug")app.get("/", (req, res) => {
res.render("index",
{
name: req.user?.name,
error: req.query?.error
});
});
กำหนดค่า view engine ให้ใช้ pug เป็น templating engine
แล้วสร้าง route GET / ขึ้นมา ซึ่งจะทำการ render ไฟล์ index.pug ที่เราสร้างด้วยข้อมูลของผู้ใช้ หรือข้อผิดพลาดจาก query error
ได้เวลาลอง 🚀
เริ่มเซิร์ฟเวอร์ด้วยคำสั่ง $ node index.js
แล้วไปยัง http://localhost:8080
จะได้ผลลัพท์แบบนี้ 🎉
สรุป 📚
มีอะไรเกิดขึ้นมากมายด้านหลัง ตอนนี้เราได้เรียนรู้การใช้งาน Express, JWT, Pug แนะนำหากต้องการเรียนรู้เพิ่มเติม สามารถเข้าไปอ่านได้ที่เว็บหลักเลย
เพิ่มเติม
ในบทความนี้ไม่ได้สอนถึงวิธีการเข้ารหัสรหัสผ่าน เป็นการเก็บรหัสผ่านแบบตรง ๆ ซึ่งไม่แนะนำให้ทำใน Production ควรจะมีการเข้ารหัสก่อน เช่น ด้วย bcrypt
Goodbye 🖐
หวังว่าบทความนี้จะช่วยทำให้เข้าใจมากขึ้น หากมีคำถามสงสัยสามารถสอบถามได้เลยครับผม ☕