Back

พื้นฐานการยืนยันตัวตนของ RESTful API ด้วย JWTBlur image

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

  1. Header (xxxxx)

    • ส่วนนี้จะเก็บข้อมูลเกี่ยวกับตัว Token เอง เช่น ประเภทของ Token (typ: “JWT”) และ Algorithm ที่ใช้ในการเข้ารหัส (alg: “HS256”)
    • ตัวอย่าง:
      {
        "alg": "HS256",
        "typ": "JWT"
      }
      json
  2. Payload (yyyyy)

    • ส่วนนี้บรรจุ “Claims” หรือข้อมูลของผู้ใช้ (User Data) และข้อมูลอื่นๆ ที่เกี่ยวข้อง
    • Claims ทั่วไปที่มักพบได้แก่:
      • sub (Subject): ID ของผู้ใช้
      • name: ชื่อผู้ใช้
      • exp (Expiration Time): เวลาหมดอายุของ Token
      • roles (หรือ admin): สิทธิ์การใช้งานของผู้ใช้
    • ตัวอย่าง:
      {
        "sub": "1234567890",
        "name": "John Doe",
        "admin": true
      }
      json
  3. 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
bash

3.2. ติดตั้ง Dependencies#

ติดตั้งแพ็กเกจที่จำเป็นสำหรับการสร้าง API ด้วย Express และการจัดการ JWT:

npm install express jsonwebtoken cors body-parser
bash
  • express: เฟรมเวิร์กสำหรับสร้าง RESTful API
  • jsonwebtoken: ไลบรารีสำหรับสร้างและตรวจสอบ JWT
  • cors: มิดเดิลแวร์สำหรับเปิดใช้งานการร้องขอแบบ 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"
plaintext

4. ขั้นตอนการยืนยันตัวตนด้วย 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):
    {
      "username": "user",
      "password": "password"
    }
    json
  • Response: ควรได้รับ accessToken และ refreshToken

ตัวอย่างการทดสอบ /login ด้วย POSTMAN ตัวอย่างการทดสอบ /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 ตัวอย่างการทดสอบ /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):
    {
      "refreshToken": "<refreshToken ที่ได้จาก /login>"
    }
    json
  • Response: accessToken ใหม่

ตัวอย่างการทดสอบ /token ด้วย POSTMAN ตัวอย่างการทดสอบ /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):
    {
      "refreshToken": "<refreshToken ที่ได้จาก /login>"
    }
    json

ตัวอย่างการทดสอบ /logout ด้วย POSTMAN ตัวอย่างการทดสอบ /logout ด้วย POSTMAN

ตัวอย่างการทดสอบ /token หลังจาก logout แล้ว ตัวอย่างการทดสอบ /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 และการนำไปใช้อย่างถูกต้องจะช่วยให้การสร้างระบบยืนยันตัวตนที่ปลอดภัย

พื้นฐานการยืนยันตัวตนของ RESTful API ด้วย JWT
ผู้เขียน กานต์ ยงศิริวิทย์ / Karn Yongsiriwit
เผยแพร่เมื่อ July 17, 2025
ลิขสิทธิ์ CC BY-NC-SA 4.0

กำลังโหลดความคิดเห็น...

ความคิดเห็น 0