Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Zabezpieczanie ścieżek w kosmicznej stacji

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.

Czym są chronione ścieżki?

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.

Podstawowa implementacja chronionej ścieżki

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>

Kontekst uwierzytelniania

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}

Przekierowanie po logowaniu

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}

Zabezpieczanie ścieżek na podstawie ról

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/>

Organizacja chronionej sekcji aplikacji

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.

Zaawansowane techniki

Lazy Loading dla chronionych tras

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} />

Zapamiętywanie wielu tras

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}

Obsługa wygaśnięcia sesji

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}

Podsumowanie

Zabezpieczanie ścieżek w React Router jest kluczowym aspektem tworzenia bezpiecznych aplikacji. Główne techniki obejmują:

  1. Tworzenie komponentów wyższego rzędu (HOC) do ochrony tras
  2. Wykorzystanie kontekstu React do zarządzania stanem uwierzytelniania
  3. Przekierowywanie użytkowników na stronę logowania z zapamiętaniem oryginalnej ścieżki
  4. Implementacja kontroli dostępu na podstawie ról
  5. Organizacja struktury aplikacji z wyraźnym podziałem na publiczne i chronione sekcje
  6. Obsługa wygaśnięcia sesji i automatyczne wylogowanie

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.

Przejdź do CodeWorlds