Cours JavaScript et React · Module 6: React Router
Protéger les routes dans la station spatiale
Dans cette leçon9
Dans les différents secteurs de notre station spatiale, nous avons besoin de différents niveaux d'accès. Tous les membres de l'équipage ne devraient pas avoir accès à toutes les zones - de même dans les applications web, où certaines pages ne devraient être accessibles qu'aux utilisateurs connectés ou aux personnes disposant de permissions spécifiques.
Que sont les routes protégées ?
Les routes protégées sont un mécanisme qui permet de restreindre l'accès à certaines parties de l'application uniquement aux utilisateurs autorisés. Elles sont généralement implémentées en vérifiant si l'utilisateur est connecté ou s'il dispose des permissions appropriées.
Implémentation de base d'une route protégée
La façon la plus simple de protéger une route est d'utiliser un composant d'ordre supérieur (HOC) qui vérifie l'état d'authentification :
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 // Afficher un indicateur de chargement pendant la vérification de l'état d'authentification
9 if (isLoading) {
10 return <div className="loading">Vérification des permissions de la section...</div>;
11 }
12
13 // Si l'utilisateur n'est pas connecté, rediriger vers la page de connexion
14 if (!user) {
15 // Enregistrer le chemin d'où vient l'utilisateur pour pouvoir y revenir après la connexion
16 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
17 }
18
19 // Si l'utilisateur est connecté, rendre le contenu protégé
20 return children;
21}Nous pouvons ensuite utiliser ce composant dans nos routes :
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route path="/login" element={<LoginPage />} />
4
5 {/* Routes protégées */}
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>Le contexte d'authentification
Pour que le code ci-dessus fonctionne, nous avons besoin d'un contexte d'authentification qui stockera les informations sur l'utilisateur connecté :
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 // Vérifier si l'utilisateur est déjà connecté (par ex. via un token dans localStorage)
11 const checkAuthStatus = async () => {
12 try {
13 setIsLoading(true);
14 const token = localStorage.getItem('authToken');
15
16 if (token) {
17 // Validation du token sur le serveur ou décodage du JWT
18 const userData = await validateToken(token); // fonction implémentée séparément
19 setUser(userData);
20 }
21 } catch (error) {
22 console.error('Erreur d\'authentification :', error);
23 // En cas d'erreur, déconnecter l'utilisateur
24 localStorage.removeItem('authToken');
25 setUser(null);
26 } finally {
27 setIsLoading(false);
28 }
29 };
30
31 checkAuthStatus();
32 }, []);
33
34 // Fonctions de connexion et de déconnexion
35 const login = async (credentials) => {
36 setIsLoading(true);
37 try {
38 // Appeler l'API de connexion
39 const response = await apiLogin(credentials);
40 const { token, user: userData } = response;
41
42 // Enregistrer le token
43 localStorage.setItem('authToken', token);
44 setUser(userData);
45 return true;
46 } catch (error) {
47 console.error('Erreur de connexion :', 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 pour accéder facilement au contexte d'authentification
67export function useAuth() {
68 return useContext(AuthContext);
69}Redirection après la connexion
Sur la page de connexion, nous pouvons utiliser l'information sur le chemin précédent pour rediriger l'utilisateur après sa connexion :
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 // Obtenir le chemin d'où l'utilisateur a été redirigé (s'il existe)
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 // Rediriger vers le chemin protégé
33 navigate(from, { replace: true });
34 }
35 };
36
37 return (
38 <div className="login-container">
39 <h1>Centre d'accès de la station spatiale</h1>
40 <form onSubmit={handleSubmit}>
41 <div className="form-group">
42 <label htmlFor="username">ID astronaute :</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">Code d'accès :</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 Obtenir l'accès
65 </button>
66 </form>
67 </div>
68 );
69}Protéger les routes en fonction des rôles
Nous avons souvent besoin d'une logique plus complexe que la simple vérification de la connexion de l'utilisateur. Par exemple, il peut être nécessaire de vérifier si l'utilisateur dispose du bon rôle :
1function RoleProtectedRoute({ children, requiredRoles = [] }) {
2 const { user, isLoading } = useAuth();
3 const location = useLocation();
4
5 if (isLoading) {
6 return <div className="loading">Vérification des permissions...</div>;
7 }
8
9 // Vérifier si l'utilisateur est connecté
10 if (!user) {
11 return <Navigate to="/login" state={{ from: location.pathname }} replace />;
12 }
13
14 // Vérifier si l'utilisateur a le rôle requis
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// Utilisation :
26<Route
27 path="/engine-room"
28 element={
29 <RoleProtectedRoute requiredRoles={['engineer', 'captain']}>
30 <EngineRoom />
31 </RoleProtectedRoute>
32 }
33/>Organiser la section protégée de l'application
Une bonne pratique consiste à regrouper toutes les routes protégées au même endroit, ce qui les rend plus faciles à gérer :
1function AppRoutes() {
2 return (
3 <Routes>
4 {/* Chemins publics */}
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 {/* Section protégée */}
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 {/* Routes pour les ingénieurs */}
17 <Route path="maintenance" element={
18 <RoleProtectedRoute requiredRoles={['engineer']}>
19 <MaintenancePanel />
20 </RoleProtectedRoute>
21 } />
22
23 {/* Routes pour les capitaines */}
24 <Route path="command" element={
25 <RoleProtectedRoute requiredRoles={['captain']}>
26 <CommandCenter />
27 </RoleProtectedRoute>
28 } />
29 </Route>
30
31 {/* Gestion des erreurs */}
32 <Route path="/unauthorized" element={<UnauthorizedPage />} />
33 <Route path="*" element={<NotFoundPage />} />
34 </Routes>
35 );
36}Dans l'exemple ci-dessus, nous utilisons des routes imbriquées où le composant principal DashboardLayout est protégé, et toutes les routes imbriquées en son sein sont automatiquement protégées. De plus, certaines routes ont des protections supplémentaires basées sur les rôles.
Techniques avancées
Chargement différé pour les routes protégées
Nous pouvons combiner la protection des routes avec le chargement différé pour améliorer les performances de l'application :
1import { lazy, Suspense } from 'react';
2
3// Chargement différé des composants
4const CommandCenter = lazy(() => import('./CommandCenter'));
5const MaintenancePanel = lazy(() => import('./MaintenancePanel'));
6
7// Dans le routage :
8<Route path="command" element={
9 <RoleProtectedRoute requiredRoles={['captain']}>
10 <Suspense fallback={<Loading />}>
11 <CommandCenter />
12 </Suspense>
13 </RoleProtectedRoute>
14} />Mémoriser plusieurs routes
Parfois, un utilisateur peut tenter d'accéder à plusieurs routes protégées avant de se connecter. Nous pouvons mémoriser plus d'une route :
1function useRedirectHistory() {
2 const [history, setHistory] = useState([]);
3
4 const addToHistory = (path) => {
5 setHistory(prev => {
6 // Éviter les doublons
7 if (!prev.includes(path)) {
8 return [...prev, path].slice(-5); // Ne garder que les 5 derniers chemins
9 }
10 return prev;
11 });
12 };
13
14 const getLastPath = () => history[history.length - 1] || '/dashboard';
15
16 return { addToHistory, getLastPath };
17}Gérer l'expiration de la session
Un aspect important de la protection des routes est la gestion de l'expiration de la session :
1function useSessionExpiration() {
2 const { logout } = useAuth();
3 const navigate = useNavigate();
4
5 useEffect(() => {
6 // Gérer l'expiration du token 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; // Convertir en millisecondes
13
14 if (Date.now() >= expirationTime) {
15 logout();
16 navigate('/login', {
17 state: {
18 message: 'Ta session a expiré. Merci de te reconnecter.'
19 }
20 });
21 }
22 } catch (e) {
23 console.error('Erreur de validation du token :', e);
24 logout();
25 }
26 }
27 };
28
29 const interval = setInterval(checkTokenExpiration, 60000); // Vérifier chaque minute
30 return () => clearInterval(interval);
31 }, [logout, navigate]);
32}Résumé
La protection des routes dans React Router est un aspect clé de la création d'applications sécurisées. Les principales techniques incluent :
- Créer des composants d'ordre supérieur (HOC) pour protéger les routes
- Utiliser React Context pour gérer l'état d'authentification
- Rediriger les utilisateurs vers la page de connexion en mémorisant le chemin d'origine
- Implémenter un contrôle d'accès basé sur les rôles
- Organiser la structure de l'application avec une séparation claire entre les sections publiques et protégées
- Gérer l'expiration de la session et la déconnexion automatique
Souviens-toi que la sécurité côté client (navigateur) ne devrait jamais être la seule couche de protection. Elle devrait toujours être complétée par une sécurité appropriée côté serveur, qui devrait vérifier indépendamment les permissions de l'utilisateur avant de donner accès aux ressources protégées.
Code de cette leçon : App.jsx
1import React, { useState, createContext, useContext } from 'react';
2import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
3
4// Contexte d'autorisation
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// Composant Protected Route
30function ProtectedRoute({ children }) {
31 const { user } = useAuth();
32 const location = useLocation();
33
34 if (!user) {
35 // Rediriger vers la connexion en enregistrant l'emplacement
36 return <Navigate to="/login" state={{ from: location }} replace />;
37 }
38
39 return children;
40}
41
42// Page de connexion
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>Connexion</h1>
62 <form onSubmit={handleSubmit}>
63 <input
64 value={username}
65 onChange={(e) => setUsername(e.target.value)}
66 placeholder="Nom d'utilisateur"
67 />
68 <button type="submit">Se connecter</button>
69 </form>
70 <p className="info">Connecte-toi pour accéder aux sections protégées.</p>
71 </div>
72 );
73}
74
75// Page publique
76function HomePage() {
77 return (
78 <div className="page">
79 <h1>Page publique</h1>
80 <p>Cette page est accessible à tout le monde.</p>
81 </div>
82 );
83}
84
85// Page protégée
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>Bienvenue, {user?.name} ! Ceci est une page protégée.</p>
99 <button onClick={handleLogout} className="logout">Déconnexion</button>
100 </div>
101 );
102}
103
104// Page secrète
105function SecretPage() {
106 return (
107 <div className="page secret">
108 <h1>Page secrète</h1>
109 <p>Félicitations ! Tu as accès aux données classifiées.</p>
110 </div>
111 );
112}
113
114// Navigation
115function Navigation() {
116 const { user } = useAuth();
117
118 return (
119 <nav>
120 <Link to="/">Accueil</Link>
121 {user ? (
122 <>
123 <Link to="/dashboard">Dashboard</Link>
124 <Link to="/secret">Secret</Link>
125 <span className="user-info">{user.name}</span>
126 </>
127 ) : (
128 <Link to="/login">Connexion</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;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment NavLink dans React Router permet-il de définir dynamiquement une classe CSS en fonction de l'état actif ?
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne les éléments de syntaxe du composant Link :
- Clique dans l'ordre
Ordonne le NavLink avec une classe dynamique :