Protected Routes: محدود کردن دسترسی به برخی مسیرها

در درس‌های قبل این فصل، دیدیم چگونه بین مسیرها حرکت کنیم. اما برخی صفحات، مثل داشبورد یا تنظیمات حساب کاربری، باید فقط برای کاربران واردشده در دسترس باشند. در این درس، الگوی رایج برای محافظت از این نوع مسیرها را می‌بینیم.

ایده اصلی: یک کامپوننت واسط که تصمیم می‌گیرد

به‌جای این‌که هر صفحه محافظت‌شده، خودش منطق بررسی ورود را تکرار کند، معمول‌ترین الگو این است که یک کامپوننت واسط بسازیم که یا کودکان خود را رندر می‌کند (اگر کاربر مجاز است)، یا کاربر را به صفحه ورود هدایت می‌کند:

import { Navigate, Outlet } from "react-router-dom";

function ProtectedRoute({ isAuthenticated }) {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
}

Navigate نسخه‌ای اعلانی از همان useNavigate است که در درس قبل دیدیم؛ به‌جای فراخوانی داخل یک Event Handler، مستقیم به‌عنوان JSX رندر می‌شود و همان لحظه کاربر را هدایت می‌کند. گزینه replace هم باعث می‌شود صفحه محافظت‌شده در تاریخچه مرورگر باقی نماند.

استفاده در ساختار Routeها

با کمک Nested Routes که در درس دوم همین فصل دیدیم، می‌توان این واسط را والد چند Route محافظت‌شده قرار داد:

const router = createBrowserRouter([
  { path: "/login", element: <LoginPage /> },
  {
    element: <ProtectedRoute isAuthenticated={checkAuth()} />,
    children: [
      { path: "/dashboard", element: <Dashboard /> },
      { path: "/settings", element: <Settings /> },
    ],
  },
]);

حالا /dashboard و /settings هر دو، پیش از رندر شدن واقعی، باید از فیلتر ProtectedRoute عبور کنند؛ بدون این‌که هرکدام جداگانه این منطق را بنویسند.

خواندن وضعیت واقعی ورود، نه یک مقدار ثابت

در عمل، isAuthenticated معمولاً از یک Context خوانده می‌شود، نه مستقیم محاسبه شود؛ دقیقاً همان الگوی AuthProvider که در فصل هشتم ساختیم:

function ProtectedRoute() {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
}

یک نکته مهم: وضعیت Loading ورود را هم در نظر بگیرید

اگر بررسی وضعیت ورود خودش یک عملیات غیرهمزمان است (مثلاً خواندن یک Token و اعتبارسنجی آن با سرور)، نباید در همان لحظه اول، پیش از پایان این بررسی، کاربر را به‌اشتباه به صفحه ورود هدایت کرد. باید یک حالت سوم هم در نظر گرفت:

function ProtectedRoute() {
  const { user, loading } = useAuth();

  if (loading) return <p>در حال بررسی ورود...</p>;
  if (!user) return <Navigate to="/login" replace />;

  return <Outlet />;
}

این دقیقاً همان سه‌حالتی است که در فصل قبل، درباره دریافت داده دیدیم؛ فقط این‌بار داده موردنظر، وضعیت ورود کاربر است.

محافظت واقعی، فقط در کلاینت کافی نیست

نکته‌ای که باید همیشه در نظر داشت: این الگو فقط تجربه کاربری را کنترل می‌کند (پنهان کردن یک صفحه از کاربر در مرورگر). هیچ داده حساسی نباید صرفاً به این دلیل که یک Route در کلاینت محافظت شده، در سمت سرور بدون بررسی مجوز واقعی ارسال شود؛ بررسی نهایی دسترسی، همیشه باید در سمت سرور هم انجام شود.

مثال قابل اجرا

شبیه‌سازی یک Protected Route ساده
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

الگوی Protected Route، یک کامپوننت واسط است که بر اساس وضعیت ورود، یا Outlet مربوط به صفحات محافظت‌شده را رندر می‌کند، یا کاربر را با Navigate به صفحه ورود هدایت می‌کند؛ باید وضعیت Loading بررسی ورود را هم جداگانه مدیریت کرد. این محافظت فقط تجربه کاربری در مرورگر را کنترل می‌کند، نه جایگزین بررسی واقعی مجوز در سرور. در درس بعدی، به Route-based Code Splitting می‌پردازیم.