DevToolBoxฟรี
บล็อก

ความปลอดภัย JWT: การเลือก Algorithm, Expiry, Rotation และการป้องกันการโจมตี

14 นาทีโดย DevToolBox

แนวปฏิบัติที่ดีที่สุดด้านความปลอดภัย JWT

JSON Web Token อยู่ทุกหนแห่ง — แต่การใช้งานส่วนใหญ่มีช่องโหว่ด้านความปลอดภัยที่สำคัญ คู่มือนี้ครอบคลุม การเลือกอัลกอริทึม, การหมดอายุและการหมุนเวียน Token และการป้องกันการโจมตี JWT หลัก

JWT คืออะไรและทำงานอย่างไร?

JWT คือสตริงที่เข้ารหัส Base64URL โดยมีสามส่วนที่คั่นด้วยจุด: header (อัลกอริทึม + ประเภท), payload (claims) และลายเซ็น

การเลือกอัลกอริทึม: HS256 vs RS256 vs ES256

ข้อผิดพลาดด้านความปลอดภัย JWT ที่พบบ่อยที่สุดคือการใช้ HS256 ในระบบแบบกระจาย RS256 และ ES256 ใช้คีย์แบบอสมมาตร

// Anatomy of a JWT — Header.Payload.Signature

// 1. Header (Base64URL decoded)
const header = {
  alg: "RS256",   // Algorithm: RS256 (asymmetric) preferred over HS256 (symmetric)
  typ: "JWT"
};

// 2. Payload (Base64URL decoded) — the claims
const payload = {
  iss: "https://auth.example.com",         // Issuer
  sub: "user_01HXYZ123",                  // Subject (user ID)
  aud: "https://api.example.com",          // Audience (target service)
  exp: 1735689600,                         // Expiry: Unix timestamp (15 min from now)
  iat: 1735688700,                         // Issued at
  jti: "8f14e0f9-21af-4c1e-8f26-3b4a5c6d7e8f", // Unique token ID (for revocation)
  roles: ["user"],                         // Custom claims
  email: "alice@example.com"
};

// 3. Signature (server-side only)
// RS256: sign(base64url(header) + '.' + base64url(payload), PRIVATE_KEY)
// Verification: verify(token, PUBLIC_KEY) — other services only need the public key

// --- Generating a JWT with jsonwebtoken (Node.js) ---
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
import { randomUUID } from 'crypto';

const privateKey = readFileSync('./private.pem');   // RSA private key
const publicKey  = readFileSync('./public.pem');    // RSA public key (shared with all services)

function issueAccessToken(userId: string, roles: string[]): string {
  return jwt.sign(
    {
      sub: userId,
      aud: 'https://api.example.com',
      roles,
      jti: randomUUID(),
    },
    privateKey,
    {
      algorithm: 'RS256',
      issuer: 'https://auth.example.com',
      expiresIn: '15m',  // Short-lived access token
    }
  );
}

function verifyAccessToken(token: string) {
  return jwt.verify(token, publicKey, {
    algorithms: ['RS256'],           // CRITICAL: whitelist algorithms — never allow 'none'
    issuer: 'https://auth.example.com',
    audience: 'https://api.example.com',
  });
}

กลยุทธ์การหมดอายุและการหมุนเวียน Token

Access token อายุสั้น (15 นาที) ร่วมกับ refresh token อายุยาว (7-30 วัน) คือมาตรฐานอุตสาหกรรม

// Refresh Token Pattern — Stateless Access + Stateful Refresh

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

// ---- Token issuance ----
async function issueTokenPair(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, jti: randomUUID() },
    process.env.JWT_PRIVATE_KEY!,
    { algorithm: 'RS256', expiresIn: '15m' }
  );

  // Refresh token: random bytes stored in DB — NOT a JWT
  const refreshToken = randomBytes(64).toString('hex');
  const refreshExpiry = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days

  // Store hashed refresh token in DB (never store plaintext)
  await db.refreshTokens.create({
    tokenHash: sha256(refreshToken),
    userId,
    expiresAt: refreshExpiry,
    createdAt: new Date(),
  });

  return { accessToken, refreshToken, refreshExpiry };
}

// ---- Token refresh endpoint ----
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies; // httpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const stored = await db.refreshTokens.findOne({
    tokenHash: sha256(refreshToken),
    expiresAt: { >: new Date() }, // Not expired
    revokedAt: null,                  // Not revoked
  });

  if (!stored) return res.status(401).json({ error: 'Invalid or expired refresh token' });

  // Rotate: invalidate old, issue new pair (refresh token rotation)
  await db.refreshTokens.update(
    { id: stored.id },
    { revokedAt: new Date() }         // Revoke old refresh token
  );

  const newPair = await issueTokenPair(stored.userId);

  // Set new refresh token as httpOnly cookie
  res.cookie('refreshToken', newPair.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    expires: newPair.refreshExpiry,
    path: '/auth/refresh',            // Scope cookie to refresh endpoint only
  });

  return res.json({ accessToken: newPair.accessToken });
});

// ---- Logout: revoke refresh token ----
app.post('/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  if (refreshToken) {
    await db.refreshTokens.update(
      { tokenHash: sha256(refreshToken) },
      { revokedAt: new Date() }
    );
  }
  res.clearCookie('refreshToken', { path: '/auth/refresh' });
  return res.json({ success: true });
});

การโจมตี JWT ทั่วไปและการป้องกัน

ช่องโหว่ JWT ที่อันตรายที่สุด: ความสับสนของอัลกอริทึม, ความสับสน RS256→HS256, ความลับที่อ่อนแอ และการขาดการตรวจสอบ exp

// JWT Attack Defenses — Code Patterns

// ---- ATTACK 1: Algorithm confusion ("none" bypass) ----
// VULNERABLE: never use verify() without specifying allowed algorithms
const VULNERABLE = jwt.verify(token, secret); // attacker can set alg: "none"

// SAFE: always whitelist algorithms
const SAFE = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Never include 'none', 'HS256' if you're using RS256
});

// ---- ATTACK 2: RS256 > HS256 confusion ----
// Attacker changes alg from RS256 to HS256 and signs with the PUBLIC key as the secret
// Defense: always specify algorithm on verify — never infer from the token header
// VULNERABLE:
const header = JSON.parse(atob(token.split('.')[0]));
jwt.verify(token, publicKey, { algorithms: [header.alg] }); // attacker controls this!

// SAFE:
jwt.verify(token, publicKey, { algorithms: ['RS256'] }); // hardcoded

// ---- ATTACK 3: Weak HS256 secrets (brute-force) ----
// VULNERABLE: short or predictable secrets
const BAD_SECRET = 'secret';
const BAD_SECRET2 = 'mysupersecretkey';

// SAFE: 256-bit cryptographically random secret
import { randomBytes } from 'crypto';
const GOOD_SECRET = randomBytes(32).toString('hex'); // 256-bit
// Store in environment variable, rotate annually

// ---- ATTACK 4: Missing expiry validation ----
// Some older libraries skip exp check by default
// SAFE: always validate exp (jsonwebtoken does this by default)
try {
  const decoded = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    clockTolerance: 30, // Allow 30 seconds clock skew max
    ignoreExpiration: false, // NEVER set this to true in production
  });
} catch (err) {
  if (err instanceof jwt.TokenExpiredError) {
    return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
  }
  return res.status(401).json({ error: 'Invalid token' });
}

// ---- Token blacklist (for early revocation via jti) ----
import { createClient } from 'redis';
const redis = createClient();

async function revokeToken(jti: string, exp: number) {
  const ttl = exp - Math.floor(Date.now() / 1000); // seconds until natural expiry
  if (ttl > 0) {
    await redis.set(`jwt:revoked:${jti}`, '1', { EX: ttl });
  }
}

async function isTokenRevoked(jti: string): Promise<boolean> {
  return (await redis.get(`jwt:revoked:${jti}`)) !== null;
}

// Middleware: check blacklist
app.use(async (req, res, next) => {
  const decoded = jwt.verify(req.headers.authorization?.split(' ')[1] ?? '', publicKey, {
    algorithms: ['RS256'],
  });
  if (await isTokenRevoked((decoded as jwt.JwtPayload).jti ?? '')) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  next();
});

การจัดเก็บ Token ที่ปลอดภัย

Access token: เก็บไว้ในหน่วยความจำ Refresh token: เก็บไว้ใน cookie httpOnly, Secure, SameSite=Strict

คำถามที่พบบ่อย

ควรใช้ JWT หรือ opaque session token?

ใช้ opaque session token สำหรับแอปที่เรนเดอร์ฝั่งเซิร์ฟเวอร์ ใช้ JWT สำหรับ microservices หรือ API ที่ต้องการการตรวจสอบแบบ stateless

วิธีเพิกถอน JWT ก่อนหมดอายุ?

วิธีแก้: อายุสั้น (15 นาที), รายการดำ token ใน Redis หรือเวอร์ชัน token ในบันทึกผู้ใช้

JWT ควรมี claims อะไรบ้าง?

จำเป็น: iss, sub, aud, exp, iat, jti ตรวจสอบ iss และ aud บนบริการที่รับเสมอ

ความลับ JWT และคีย์ส่วนตัวควรยาวแค่ไหน?

ความลับ HS256: ขั้นต่ำ 256 บิต RS256: คีย์ RSA ขั้นต่ำ 2048 บิต ES256: คีย์ elliptic curve 256 บิต

เครื่องมือที่เกี่ยวข้อง

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

JWTJWT Decoder→BBase64 Decoder{ }JSON Formatter

บทความที่เกี่ยวข้อง

การยืนยันตัวตน JWT: คู่มือการใช้งานฉบับสมบูรณ์

สร้างระบบยืนยันตัวตน JWT ตั้งแต่เริ่มต้น โครงสร้างโทเค็น, access และ refresh token, การใช้งาน Node.js, การจัดการฝั่งไคลเอนต์, แนวทางปฏิบัติด้านความปลอดภัย และ Next.js middleware

คู่มือสมบูรณ์ HTTP Headers

คู่มือสมบูรณ์ HTTP headers: request, response และ security headers

คู่มือออกแบบ REST API: แนวปฏิบัติที่ดีที่สุดสำหรับ 2026

ออกแบบ REST API ที่แข็งแกร่ง: การตั้งชื่อ resource, HTTP methods, pagination, การจัดการ error, versioning และ authentication

JWT Decoder Online: Decode, Inspect & Debug JSON Web Tokens (2026 Guide)

Use our free JWT decoder online to instantly inspect JWT headers, payloads, and claims. Covers JWT structure, standard claims, decoding in JavaScript, Python, Go, Java, signing algorithms, security best practices, and common mistakes.