پیاده سازی سیستم ورود دو مرحله ای در Node.js
ورود دو مرحلهای یا Two-Factor Authentication (2FA) یکی از مؤثرترین روشها برای افزایش امنیت حسابهای کاربری است. در این مقاله به زبان فارسی و با مثالهای عملی در محیط Node.js توضیح میدهیم چگونه یک 2FA مبتنی بر TOTP (اپلیکیشنهای احراز هویت مانند Google Authenticator یا Authy) را پیادهسازی و بهینهسازی کنید. این نوشته شامل کد نمونه، نکات امنیتی و پیشنهادات عملی برای تولید، ذخیره و تأیید توکنهاست.
چرا 2FA مهم است؟
- کاهش خطر دسترسی غیرمجاز حتی در صورت لو رفتن رمز عبور.
- محافظت در برابر حملات فیشینگ ساده و کوکیهای سرقتشده.
- افزایش اعتماد کاربران به سرویس شما.
روشهای رایج 2FA و معایب/مزایا
| روش | مزایا | معایب |
|---|---|---|
| TOTP (اپلیکیشن) | آفلاین، امن، بدون هزینه پیامکی | نیاز به نصب اپلیکیشن، آسیبپذیر در برابر فیشینگهای پیچیده |
| SMS | ساده و شناختهشده | قابلتزریق توسط SIM swap یا حملات شبکه |
| WebAuthn / FIDO2 | بالاترین امنیت، منع فیشینگ | پیچیدهتر برای پیادهسازی و پشتیبانی |
فلو و اجزای کلی پیادهسازی TOTP
- مرحلهٔ ثبتنام: تولید یک secret برای کاربر و نمایش QR
- مرحلهٔ فعالسازی: کاربر توکن تولیدشده توسط اپ را وارد کرده و سرور آن را بررسی میکند
- مرحلهٔ ورود: ابتدا اعتبارسنجی رمز عبور؛ سپس در صورت فعال بودن 2FA، درخواست توکن و بررسی آن
- مدیریت پشتیبان: کدهای بازیابی، امکان غیرفعالسازی پس از احراز هویت قوی
نمونه کد: تولید Secret و QR با speakeasy و qrcode
const express = require('express');
const speakeasy = require('speakeasy');
const qrcode = require('qrcode');
const app = express();
app.use(express.json());
app.post('/2fa/setup', (req, res) => {
// فرض کنید userId از احراز هویت اولیه در دسترس است
const secret = speakeasy.generateSecret({ length: 20, name: `MyApp (${req.body.email})` });
// ذخیرهی secret.base32 در دیتابیس (باید رمزنگاری شود)
// ارسال URL برای تولید QR به کلاینت
qrcode.toDataURL(secret.otpauth_url, (err, dataUrl) => {
if (err) return res.status(500).json({ error: 'QR generation failed' });
res.json({ secret: secret.base32, qr: dataUrl });
});
});توضیح: این نقطهٔ شروع برای تولید secret است. کتابخانه speakeasy یک secret جدید ساخته و otpauth URL را تولید میکند که میتوان با qrcode آن را به تصویر QR تبدیل کرد. متغیر secret.base32 را در دیتابیس ذخیره کنید، اما حتماً قبل از ذخیره رمزنگاری انجام دهید.
بررسی توکن برای فعالسازی 2FA
app.post('/2fa/verify', (req, res) => {
const { token, secret } = req.body;
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 1 // اجازه یک بازه زمانی جلو/عقب
});
if (verified) {
// بهروزرسانی وضعیت کاربر: 2FA فعال شد
return res.json({ success: true });
}
res.status(400).json({ error: 'Invalid token' });
});توضیح: این مسیر توکنی را که کاربر وارد کرده دریافت و با secret ذخیرهشده مقایسه میکند. پارامتر window برای کمی انعطاف زمانی (مثلاً تأخیر موبایل) استفاده میشود.
فلو ورود دو مرحلهای (Login + 2FA)
// 1) مسیر لاگین معمولی
app.post('/login', async (req, res) => {
// اعتبارسنجی نامکاربری/رمزعبور...
// اگر کاربر 2FA فعال داشته باشد:
// ایجاد جلسه موقت یا JWT با flag نیاز به 2FA و بازگرداندن وضعیت به کلاینت.
res.json({ need2FA: true, tempToken: '...' });
});
// 2) مسیر بررسی TOTP پس از لاگین
app.post('/login/2fa', (req, res) => {
const { tempToken, token } = req.body;
// اعتبارسنجی tempToken و بازیابی secret کاربر
const verified = speakeasy.totp.verify({ secret: userSecret, encoding: 'base32', token });
if (verified) {
// صدور نشست کامل یا JWT نهایی
return res.json({ success: true, authToken: '...' });
}
res.status(401).json({ error: 'Invalid 2FA token' });
});توضیح: در این الگو، پس از ورود اولیه که رمز عبور صحیح است، سیستم یک مرحلهٔ اضافی نیاز دارد. استفاده از یک tempToken امن باعث میشود فرآیند فلو بین صفحات قابل پیگیری باشد.
رمزنگاری Secrets و مدیریت امن
const crypto = require('crypto');
const ALGO = 'aes-256-gcm';
function encrypt(text, key) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(ALGO, key, iv);
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString('base64');
}
function decrypt(enc, key) {
const data = Buffer.from(enc, 'base64');
const iv = data.slice(0, 12);
const tag = data.slice(12, 28);
const encrypted = data.slice(28);
const decipher = crypto.createDecipheriv(ALGO, key, iv);
decipher.setAuthTag(tag);
return decipher.update(encrypted, null, 'utf8') + decipher.final('utf8');
}توضیح: این توابع AES-GCM را برای رمزنگاری secret در دیتابیس نشان میدهند. کلید رمزنگاری نباید در کد هاردکد شود؛ از KMS (مانند AWS KMS) یا متغیرهای محیطی محافظتشده استفاده کنید.
نکات عملی و توصیههای امنیتی
- از rate limiting برای مسیرهای بررسی توکن استفاده کنید (مثلاً express-rate-limit).
- پشتیبانی از کدهای بازیابی یکبار مصرف برای مواقعی که دستگاه کاربر گمشده است.
- برای حساسترین عملیات، از WebAuthn/HWK (کلید سختافزاری) استفاده کنید.
- اگر از SMS استفاده میکنید، آگاه باشید که SIM swap یک تهدید است؛ SMS را بهعنوان آخرین راهحل در نظر بگیرید.
- لاگینگ و مانیتورینگ: تلاشهای ناموفق باید ثبت و در صورت الگوی غیرطبیعی هشدار داده شود.
- آموزش UX: کاربران را راهنمایی کنید چگونه اپلیکیشن احراز هویت را نصب و بکاپ بگیرند.
بهینهسازی و مقیاسپذیری
- کیفیت تجربه: ارائه QR و کد متنی در کنار هم برای دستگاههای مختلف.
- ذخیرهٔ secret بهصورت رمزنگاریشده و کلید رمزنگاری در KMS مرکزی.
- کشینگ موقت برای tempTokenها با زمان عمر کوتاه (مثلاً Redis).
- استفاده از سرویسهای تحویل پیام معتبر (Twilio، AWS SNS) برای SMS و نظارت روی هزینهها.
جمعبندی و بهترین عملکرد
پیادهسازی 2FA در Node.js نسبتاً ساده است اما رعایت جزئیات امنیتی و UX اهمیت زیادی دارد. استفاده از TOTP با speakeasy، تولید QR با qrcode و رمزنگاری secretها با الگوریتمهای مدرن، یک پایهٔ امن فراهم میکند. برای حساسیت بالاتر، WebAuthn را در نظر بگیرید. در نهایت، سیاستهایی مثل rate limiting، کدهای بازیابی و مانیتورینگ، تجربهٔ امن و قابل اعتمادی برای کاربران تضمین میکنند.
منابع پیشنهادی برای مطالعهٔ بیشتر: مستندات speakeasy، RFC 6238 (TOTP)، و راهنمای WebAuthn.
آیا این مطلب برای شما مفید بود ؟




