ویژگی تصویر

ساخت API احراز هویت با JWT — راهنمای کامل

  /  Node.js   /  ساخت API احراز هویت JWT
بنر تبلیغاتی الف
NodeJS - Node.js

JSON Web Token (JWT) یکی از محبوب‌ترین روش‌ها برای احراز هویت و مدیریت نشست‌ها در معماری‌های مدرن است. در این مقاله به صورت گام‌به‌گام، اصول، معماری، بهترین شیوه‌ها و یک پیاده‌سازی نمونه با Node.js/Express را بررسی می‌کنیم تا بتوانید یک API احراز هویت امن و مقیاس‌پذیر بسازید.

چرا JWT؟ مزایا و محدودیت‌ها

  • عدم نیاز به ذخیره نشست در سرور — مقیاس‌پذیری بهتر.
  • قابل حمل بین سرویس‌ها — مناسب برای میکروسرویس‌ها.
  • قابلیت افزودن ادعاها (claims) برای اطلاعات کاربر.
  • محدودیت: در صورت لو رفتن توکن، تا انقضای آن قابل استفاده است — نیاز به مکانیسم ری‌ووک یا توکن رفرش.

ساختار JWT

قسمتتوضیح
Headerاطلاعات الگوریتم (مثلاً HS256/RS256) و نوع توکن
Payloadادعاها (claims) شامل sub, iat, exp و اطلاعات دلخواه
Signatureامضای پایه64 رمز‌شده برای اعتبارسنجی توکن

الگوریتم‌ها: HS256 در برابر RS256

HS256 (کلید مشترک) ساده و سریع است اما اشتراک کلید بین سرویس‌ها می‌تواند مشکل‌زا باشد. RS256 (امضای نامتقارن) از کلید خصوصی برای امضا و کلید عمومی برای تایید استفاده می‌کند — امن‌تر در محیط‌های توزیع‌شده.

نمونه پیاده‌سازی با Node.js و Express

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET;
let refreshTokensStore = []; // در عمل از DB استفاده کنید

// تولید توکن
function generateAccessToken(user) {
  return jwt.sign({ sub: user.id, role: user.role }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
}

function generateRefreshToken(user) {
  return jwt.sign({ sub: user.id }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
}

// ورود کاربر
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  // یافتن کاربر در DB (نمونه فرضی)
  const user = await findUserByUsername(username);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });

  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);
  refreshTokensStore.push(refreshToken);

  res.json({ accessToken, refreshToken });
});

// رفرش توکن
app.post('/token', (req, res) => {
  const { token } = req.body;
  if (!token) return res.status(401).send('No token provided');
  if (!refreshTokensStore.includes(token)) return res.status(403).send('Forbidden');

  jwt.verify(token, REFRESH_TOKEN_SECRET, (err, payload) => {
    if (err) return res.status(403).send('Forbidden');
    const user = { id: payload.sub }; // می‌توانید از DB نیز بارگذاری کنید
    const newAccessToken = generateAccessToken(user);
    res.json({ accessToken: newAccessToken });
  });
});

// middleware بررسی توکن دسترسی
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'Protected data', user: req.user });
});

app.listen(3000);

توضیح: این نمونه نشان می‌دهد چگونه توکن دسترسی و رفرش ساخته می‌شوند، رفرش توکن استفاده می‌شود و middleware برای محافظت از مسیرها به کار می‌رود. توجه کنید که برای سادگی از آرایه برای ذخیره رفرش توکن استفاده شده؛ در عمل باید از دیتابیس یا کش استفاده کنید.

بهترین شیوه‌ها و بهینه‌سازی‌ها

  • ذخیره توکن رفرش در DB یا Redis برای امکان ری‌ووک و ردیابی.
  • استفاده از httpOnly و secure cookie برای نگهداری رفرش توکن در مرورگر (کاهش خطر XSS).
  • استفاده از RS256 و مدیریت کلید‌های خصوصی/عمومی با KMS یا Vault برای امنیت بالاتر.
  • چرخش توکن رفرش (rotating refresh tokens): در هر بار رفرش، توکن قدیمی را با توکن جدید جایگزین و قدیمی را باطل کنید.
  • تنظیم زمان انقضای کوتاه برای access token و بلندتر برای refresh token.
  • استفاده از claimهای استاندارد: iss, sub, aud, iat, exp برای کنترل بهتر.

نمونه بهبود: استفاده از RS256 (کلید نامتقارن)

// sign with private key
const fs = require('fs');
const privateKey = fs.readFileSync('private.pem', 'utf8');
const publicKey = fs.readFileSync('public.pem', 'utf8');

function generateAccessTokenRSA(user) {
  return jwt.sign({ sub: user.id }, privateKey, { algorithm: 'RS256', expiresIn: '15m' });
}

function verifyAccessTokenRSA(token, cb) {
  jwt.verify(token, publicKey, { algorithms: ['RS256'] }, cb);
}

توضیح: در این بهینه‌سازی از کلید خصوصی برای امضا و کلید عمومی برای اعتبارسنجی استفاده شده که امنیت را در معماری توزیع‌شده افزایش می‌دهد. کلیدها باید در مکان امن (مثلاً HashiCorp Vault یا AWS KMS) نگهداری شوند.

مقابله با تهدیدات رایج

  • XSS: نگهداری توکن‌ها در httpOnly cookie و حذف نگهداری در localStorage.
  • CSRF: هنگام استفاده از کوکی، از token-based CSRF protection یا SameSite=strict بهره ببرید.
  • Replay attacks: استفاده از nonce یا جستجوی توکن در لیست سیاه هنگام لزوم.
  • کلیدهای ضعیف: استفاده از کلیدهای قوی و چرخش دوره‌ای کلیدها.

جمع‌بندی و چک‌لیست پیاده‌سازی

  • تعریف واضح زمان انقضا برای access و refresh token.
  • ذخیره امن refresh token و امکان ری‌ووک.
  • استفاده از کلیدهای نامتقارن در سرویس‌های توزیع‌شده.
  • پیاده‌سازی middleware برای بررسی توکن و مدیریت خطاها.
  • استفاده از HTTPS، سیاست‌های CORS مناسب و محافظت در برابر XSS/CSRF.

با پیروی از این اصول و انتخاب‌های طراحی مناسب می‌توانید یک API احراز هویت مبتنی بر JWT بسازید که هم امن و هم مقیاس‌پذیر باشد. در محیط‌های حساس، همیشه مدیریت کلیدها و ذخیره امن توکن‌ها را در اولویت قرار دهید.

آیا این مطلب برای شما مفید بود ؟

خیر
بله
موضوعات شما در انجمن: