Пошук уроків, статей та іншого контенту
Захистите маршрути перевіркою автентифікації та перенаправлятимете неавторизованих користувачів на сторінку входу.
Захищений маршрут — це маршрут, доступ до якого мають лише автентифіковані користувачі.
Наприклад:
/login доступний усім;
/dashboard доступний лише після входу;
якщо неавторизований користувач відкриває /dashboard, його потрібно перенаправити на /login;
після успішного входу користувача можна повернути на сторінку, яку він намагався відкрити.
У React Router захист маршруту зазвичай реалізують окремим компонентом-обгорткою. Цей компонент:
отримує поточний статус автентифікації;
показує захищений вміст, якщо користувач увійшов;
повертає <Navigate>, якщо користувач неавторизований.
Компонентам застосунку потрібен спільний доступ до інформації про автентифікацію. Для цього зручно використати React Context.
Створимо контекст із:
isAuthenticated — чи автентифікований користувач;
login — функція входу;
logout — функція виходу.
У реальному застосунку login зазвичай надсилає запит на сервер. У прикладі нижче стан змінюється локально, щоб зосередитися на роботі маршрутів.
Для вкладених маршрутів React Router надає компонент <Outlet />. Він відображає той дочірній маршрут, який відповідає поточній URL-адресі.
Захисний компонент може виглядати так:
function ProtectedRoute() {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return (
<Navigate
to="/login"
replace
state={{ from: location }}
/>
);
}
return <Outlet />;
}Якщо користувач не увійшов:
відбувається перенаправлення на /login;
поточна адреса зберігається в state.from;
після входу можна повернути користувача на цю адресу.
Властивість replace замінює поточний запис в історії браузера. Завдяки цьому натискання кнопки «Назад» не повертає користувача на захищену сторінку, з якої його перенаправили.
Приклад використовує react-router-dom версії 6 або новішої.
import { createContext, useContext, useState } from "react";
import { createRoot } from "react-dom/client";
import {
BrowserRouter,
Link,
Navigate,
Outlet,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
function login() {
setIsAuthenticated(true);
}
function logout() {
setIsAuthenticated(false);
}
const value = {
isAuthenticated,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth має використовуватися всередині AuthProvider");
}
return context;
}
function ProtectedRoute() {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
return (
<Navigate
to="/login"
replace
state={{ from: location }}
/>
);
}
return <Outlet />;
}
function Navigation() {
const { isAuthenticated, logout } = useAuth();
const navigate = useNavigate();
function handleLogout() {
logout();
navigate("/login", { replace: true });
}
return (
<nav>
<Link to="/">Головна</Link>{" "}
<Link to="/dashboard">Панель керування</Link>{" "}
<Link to="/profile">Профіль</Link>{" "}
{isAuthenticated && (
<button type="button" onClick={handleLogout}>
Вийти
</button>
)}
</nav>
);
}
function HomePage() {
return (
<main>
<h1>Головна сторінка</h1>
<p>Ця сторінка доступна всім користувачам.</p>
</main>
);
}
function LoginPage() {
const { isAuthenticated, login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || "/dashboard";
if (isAuthenticated) {
return <Navigate to={from} replace />;
}
function handleSubmit(event) {
event.preventDefault();
// У реальному застосунку тут потрібно виконати запит до сервера.
login();
navigate(from, { replace: true });
}
return (
<main>
<h1>Вхід</h1>
<form onSubmit={handleSubmit}>
<label>
Електронна пошта
<input
type="email"
name="email"
defaultValue="user@example.com"
required
/>
</label>
<br />
<label>
Пароль
<input
type="password"
name="password"
defaultValue="password"
required
/>
</label>
<br />
<button type="submit">Увійти</button>
</form>
</main>
);
}
function DashboardPage() {
return (
<main>
<h1>Панель керування</h1>
<p>Ця сторінка доступна лише автентифікованим користувачам.</p>
</main>
);
}
function ProfilePage() {
return (
<main>
<h1>Профіль</h1>
<p>Особисті дані користувача.</p>
</main>
);
}
function App() {
return (
<BrowserRouter>
<AuthProvider>
<Navigation />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Route>
<Route
path="*"
element={<Navigate to="/" replace />}
/>
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
createRoot(document.getElementById("root")).render(<App />);Розглянемо послідовність дій користувача:
Користувач відкриває /profile.
React Router знаходить маршрут, вкладений у ProtectedRoute.
ProtectedRoute перевіряє isAuthenticated.
Якщо значення дорівнює false, компонент повертає:
<Navigate
to="/login"
replace
state={{ from: location }}
/>Користувач потрапляє на /login.
Після відправлення форми викликається login().
Зі стану маршруту дістається початкова адреса:
const from = location.state?.from?.pathname || "/dashboard";Користувач повертається на /profile.
Якщо користувач просто відкрив /login, значення location.state?.from відсутнє. У такому випадку після входу використовується запасний маршрут /dashboard.
Один ProtectedRoute може захищати одразу кілька маршрутів:
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>Усі дочірні маршрути будуть доступні лише після автентифікації.
Це зручніше, ніж дублювати перевірку в кожному компоненті сторінки:
// Не потрібно повторювати таку логіку в кожній сторінці
function ProfilePage() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <h1>Профіль</h1>;
}Перевірка на рівні маршруту відокремлює навігаційну логіку від вмісту сторінки.
У реальному застосунку статус автентифікації може бути невідомим під час завантаження. Наприклад, застосунок перевіряє сесію на сервері.
У такому випадку недостатньо лише двох станів:
користувач автентифікований;
користувач неавторизований.
Потрібен ще стан завантаження:
function ProtectedRoute() {
const { isAuthenticated, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
return <p>Перевірка автентифікації...</p>;
}
if (!isAuthenticated) {
return (
<Navigate
to="/login"
replace
state={{ from: location }}
/>
);
}
return <Outlet />;
}Без цього стану застосунок може передчасно перенаправити користувача на сторінку входу, хоча сесія ще не встигла завантажитися.
Захищений маршрут приховує сторінку в інтерфейсі, але сам по собі не захищає дані на сервері.
Користувач може вручну виконати HTTP-запит до API, тому сервер також повинен:
перевіряти токен або сесію;
визначати права доступу;
повертати помилку для неавторизованих запитів.
Перевірка в React потрібна для коректної навігації та зручності користувача. Справжня безпека даних має забезпечуватися на сервері.
Недостатньо приховати посилання на захищену сторінку:
{isAuthenticated && <Link to="/profile">Профіль</Link>}Користувач усе одно може вручну ввести /profile в адресному рядку. Саме тому перевірка повинна бути в компоненті маршруту.
OutletЯкщо ProtectedRoute використовується для вкладених маршрутів, він має повернути <Outlet /> після успішної перевірки:
function ProtectedRoute() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return <Outlet />;
}Без <Outlet /> дочірня сторінка не буде відображена.
replaceЯкщо не використовувати replace, захищений маршрут залишиться в історії браузера. Після натискання «Назад» користувач знову потрапить на маршрут, який одразу перенаправить його на сторінку входу.
Користувач може перейти на сторінку входу без попередньої спроби відкрити захищений маршрут. Тому потрібно передбачити значення за замовчуванням:
const from = location.state?.from?.pathname || "/dashboard";Стан useState скидається після перезавантаження сторінки. Для реального входу стан потрібно відновлювати з механізму сесії, який використовує застосунок, а доступ до даних у будь-якому разі має перевіряти сервер.
Захищений маршрут перевіряє статус автентифікації перед відображенням сторінки.
Для перенаправлення в React Router використовують <Navigate>.
<Outlet /> відображає дочірні захищені маршрути.
location.state дає змогу запам’ятати сторінку, яку користувач хотів відкрити.
replace не залишає недоступний маршрут в історії браузера.
Для кількох сторінок можна створити одну групу маршрутів під ProtectedRoute.
Перевірка на клієнті відповідає за навігацію, але не замінює перевірку доступу на сервері.