

1. วัตถุประสงค์#
วัตถุประสงค์ของบทความนี้คือเพื่อนำเสนอ วิธีการสร้างระบบยืนยันตัวตนด้วย JWT (JSON Web Token) ในแอปพลิเคชันที่พัฒนาด้วย Node.js และ Express.js โดยจะครอบคลุมตั้งแต่การตั้งค่าโปรเจค การทำความเข้าใจโครงสร้างของ JWT และกลไกการทำงานของ JWT Flow ตั้งแต่เริ่มต้นจนจบกระบวนการ เพื่อให้ผู้อ่านสามารถนำไปประยุกต์ใช้ในการสร้าง API ที่มีความปลอดภัยเบื้องต้นได้
เพื่อความง่ายในการสาธิต Refresh Token จะถูกจัดเก็บไว้ในหน่วยความจำ (in-memory) แทนที่จะเป็นฐานข้อมูล ซึ่งเหมาะสำหรับการเรียนรู้เบื้องต้นแต่ไม่แนะนำสำหรับการใช้งานจริงใน Production
2. JWT คืออะไร?#
JSON Web Token (JWT) คือวิธีการ Authentication (ยืนยันตัวตน) ที่ได้รับความนิยมอย่างมากในยุคของ API และ Web/Mobile Applications ด้วยคุณสมบัติที่สำคัญดังนี้:
- ขนาดเล็ก (Compact): เหมาะสำหรับส่งผ่านอินเทอร์เน็ตได้อย่างรวดเร็ว
- ปลอดภัยกับ URL (URL-safe): เนื่องจากใช้ Base64URL encoding ทำให้สามารถส่งผ่าน URL, POST data หรือ HTTP Header ได้อย่างปลอดภัย
- Stateless: Server ไม่จำเป็นต้องเก็บ Session ของผู้ใช้ เพราะข้อมูลที่จำเป็น (เช่น User ID, สิทธิ์การใช้งาน, วันหมดอายุ) จะถูกรวมอยู่ใน Token เรียบร้อยแล้ว
โครงสร้างของ JWT#
JWT แบ่งออกเป็น 3 ส่วนหลักๆ ซึ่งแต่ละส่วนจะถูก Base64URL encoded แล้วคั่นด้วยจุด (.
) ดังนี้:
xxxxx.yyyyy.zzzzz
-
Header (xxxxx)
- ส่วนนี้จะเก็บข้อมูลเกี่ยวกับตัว Token เอง เช่น ประเภทของ Token (
typ
: “JWT”) และ Algorithm ที่ใช้ในการเข้ารหัส (alg
: “HS256”) - ตัวอย่าง:
json{ "alg": "HS256", "typ": "JWT" }
- ส่วนนี้จะเก็บข้อมูลเกี่ยวกับตัว Token เอง เช่น ประเภทของ Token (
-
Payload (yyyyy)
- ส่วนนี้บรรจุ “Claims” หรือข้อมูลของผู้ใช้ (User Data) และข้อมูลอื่นๆ ที่เกี่ยวข้อง
- Claims ทั่วไปที่มักพบได้แก่:
sub
(Subject): ID ของผู้ใช้name
: ชื่อผู้ใช้exp
(Expiration Time): เวลาหมดอายุของ Tokenroles
(หรือadmin
): สิทธิ์การใช้งานของผู้ใช้
- ตัวอย่าง:
json{ "sub": "1234567890", "name": "John Doe", "admin": true }
-
Signature (zzzzz)
- ส่วนนี้มีไว้เพื่อ ป้องกันการแก้ไขข้อมูล ของ Header และ Payload
- Signature จะถูกสร้างขึ้นโดยการนำ Header และ Payload มารวมกัน แล้วเข้ารหัสด้วย Secret Key (สำหรับ HMAC) หรือ Private Key (สำหรับ RSA/ECDSA) ที่รู้กันเฉพาะระหว่าง Server เท่านั้น
- เมื่อ Server ได้รับ JWT ก็จะใช้ Secret Key/Public Key เดียวกันเพื่อสร้าง Signature ขึ้นมาใหม่ หาก Signature ที่คำนวณได้ไม่ตรงกับ Signature ที่ส่งมาใน Token ก็จะถือว่า Token นั้นไม่ถูกต้องหรือถูกแก้ไข
ดูรายละเอียดเพิ่มเติมได้ที่ https://jwt.io/ ↗
3. การตั้งค่าโปรเจค#
3.1. เริ่มต้นโปรเจค#
สร้างโฟลเดอร์สำหรับโปรเจคและเริ่มต้นโปรเจค Node.js โดยใช้คำสั่งเหล่านี้ใน Terminal:
mkdir jwt-auth-demo
cd jwt-auth-demo
npm init -y
bash3.2. ติดตั้ง Dependencies#
ติดตั้งแพ็กเกจที่จำเป็นสำหรับการสร้าง API ด้วย Express และการจัดการ JWT:
npm install express jsonwebtoken cors body-parser
bashexpress
: เฟรมเวิร์กสำหรับสร้าง RESTful APIjsonwebtoken
: ไลบรารีสำหรับสร้างและตรวจสอบ JWTcors
: มิดเดิลแวร์สำหรับเปิดใช้งานการร้องขอแบบ Cross-Origin (CORS)body-parser
: มิดเดิลแวร์สำหรับแยกวิเคราะห์ JSON body จาก Request เข้าสู่req.body
3.3. สร้าง index.js
(การตั้งค่าพื้นฐาน)#
สร้างไฟล์ index.js
ในโฟลเดอร์ root ของโปรเจค และเพิ่มโค้ดการตั้งค่า Express และมิดเดิลแวร์เริ่มต้น:
const express = require("express");
const jwt = require("jsonwebtoken");
const bodyParser = require("body-parser");
const cors = require("cors");
const app = express();
app.use(bodyParser.json()); // แยกวิเคราะห์ JSON bodies
app.use(cors()); // เปิดใช้งาน CORS สำหรับทุก requests
// Secret keys สำหรับการลงนามโทเค็น (ควรใช้ Environment Variables ใน Production)
const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_TOKEN_SECRET;
// อาร์เรย์สำหรับเก็บ Refresh Token (ชั่วคราว, สำหรับ Demo เท่านั้น)
let refreshTokens = [];
javascriptคำอธิบาย:
ACCESS_TOKEN_SECRET
และREFRESH_TOKEN_SECRET
: คีย์ลับเหล่านี้ใช้สำหรับลงนาม (signing) JWTs สำคัญมาก: ในสภาพแวดล้อม Production ควรเก็บค่าเหล่านี้ไว้ใน Environment Variables เพื่อความปลอดภัยrefreshTokens
: อาร์เรย์นี้ใช้เพื่อติดตาม Refresh Token ที่ยังคงใช้งานได้ เป็นการจัดเก็บแบบชั่วคราวในหน่วยความจำ ซึ่งหมายความว่าโทเค็นจะหายไปเมื่อเซิร์ฟเวอร์รีสตาร์ท
3.4. สร้าง .env
#
สร้างไฟล์ชื่อ .env
ในโฟลเดอร์ root ของโปรเจค เพื่อเก็บ Secret Keys:
JWT_ACCESS_TOKEN_SECRET="This is my access token secret"
JWT_REFRESH_TOKEN_SECRET="This is my refresh token secret"
plaintext4. ขั้นตอนการยืนยันตัวตนด้วย JWT#
4.1. ขั้นตอนที่ 1: Endpoint สำหรับเข้าสู่ระบบ (/login
)#
Endpoint นี้จะรับข้อมูลชื่อผู้ใช้และรหัสผ่านเพื่อตรวจสอบสิทธิ์ และหากสำเร็จ จะออก Access Token และ Refresh Token ให้แก่ผู้ใช้
app.post("/login", (req, res) => {
const { username, password } = req.body;
// ตรวจสอบข้อมูลรับรอง (การตรวจสอบแบบ Dummy สำหรับ Demo เท่านั้น)
if (username !== "user" || password !== "password") {
return res.status(401).json({ message: "Invalid credentials" });
}
const user = { username }; // ข้อมูลผู้ใช้ที่ต้องการเข้ารหัสใน Token
// สร้าง Access Token (หมดอายุใน 15 นาที)
const accessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
// สร้าง Refresh Token (อายุการใช้งานนานขึ้น, 7 วัน)
const refreshToken = jwt.sign(user, REFRESH_TOKEN_SECRET, { expiresIn: "7d" });
refreshTokens.push(refreshToken); // บันทึก Refresh Token ในหน่วยความจำ
res.json({ accessToken, refreshToken });
});
javascriptทำไมต้องใช้สองโทเค็น?
- Access Token: มีอายุสั้น (เช่น 15 นาที) ใช้สำหรับส่งไปพร้อมกับการร้องขอ API ที่ต้องการการยืนยันตัวตนทุกครั้ง หาก Access Token ถูกขโมย ความเสียหายจะจำกัดอยู่ในระยะเวลาสั้นๆ
- Refresh Token: มีอายุยาวนานกว่า (เช่น 7 วัน) ใช้เพื่อขอ Access Token ใหม่โดยไม่ต้องให้ผู้ใช้เข้าสู่ระบบซ้ำเมื่อ Access Token เดิมหมดอายุ การเก็บ Refresh Token แยกต่างหากช่วยเพิ่มความปลอดภัย
เริ่มต้น Server
เพิ่มโค้ดด้านล่างเพื่อเริ่ม Express Server:
const PORT = 4000;
app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
javascriptวิธีการเริ่ม Server:
รันคำสั่งนี้ใน Terminal เพื่อให้ Node.js โหลดไฟล์ .env
โดยอัตโนมัติ:
node --env-file=.env index.js
bashทดสอบ API Endpoint นี้ด้วย Postman:
- Method:
POST
- URL:
http://localhost:4000/login
- Body (JSON):
json{ "username": "user", "password": "password" }
- Response: ควรได้รับ
accessToken
และrefreshToken
ตัวอย่างการทดสอบ /login ด้วย POSTMAN
4.2. ขั้นตอนที่ 2: Middleware สำหรับ Protected Routes#
มิดเดิลแวร์นี้ใช้สำหรับปกป้อง API Routes โดยจะตรวจสอบและยืนยัน Access Token ที่ส่งมาพร้อมกับ Request
function authenticateToken(req, res, next) {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1]; // ดึง Token จาก "Bearer <token>"
if (!token) return res.status(401).json({ message: "no access token" }); // ไม่มี Token = ไม่อนุญาต
jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: err }); // Token ไม่ถูกต้อง/หมดอายุ = ห้ามเข้าถึง
req.user = user; // แนบข้อมูลผู้ใช้เข้ากับ Request
next(); // ดำเนินการไปยังมิดเดิลแวร์หรือ Route Handler ถัดไป
});
}
javascriptจุดสำคัญ:
- ตรวจสอบ
Authorization
Header เพื่อหาBearer
Token - ใช้
jwt.verify()
เพื่อตรวจสอบลายเซ็น (signature) และการหมดอายุ (expiry) ของ JWT - หาก Token ถูกต้อง ข้อมูลผู้ใช้ที่ถอดรหัสจาก Token จะถูกแนบไปกับอ็อบเจกต์
req
เพื่อให้ Route Handler สามารถเข้าถึงได้
4.3. ขั้นตอนที่ 3: Protected Route#
Route นี้จะสามารถเข้าถึงได้ก็ต่อเมื่อมี Access Token ที่ถูกต้องเท่านั้น
app.get("/protected", authenticateToken, (req, res) => {
res.json({ message: `Hello, ${req.user.username}! This is protected data.` });
});
javascriptวิธีการทำงาน:
- เมื่อมีการร้องขอไปยัง
/protected
มิดเดิลแวร์authenticateToken
จะถูกเรียกใช้งานก่อน - หาก
authenticateToken
ตรวจสอบแล้วว่า Access Token ถูกต้องและยังไม่หมดอายุ Request จะถูกส่งต่อไปยัง Route Handler และสามารถเข้าถึงreq.user
ที่มีข้อมูลผู้ใช้ได้
ทดสอบ API Endpoint นี้ด้วย Postman:
คัดลอก accessToken
ที่ได้จาก /login
- Method:
GET
- URL:
http://localhost:4000/protected
- Headers:
Authorization
:Bearer <YOUR_ACCESS_TOKEN>
(แทนที่<YOUR_ACCESS_TOKEN>
ด้วย Access Token ที่คัดลอกมา)
- Response: ข้อความตามภาพ โดยมีชื่อของ user ที่ดึงมาจาก
req.user.username
อยู่ในข้อความด้วย
ตัวอย่างการทดสอบ /protected ด้วย POSTMAN
4.4. ขั้นตอนที่ 4: Endpoint สำหรับ Refresh Token (/token
)#
Endpoint นี้อนุญาตให้ผู้ใช้ขอ Access Token ใหม่เมื่อ Access Token เดิมหมดอายุ โดยใช้ Refresh Token
app.post("/token", (req, res) => {
const { refreshToken } = req.body;
// ตรวจสอบว่ามี Refresh Token และ Refresh Token นั้นมีอยู่ในอาร์เรย์ของเราหรือไม่
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.status(403).json({ message: "Invalid Refresh Token" });
}
jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: err });
// สร้าง Access Token ใหม่
const accessToken = jwt.sign({ username: user.username }, ACCESS_TOKEN_SECRET, { expiresIn: "15m" });
res.json({ accessToken });
});
});
javascriptทดสอบ API Endpoint นี้ด้วย Postman:
คัดลอก refreshToken
ที่ได้จาก /login
- Method:
GET
- URL:
http://localhost:4000/token
- Body (JSON):
json{ "refreshToken": "<refreshToken ที่ได้จาก /login>" }
- Response:
accessToken
ใหม่
ตัวอย่างการทดสอบ /token ด้วย POSTMAN
ข้อควรจำ:
- Endpoint นี้จะตรวจสอบ Refresh Token หากถูกต้องและยังไม่หมดอายุ จะสร้าง Access Token ใหม่ให้
- ไม่จำเป็นต้องให้ผู้ใช้ป้อนชื่อผู้ใช้และรหัสผ่านซ้ำอีกครั้ง
4.5. ขั้นตอนที่ 5: Endpoint สำหรับ Logout (/logout
)#
Endpoint นี้ใช้สำหรับ “เพิกถอน” Refresh Token เพื่อป้องกันไม่ให้ถูกนำไปใช้ซ้ำหลังจากผู้ใช้ออกจากระบบ
app.post("/logout", (req, res) => {
const { refreshToken } = req.body;
// ลบ Refresh Token ออกจากอาร์เรย์ (ใน Production ควรลบออกจาก Database/Cache)
refreshTokens = refreshTokens.filter(token => token !== refreshToken);
res.sendStatus(204); // No Content
});
javascript- เพื่อป้องกันไม่ให้ Refresh Token ถูกนำกลับมาใช้เพื่อสร้าง Access Token ใหม่หลังจากที่ผู้ใช้ออกจากระบบไปแล้ว หากไม่ลบออก ผู้ใช้หรือผู้โจมตีที่ขโมย Refresh Token ไปยังสามารถสร้าง Access Token ใหม่ได้
- ใน Production, Refresh Token ควรถูกลบออกจากฐานข้อมูลหรือ Redis cache
ทดสอบ API Endpoint นี้ด้วย Postman:
คัดลอก refreshToken
ที่ได้จาก /login
- Method:
GET
- URL:
http://localhost:4000/logout
- Body (JSON):
json{ "refreshToken": "<refreshToken ที่ได้จาก /login>" }
ตัวอย่างการทดสอบ /logout ด้วย POSTMAN
ตัวอย่างการทดสอบ /token หลังจาก logout แล้ว
5. สรุปและแนวทางปฏิบัติที่ดี#
5.1. สรุป JWT Flow#
- Access Tokens: มีอายุสั้น เพื่อเพิ่มความปลอดภัย หากถูกขโมยจะใช้งานได้ไม่นาน
- Refresh Tokens: มีอายุยาวนานกว่า ช่วยให้ผู้ใช้สามารถรักษา Session ไว้ได้โดยไม่ต้องเข้าสู่ระบบบ่อยๆ
- การจัดเก็บในหน่วยความจำ (In-memory storage): เหมาะสำหรับ Demo และการเรียนรู้เท่านั้น ไม่แนะนำ สำหรับ Production เนื่องจากโทเค็นจะหายไปเมื่อเซิร์ฟเวอร์รีสตาร์ท
5.2. แนวทางปฏิบัติที่ดีที่สุด (Best Practices)#
- จัดเก็บ Refresh Token ใน HTTP-only Cookies: เพื่อเพิ่มความปลอดภัย ป้องกันการเข้าถึงจาก JavaScript บนฝั่ง Client ซึ่งช่วยลดความเสี่ยงจากการโจมตีแบบ XSS (Cross-Site Scripting)
- ใช้ Database หรือ Cache สำหรับ Refresh Token: ใน Production ควรเก็บ Refresh Token ในฐานข้อมูล (เช่น MongoDB, PostgreSQL) หรือ In-memory Cache (เช่น Redis) เพื่อให้สามารถจัดการ (เพิ่ม, ลบ, เพิกถอน) และคงอยู่แม้เซิร์ฟเวอร์จะรีสตาร์ท
- Implement Token Revocation: นอกจากการลบออกจากหน่วยความจำ ควรมีการจัดการ Blacklist ของ Token ที่ถูกเพิกถอนในระบบ Production
- HTTPS Everywhere: ใช้ HTTPS เสมอเพื่อเข้ารหัสการสื่อสารทั้งหมดระหว่าง Client และ Server เพื่อป้องกันการดักฟังข้อมูล
- Rotate Secret Keys: เปลี่ยน Secret Keys ที่ใช้ลงนาม Token เป็นประจำเพื่อเพิ่มความปลอดภัย
- Error Handling ที่แข็งแกร่ง: มีการจัดการข้อผิดพลาดที่ดีในทุก Endpoint และให้ข้อความที่ชัดเจนแก่ Client
การเข้าใจหลักการทำงานของ JWT และการนำไปใช้อย่างถูกต้องจะช่วยให้การสร้างระบบยืนยันตัวตนที่ปลอดภัย