Kurs JavaScript i React · Moduł 6: React Router

Zabezpieczanie ścieżek w kosmicznej stacji

7 min czytania
W tej lekcji9

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.

Kod do tej lekcji: App.jsx
1import React, { useState, createContext, useContext } from 'react';
2import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
3
4// Kontekst autoryzacji
5const AuthContext = createContext();
6
7function AuthProvider({ children }) {
8  const [user, setUser] = useState(null);
9
10  const login = (username) => {
11    setUser({ name: username, role: 'astronaut' });
12  };
13
14  const logout = () => {
15    setUser(null);
16  };
17
18  return (
19    <AuthContext.Provider value={{ user, login, logout }}>
20      {children}
21    </AuthContext.Provider>
22  );
23}
24
25function useAuth() {
26  return useContext(AuthContext);
27}
28
29// Komponent Protected Route
30function ProtectedRoute({ children }) {
31  const { user } = useAuth();
32  const location = useLocation();
33
34  if (!user) {
35    // Przekieruj do logowania z zapisaniem lokalizacji
36    return <Navigate to="/login" state={{ from: location }} replace />;
37  }
38
39  return children;
40}
41
42// Strona logowania
43function LoginPage() {
44  const [username, setUsername] = useState('');
45  const { login } = useAuth();
46  const navigate = useNavigate();
47  const location = useLocation();
48
49  const from = location.state?.from?.pathname || '/dashboard';
50
51  const handleSubmit = (e) => {
52    e.preventDefault();
53    if (username.trim()) {
54      login(username);
55      navigate(from, { replace: true });
56    }
57  };
58
59  return (
60    <div className="page login">
61      <h1>Logowanie</h1>
62      <form onSubmit={handleSubmit}>
63        <input
64          value={username}
65          onChange={(e) => setUsername(e.target.value)}
66          placeholder="Nazwa użytkownika"
67        />
68        <button type="submit">Zaloguj</button>
69      </form>
70      <p className="info">Zaloguj się, aby uzyskać dostęp do chronionych sekcji.</p>
71    </div>
72  );
73}
74
75// Strona publiczna
76function HomePage() {
77  return (
78    <div className="page">
79      <h1>Strona publiczna</h1>
80      <p>Ta strona jest dostępna dla wszystkich.</p>
81    </div>
82  );
83}
84
85// Strona chroniona
86function DashboardPage() {
87  const { user, logout } = useAuth();
88  const navigate = useNavigate();
89
90  const handleLogout = () => {
91    logout();
92    navigate('/');
93  };
94
95  return (
96    <div className="page dashboard">
97      <h1>Dashboard</h1>
98      <p>Witaj, {user?.name}! To jest strona chroniona.</p>
99      <button onClick={handleLogout} className="logout">Wyloguj</button>
100    </div>
101  );
102}
103
104// Strona sekretna
105function SecretPage() {
106  return (
107    <div className="page secret">
108      <h1>Strona tajne</h1>
109      <p>Gratulacje! Masz dostęp do tajnych danych.</p>
110    </div>
111  );
112}
113
114// Nawigacja
115function Navigation() {
116  const { user } = useAuth();
117
118  return (
119    <nav>
120      <Link to="/">Home</Link>
121      {user ? (
122        <>
123          <Link to="/dashboard">Dashboard</Link>
124          <Link to="/secret">Tajne</Link>
125          <span className="user-info">{user.name}</span>
126        </>
127      ) : (
128        <Link to="/login">Zaloguj</Link>
129      )}
130    </nav>
131  );
132}
133
134function App() {
135  return (
136    <AuthProvider>
137      <BrowserRouter>
138        <div className="app">
139          <Navigation />
140          <Routes>
141            <Route path="/" element={<HomePage />} />
142            <Route path="/login" element={<LoginPage />} />
143            <Route
144              path="/dashboard"
145              element={
146                <ProtectedRoute>
147                  <DashboardPage />
148                </ProtectedRoute>
149              }
150            />
151            <Route
152              path="/secret"
153              element={
154                <ProtectedRoute>
155                  <SecretPage />
156                </ProtectedRoute>
157              }
158            />
159          </Routes>
160        </div>
161      </BrowserRouter>
162    </AuthProvider>
163  );
164}
165
166export default App;

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jak NavLink w React Router pozwala dynamicznie ustawić klasę CSS na podstawie aktywnego stanu?

Zadania praktyczne w grze

  • Układanie w poziomie

    Ułóż elementy składni komponentu Link:

  • Klikanie w kolejności

    Uporządkuj NavLink z dynamiczną klasą:

Przydatne artykuły