W różnych sektorach naszej kosmicznej stacji potrzebujemy różnych poziomów dostępu. Nie każdy członek załogi powinien mieć dostęp do wszystkich obszarów - podobnie w aplikacjach internetowych, gdzie niektóre strony powinny być dostępne tylko dla zalogowanych użytkowników lub osób z określonymi uprawnieniami.
Chronione ścieżki (protected routes) to mechanizm, który pozwala na ograniczenie dostępu do określonych części aplikacji tylko dla uprawnionych użytkowników. Zwykle implementuje się je sprawdzając, czy użytkownik jest zalogowany lub czy posiada odpowiednie uprawnienia.
Najprostszym sposobem na zabezpieczenie ścieżki jest wykorzystanie komponentu wyższego rzędu (HOC), który sprawdza stan uwierzytelnienia:
1import { Navigate, useLocation } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3
4function ProtectedRoute({ children }) {
5 const { user, isLoading } = useAuth();
6 const location = useLocation();
7
8 // Pokazujemy wskaźnik ładowania podczas sprawdzania stanu uwierzytelnienia
9 if (isLoading) {
10 return <div className="loading">Sprawdzanie uprawnień do sekcji...</div>;
11 }
12
13 // Jeśli użytkownik nie jest zalogowany, przekieruj do strony logowania
14 if (!user) {
15 // Zapisujemy ścieżkę, z której użytkownik przyszedł, aby móc wrócić po zalogowaniu
16 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
17 }
18
19 // Jeśli użytkownik jest zalogowany, renderuj chronioną zawartość
20 return children;
21}Następnie możemy użyć tego komponentu w naszych ścieżkach:
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route path="/login" element={<LoginPage />} />
4
5 {/* Chronione ścieżki */}
6 <Route path="/command-center" element={
7 <ProtectedRoute>
8 <CommandCenter />
9 </ProtectedRoute>
10 } />
11
12 <Route path="/restricted-area" element={
13 <ProtectedRoute>
14 <RestrictedArea />
15 </ProtectedRoute>
16 } />
17</Routes>Aby powyższy kod działał, potrzebujemy kontekstu uwierzytelniania, który będzie przechowywał informacje o zalogowanym użytkowniku:
1import { createContext, useContext, useState, useEffect } from 'react';
2
3const AuthContext = createContext(null);
4
5export function AuthProvider({ children }) {
6 const [user, setUser] = useState(null);
7 const [isLoading, setIsLoading] = useState(true);
8
9 useEffect(() => {
10 // Sprawdzamy, czy użytkownik jest już zalogowany (np. przez token w localStorage)
11 const checkAuthStatus = async () => {
12 try {
13 setIsLoading(true);
14 const token = localStorage.getItem('authToken');
15
16 if (token) {
17 // Walidacja tokenu na serwerze lub dekodowanie JWT
18 const userData = await validateToken(token); // funkcja implementowana oddzielnie
19 setUser(userData);
20 }
21 } catch (error) {
22 console.error('Błąd uwierzytelniania:', error);
23 // W przypadku błędu, wylogowujemy użytkownika
24 localStorage.removeItem('authToken');
25 setUser(null);
26 } finally {
27 setIsLoading(false);
28 }
29 };
30
31 checkAuthStatus();
32 }, []);
33
34 // Funkcje logowania i wylogowywania
35 const login = async (credentials) => {
36 setIsLoading(true);
37 try {
38 // Wywołanie API logowania
39 const response = await apiLogin(credentials);
40 const { token, user: userData } = response;
41
42 // Zapisanie tokenu
43 localStorage.setItem('authToken', token);
44 setUser(userData);
45 return true;
46 } catch (error) {
47 console.error('Błąd logowania:', error);
48 return false;
49 } finally {
50 setIsLoading(false);
51 }
52 };
53
54 const logout = () => {
55 localStorage.removeItem('authToken');
56 setUser(null);
57 };
58
59 return (
60 <AuthContext.Provider value={{ user, isLoading, login, logout }}>
61 {children}
62 </AuthContext.Provider>
63 );
64}
65
66// Hook do łatwego dostępu do kontekstu uwierzytelniania
67export function useAuth() {
68 return useContext(AuthContext);
69}Na stronie logowania możemy wykorzystać informacje o poprzedniej ścieżce, aby przekierować użytkownika z powrotem po zalogowaniu:
1import { useLocation, useNavigate } from 'react-router-dom';
2import { useAuth } from './AuthContext';
3import { useState } from 'react';
4
5function LoginPage() {
6 const { login } = useAuth();
7 const navigate = useNavigate();
8 const location = useLocation();
9
10 // Pobieramy ścieżkę, z której użytkownik został przekierowany (jeśli istnieje)
11 const from = location.state?.from || '/command-center';
12
13 const [credentials, setCredentials] = useState({
14 username: '',
15 password: ''
16 });
17
18 const handleChange = (e) => {
19 const { name, value } = e.target;
20 setCredentials(prev => ({
21 ...prev,
22 [name]: value
23 }));
24 };
25
26 const handleSubmit = async (e) => {
27 e.preventDefault();
28
29 const success = await login(credentials);
30
31 if (success) {
32 // Przekierowanie z powrotem do chronionej ścieżki
33 navigate(from, { replace: true });
34 }
35 };
36
37 return (
38 <div className="login-container">
39 <h1>Centrum Dostępu Kosmicznej Stacji</h1>
40 <form onSubmit={handleSubmit}>
41 <div className="form-group">
42 <label htmlFor="username">ID Astronauty:</label>
43 <input
44 id="username"
45 name="username"
46 type="text"
47 value={credentials.username}
48 onChange={handleChange}
49 required
50 />
51 </div>
52 <div className="form-group">
53 <label htmlFor="password">Kod Dostępu:</label>
54 <input
55 id="password"
56 name="password"
57 type="password"
58 value={credentials.password}
59 onChange={handleChange}
60 required
61 />
62 </div>
63 <button type="submit" className="login-button">
64 Uzyskaj Dostęp
65 </button>
66 </form>
67 </div>
68 );
69}Często potrzebujemy bardziej skomplikowanej logiki niż tylko sprawdzenie, czy użytkownik jest zalogowany. Na przykład, może być konieczne sprawdzenie, czy użytkownik ma odpowiednią rolę:
1function RoleProtectedRoute({ children, requiredRoles = [] }) {
2 const { user, isLoading } = useAuth();
3 const location = useLocation();
4
5 if (isLoading) {
6 return <div className="loading">Sprawdzanie uprawnień...</div>;
7 }
8
9 // Sprawdź, czy użytkownik jest zalogowany
10 if (!user) {
11 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
12 }
13
14 // Sprawdź, czy użytkownik ma wymaganą rolę
15 const hasRequiredRole = requiredRoles.length === 0 ||
16 requiredRoles.some(role => user.roles.includes(role));
17
18 if (!hasRequiredRole) {
19 return <Navigate to="/unauthorized" replace />;
20 }
21
22 return children;
23}
24
25// Użycie:
26<Route
27 path="/engine-room"
28 element={
29 <RoleProtectedRoute requiredRoles={['engineer', 'captain']}>
30 <EngineRoom />
31 </RoleProtectedRoute>
32 }
33/>Dobrą praktyką jest zgrupowanie wszystkich chronionych tras w jednym miejscu, co ułatwia zarządzanie nimi:
1function AppRoutes() {
2 return (
3 <Routes>
4 {/* Publiczne ścieżki */}
5 <Route path="/" element={<HomePage />} />
6 <Route path="/login" element={<LoginPage />} />
7 <Route path="/register" element={<RegisterPage />} />
8 <Route path="/about" element={<AboutPage />} />
9
10 {/* Chroniona sekcja */}
11 <Route path="/dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
12 <Route index element={<Overview />} />
13 <Route path="profile" element={<Profile />} />
14 <Route path="settings" element={<Settings />} />
15
16 {/* Trasy dla inżynierów */}
17 <Route path="maintenance" element={
18 <RoleProtectedRoute requiredRoles={['engineer']}>
19 <MaintenancePanel />
20 </RoleProtectedRoute>
21 } />
22
23 {/* Trasy dla kapitanów */}
24 <Route path="command" element={
25 <RoleProtectedRoute requiredRoles={['captain']}>
26 <CommandCenter />
27 </RoleProtectedRoute>
28 } />
29 </Route>
30
31 {/* Obsługa błędów */}
32 <Route path="/unauthorized" element={<UnauthorizedPage />} />
33 <Route path="*" element={<NotFoundPage />} />
34 </Routes>
35 );
36}W powyższym przykładzie używamy zagnieżdżonych tras, gdzie główny komponent
DashboardLayout jest chroniony, a wszystkie zagnieżdżone w nim trasy są automatycznie chronione. Dodatkowo, niektóre trasy mają dodatkowe zabezpieczenia oparte na rolach.Możemy połączyć ochronę tras z leniowym ładowaniem, aby poprawić wydajność aplikacji:
1import { lazy, Suspense } from 'react';
2
3// Leniwe ładowanie komponentów
4const CommandCenter = lazy(() => import('./CommandCenter'));
5const MaintenancePanel = lazy(() => import('./MaintenancePanel'));
6
7// W routingu:
8<Route path="command" element={
9 <RoleProtectedRoute requiredRoles={['captain']}>
10 <Suspense fallback={<Loading />}>
11 <CommandCenter />
12 </Suspense>
13 </RoleProtectedRoute>
14} />Czasami użytkownik może próbować uzyskać dostęp do wielu chronionych tras przed zalogowaniem. Możemy zapamiętać więcej niż jedną trasę:
1function useRedirectHistory() {
2 const [history, setHistory] = useState([]);
3
4 const addToHistory = (path) => {
5 setHistory(prev => {
6 // Unikaj duplikatów
7 if (!prev.includes(path)) {
8 return [...prev, path].slice(-5); // Zachowaj tylko 5 ostatnich ścieżek
9 }
10 return prev;
11 });
12 };
13
14 const getLastPath = () => history[history.length - 1] || '/dashboard';
15
16 return { addToHistory, getLastPath };
17}Ważnym aspektem zabezpieczania ścieżek jest obsługa wygaśnięcia sesji:
1function useSessionExpiration() {
2 const { logout } = useAuth();
3 const navigate = useNavigate();
4
5 useEffect(() => {
6 // Obsługa wygaśnięcia tokena JWT
7 const checkTokenExpiration = () => {
8 const token = localStorage.getItem('authToken');
9 if (token) {
10 try {
11 const payload = JSON.parse(atob(token.split('.')[1]));
12 const expirationTime = payload.exp * 1000; // Konwersja na milisekundy
13
14 if (Date.now() >= expirationTime) {
15 logout();
16 navigate('/login', {
17 state: {
18 message: 'Twoja sesja wygasła. Proszę zalogować się ponownie.'
19 }
20 });
21 }
22 } catch (e) {
23 console.error('Błąd walidacji tokenu:', e);
24 logout();
25 }
26 }
27 };
28
29 const interval = setInterval(checkTokenExpiration, 60000); // Sprawdzaj co minutę
30 return () => clearInterval(interval);
31 }, [logout, navigate]);
32}Zabezpieczanie ścieżek w React Router jest kluczowym aspektem tworzenia bezpiecznych aplikacji. Główne techniki obejmują:
Pamiętaj, że zabezpieczenia po stronie klienta (w przeglądarce) nigdy nie powinny być jedyną warstwą ochrony. Zawsze powinny być uzupełnione o odpowiednie zabezpieczenia na serwerze, który powinien niezależnie weryfikować uprawnienia użytkownika przed udostępnieniem chronionych zasobów.