บทที่ 3: ระบบ Authentication (JWT & bcrypt)
สารบัญเนื้อหา
บทที่ 3: ระบบ Authentication (JWT & bcrypt)
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจหลักการทำงานของ Authentication และ Authorization
- Hash รหัสผ่านด้วย bcrypt เพื่อความปลอดภัย
- สร้างและตรวจสอบ JWT (JSON Web Token)
- สร้าง API endpoints สำหรับ Register และ Login
- ป้องกัน Routes ด้วย Authentication Middleware
📚 ทฤษฎีและแนวคิด
Authentication vs Authorization
| Authentication (การยืนยันตัวตน) | Authorization (การอนุญาต) |
|---|---|
| "คุณคือใคร?" | "คุณทำอะไรได้บ้าง?" |
| Login ด้วย email/password | ตรวจสอบ role (user/admin) |
| ตรวจสอบ credentials | ตรวจสอบ permissions |
ตัวอย่าง:
- 🔐 Authentication: ผู้ใช้ login ด้วย email และ password
- 🛡️ Authorization: เฉพาะ admin เท่านั้นที่เข้าถึง admin dashboard ได้
Password Hashing คืออะไร?
ห้าม เก็บรหัสผ่านแบบ plain text ใน Database!
// ❌ อันตราย!
{ password: "123456" }
// ✅ ปลอดภัย
{ passwordHash: "$2b$10$N9qo8uLOickgx2ZMRZoMye..." }
bcrypt คือ algorithm สำหรับ hash รหัสผ่าน
คุณสมบัติ:
- 🔒 One-way function (hash ได้ แต่ decrypt ไม่ได้)
- 🧂 Salt (เพิ่มความสุ่ม ทำให้ hash ของรหัสผ่านเดียวกันไม่เหมือนกัน)
- ⏱️ Slow by design (ป้องกัน brute-force attacks)
JWT (JSON Web Token) คืออะไร?
JWT คือ Token ที่ใช้สำหรับ Authentication แบบ stateless
โครงสร้าง JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NWE...
│ │
│ Header (Base64) │
│
eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva...
│
│ Payload (Base64)
│
HsV3K9qLmNoPqRsTuVwXyZ...
│
│ Signature
ส่วนประกอบ:
- Header: ข้อมูลเกี่ยวกับ algorithm
- Payload: ข้อมูลที่ต้องการเก็บ (user ID, role, etc.)
- Signature: ลายเซ็นเพื่อตรวจสอบความถูกต้อง
💻 โค้ดจริงจาก QUOM App
1. Register Endpoint (สมัครสมาชิก)
ไฟล์: backend/src/routes/auth.js
import { Router } from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { z } from "zod";
import { User } from "../models/User.js";
export const authRouter = Router();
authRouter.post("/register", async (req, res) => {
// 1. Validate input data
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
displayName: z.string().optional()
});
const data = schema.parse(req.body);
// 2. Check if email already exists
const exists = await User.findOne({ email: data.email });
if (exists) {
return res.status(409).json({
ok: false,
message: "Email already used"
});
}
// 3. Hash password
const passwordHash = await bcrypt.hash(data.password, 10);
// 4. Create user
const user = await User.create({
email: data.email,
passwordHash,
displayName: data.displayName || ""
});
// 5. Generate JWT token
const token = jwt.sign(
{
_id: user._id.toString(),
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "30d" }
);
// 6. Send response
res.json({
ok: true,
token,
user: {
_id: user._id,
email: user.email,
displayName: user.displayName,
role: user.role,
profile: user.profile
}
});
});
อธิบายทีละบรรทัด (Imports):
-
import { Router } from "express"- นำเข้า Router เพื่อแยกไฟล์ routing (ไม่ให้ code รกใน
server.js)
- นำเข้า Router เพื่อแยกไฟล์ routing (ไม่ให้ code รกใน
-
import bcrypt from "bcrypt"- Library สำหรับเข้ารหัสรหัสผ่าน (Hashing) ที่มีความปลอดภัยสูง
-
import jwt from "jsonwebtoken"- ใช้สร้าง (Sign) และตรวจสอบ (Verify) Token สำหรับยืนยันตัวตน
-
import { z } from "zod"- Library สำหรับ Validate ข้อมูล (ตรวจสอบว่า email ถูก pattern ไหม, password ครบกี่ตัว)
-
import { User } from "../models/User.js"- เรียกใช้ User Model เพื่อติดต่อกับฐานข้อมูล
อธิบาย Step-by-Step:
Step 1: Validate Input Data
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
displayName: z.string().optional()
});
const data = schema.parse(req.body);
คำอธิบาย:
- ใช้ Zod library สำหรับ validation
z.string().email(): ตรวจสอบว่าเป็น email formatz.string().min(6): รหัสผ่านต้องยาวอย่างน้อย 6 ตัวอักษรz.string().optional(): displayName ไม่บังคับschema.parse(): ถ้า validation ผ่าน จะ return ข้อมูล, ถ้าไม่ผ่านจะ throw error
Step 2: Check Duplicate Email
const exists = await User.findOne({ email: data.email });
if (exists) {
return res.status(409).json({
ok: false,
message: "Email already used"
});
}
คำอธิบาย:
- หา User ที่มี email เดียวกัน
- ถ้าเจอ return status 409 (Conflict)
- ป้องกันการสมัครซ้ำ
Step 3: Hash Password
const passwordHash = await bcrypt.hash(data.password, 10);
คำอธิบาย:
bcrypt.hash(password, saltRounds): hash รหัสผ่านsaltRounds = 10: จำนวนรอบในการ hash (ยิ่งมากยิ่งปลอดภัย แต่ช้า)- ตัวอย่าง output:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
Step 4: Create User
const user = await User.create({
email: data.email,
passwordHash,
displayName: data.displayName || ""
});
คำอธิบาย:
- สร้าง User ใหม่ใน Database
- เก็บ
passwordHashแทนpassword - ถ้าไม่มี displayName ใช้ empty string
Step 5: Generate JWT Token
const token = jwt.sign(
{
_id: user._id.toString(),
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "30d" }
);
คำอธิบาย:
jwt.sign(payload, secret, options): สร้าง JWT token- Payload: ข้อมูลที่ต้องการเก็บใน token (user ID, email, role)
- Secret: รหัสลับสำหรับเซ็น token (เก็บใน .env)
- expiresIn: token หมดอายุใน 30 วัน
⚠️ ข้อควรระวัง:
- ห้ามเก็บข้อมูลลับใน payload (เช่น password)
- ห้ามเก็บข้อมูลมากเกินไป (token จะใหญ่)
Step 6: Send Response
res.json({
ok: true,
token,
user: {
_id: user._id,
email: user.email,
displayName: user.displayName,
role: user.role,
profile: user.profile
}
});
คำอธิบาย:
- ส่ง token และข้อมูล user กลับไป
- Frontend จะเก็บ token ไว้ใช้ในการเรียก API ครั้งต่อไป
2. Login Endpoint (เข้าสู่ระบบ)
authRouter.post("/login", async (req, res) => {
try {
// 1. Validate input
const schema = z.object({
email: z.string().email(),
password: z.string()
});
const data = schema.parse(req.body);
// 2. Find user by email
const user = await User.findOne({ email: data.email });
if (!user) {
return res.status(401).json({
ok: false,
message: "Invalid credentials"
});
}
// 3. Verify password
const ok = await bcrypt.compare(data.password, user.passwordHash);
if (!ok) {
return res.status(401).json({
ok: false,
message: "Invalid credentials"
});
}
// 4. Check JWT_SECRET
if (!process.env.JWT_SECRET) {
console.error("JWT_SECRET is not defined");
return res.status(500).json({
ok: false,
message: "Server configuration error"
});
}
// 5. Generate token
const token = jwt.sign(
{
_id: user._id.toString(),
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: "30d" }
);
// 6. Send response
res.json({
ok: true,
token,
user: {
_id: user._id,
email: user.email,
displayName: user.displayName,
role: user.role,
profile: user.profile
}
});
} catch (error) {
console.error("Login error:", error);
if (error.name === 'ZodError') {
return res.status(400).json({
ok: false,
message: "Invalid input data"
});
}
res.status(500).json({
ok: false,
message: "Login failed. Please try again."
});
}
});
อธิบายส่วนสำคัญ:
Password Verification
const ok = await bcrypt.compare(data.password, user.passwordHash);
คำอธิบาย:
bcrypt.compare(plainPassword, hash): เปรียบเทียบรหัสผ่าน- return
trueถ้าตรงกัน,falseถ้าไม่ตรง - ไม่สามารถ decrypt hash กลับเป็น plain text ได้
Error Handling
try {
// ... code
} catch (error) {
if (error.name === 'ZodError') {
return res.status(400).json({
ok: false,
message: "Invalid input data"
});
}
res.status(500).json({
ok: false,
message: "Login failed. Please try again."
});
}
คำอธิบาย:
- จับ errors ทั้งหมด
- ถ้าเป็น Zod validation error → status 400
- ถ้าเป็น error อื่น → status 500
3. Authentication Middleware
ไฟล์: backend/src/middleware/auth.js
import jwt from "jsonwebtoken";
export function auth(req, res, next) {
try {
// 1. Get token from header
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
ok: false,
message: "No token provided"
});
}
// 2. Extract token (format: "Bearer <token>")
const token = authHeader.startsWith("Bearer ")
? authHeader.slice(7)
: authHeader;
// 3. Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 4. Attach user info to request
req.user = decoded;
// 5. Continue to next middleware/route
next();
} catch (error) {
if (error.name === "TokenExpiredError") {
return res.status(401).json({
ok: false,
message: "Token expired"
});
}
return res.status(401).json({
ok: false,
message: "Invalid token"
});
}
}
อธิบาย Step-by-Step:
Step 1: Get Token from Header
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
ok: false,
message: "No token provided"
});
}
คำอธิบาย:
- อ่าน
Authorizationheader - ตัวอย่าง:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Step 2: Extract Token
const token = authHeader.startsWith("Bearer ")
? authHeader.slice(7)
: authHeader;
คำอธิบาย:
- ถ้าขึ้นต้นด้วย "Bearer " ให้ตัดออก (เหลือแค่ token)
slice(7): ตัดตั้งแต่ตัวอักษรที่ 7 เป็นต้นไป
Step 3: Verify Token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
คำอธิบาย:
jwt.verify(token, secret): ตรวจสอบ token- ถ้า token ถูกต้อง จะ return payload
- ถ้า token ไม่ถูกต้องหรือหมดอายุ จะ throw error
ตัวอย่าง decoded:
{
_id: "65a1b2c3d4e5f6g7h8i9j0k1",
email: "user@example.com",
role: "user",
iat: 1705747200, // issued at
exp: 1708339200 // expiration
}
Step 4: Attach to Request
req.user = decoded;
คำอธิบาย:
- เก็บข้อมูล user ไว้ใน
req.user - Route handlers สามารถเข้าถึง
req.userได้
Step 5: Continue
next();
คำอธิบาย:
- เรียก middleware หรือ route handler ถัดไป
4. การใช้งาน Auth Middleware
import { auth } from "./middleware/auth.js";
// Protected route (ต้อง login)
app.get("/api/profile", auth, async (req, res) => {
const user = await User.findById(req.user._id);
res.json({ user });
});
// Public route (ไม่ต้อง login)
app.get("/api/public", (req, res) => {
res.json({ message: "Public data" });
});
🔄 Authentication Flow Diagram
┌─────────────┐
│ Client │
└──────┬──────┘
│
│ 1. POST /api/auth/register
│ { email, password }
▼
┌─────────────────────┐
│ Auth Router │
│ ┌──────────────┐ │
│ │ Validate │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Hash Password│ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Create User │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Generate JWT │ │
│ └──────┬───────┘ │
└─────────┼───────────┘
│
│ 2. Return { token, user }
▼
┌─────────────┐
│ Client │
│ Store token │
└──────┬──────┘
│
│ 3. GET /api/profile
│ Authorization: Bearer <token>
▼
┌─────────────────────┐
│ Auth Middleware │
│ ┌──────────────┐ │
│ │ Verify Token │ │
│ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Attach User │ │
│ └──────┬───────┘ │
└─────────┼───────────┘
│
│ 4. Continue to route
▼
┌─────────────────────┐
│ Route Handler │
│ Access req.user │
└─────────┬───────────┘
│
│ 5. Return user data
▼
┌─────────────┐
│ Client │
└─────────────┘
🛡️ Security Best Practices
1. Password Security
// ✅ ดี
const passwordHash = await bcrypt.hash(password, 10);
// ❌ อันตราย
const passwordHash = password; // ห้ามเก็บ plain text!
2. JWT Secret
// ✅ ดี - เก็บใน .env
JWT_SECRET=your-super-secret-key-min-32-chars
// ❌ อันตราย - hard-code ในโค้ด
const secret = "123456";
3. Token Expiration
// ✅ ดี - กำหนดเวลาหมดอายุ
jwt.sign(payload, secret, { expiresIn: "30d" });
// ❌ อันตราย - ไม่หมดอายุ
jwt.sign(payload, secret);
4. Error Messages
// ✅ ดี - ข้อความทั่วไป
return res.status(401).json({ message: "Invalid credentials" });
// ❌ อันตราย - เปิดเผยข้อมูล
return res.status(401).json({ message: "Password is incorrect" });
// ↑ Hacker รู้ว่า email ถูกต้อง
📝 แบบฝึกหัด
คำถามทบทวน
- ทำไมต้อง hash รหัสผ่าน?
- JWT ประกอบด้วยส่วนอะไรบ้าง?
bcrypt.compare()ทำงานอย่างไร?- Middleware
authทำหน้าที่อะไร? - ทำไมต้องกำหนด
expiresInให้ JWT?
โจทย์ปฏิบัติ
โจทย์ที่ 1: สร้าง Logout Endpoint
// TODO: สร้าง POST /api/auth/logout
// Hint: JWT เป็น stateless, ต้องให้ client ลบ token
โจทย์ที่ 2: สร้าง Change Password Endpoint
// TODO: สร้าง POST /api/auth/change-password
// Input: { oldPassword, newPassword }
// 1. Verify old password
// 2. Hash new password
// 3. Update user
โจทย์ที่ 3: สร้าง Admin Middleware
// TODO: สร้าง middleware ที่ตรวจสอบว่า user เป็น admin
// ใช้หลัง auth middleware
// ตรวจสอบ req.user.role === "admin"
📌 สรุป
ในบทนี้เราได้เรียนรู้:
✅ bcrypt ใช้ hash รหัสผ่านเพื่อความปลอดภัย
✅ JWT ใช้สำหรับ stateless authentication
✅ Register สร้าง user ใหม่และ generate token
✅ Login ตรวจสอบ credentials และ generate token
✅ Auth Middleware ป้องกัน routes ที่ต้อง login
✅ Security Best Practices ป้องกันช่องโหว่ด้านความปลอดภัย
ไฟล์ที่เกี่ยวข้อง
backend/src/routes/auth.js- Auth routesbackend/src/middleware/auth.js- Auth middlewarebackend/src/models/User.js- User model
🔜 บทถัดไป
บทที่ 4: API Routes และ CRUD Operations
เราจะเรียนรู้การสร้าง RESTful API สำหรับจัดการข้อมูล Notes
📚 แหล่งเรียนรู้เพิ่มเติม
- JWT.io - JWT Debugger
- bcrypt Documentation
- OWASP Authentication Cheat Sheet