สร้างระบบสมาชิกแบบง่าย ๆ🚀

เขียนเว็บระบบสมาชิกด้วย Pug, Express, JWT

Thanawat Yodnil
4 min readJun 8, 2021
Photo by Sigmund on Unsplash

ข้อควรรู้

เนื่องจากเราจะใช้ Ecosystem ของ JavaScript ในการพัฒนาระบบนี้ ผมขอแนะนำให้ทำความคุ้นเคย JavaScript ES6 ไว้ก่อน รวมถึง HTTP, Cookies เบื้องต้น เพื่อความเข้าใจอย่างถ่องแท้ครับ 📕

โครงสร้าง (Architecture)

Authentication architecture designed by me

เราจะใช้ 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= error
form(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

จะได้ผลลัพท์แบบนี้ 🎉

In action

สรุป 📚

มีอะไรเกิดขึ้นมากมายด้านหลัง ตอนนี้เราได้เรียนรู้การใช้งาน Express, JWT, Pug แนะนำหากต้องการเรียนรู้เพิ่มเติม สามารถเข้าไปอ่านได้ที่เว็บหลักเลย

เพิ่มเติม

ในบทความนี้ไม่ได้สอนถึงวิธีการเข้ารหัสรหัสผ่าน เป็นการเก็บรหัสผ่านแบบตรง ๆ ซึ่งไม่แนะนำให้ทำใน Production ควรจะมีการเข้ารหัสก่อน เช่น ด้วย bcrypt

Goodbye 🖐

หวังว่าบทความนี้จะช่วยทำให้เข้าใจมากขึ้น หากมีคำถามสงสัยสามารถสอบถามได้เลยครับผม ☕

Example code

--

--

Thanawat Yodnil

Look at that new shiny js library | Web dev who love to learn and try new things !