<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

🔐 Sécurité des routeurs angulaires : Gardes et intercepteurs en action

Introduction : La sécurité est une exigence, pas une option. Les applications angulaires modernes gèrent des données sensibles : profils d'utilisateurs, tableaux de bord d'entreprise, panneaux d'administration. Laisser ces sections non protégées expose votre application à des accès non autorisés, au vol de jetons et à des violations de la vie privée. Angular fournit un système de sécurité de routage intégré et composable : Route Guards et HTTP Interceptors. Ce guide vous montre comment créer un système de sécurité en couches à l'aide des outils modernes Angular 17+. Nous examinerons chaque mécanisme en détail, avec de vrais exemples de code prêts à être utilisés en production. Sujets : #Angular #Guards #Interceptor #WebDevelopment #FrontendDevelopment Architecture de sécurité côté frontend Avant d'entrer dans le code, il est essentiel de comprendre comment la sécurité est superposée dans une application Angular : Niveau 1 – Route Guards : ils contrôlent si l'utilisateur peut naviguer vers un certain itinéraire avant même que le composant ne soit créé. Niveau 2 – Intercepteurs HTTP : ajoutez des jetons d'authentification à chaque requête sortante et gérez les réponses d'erreur (401, 403) globalement. Niveau 3 – Validation backend : le backend doit toujours valider l'authentification et l'autorisation. La sécurité frontale est une défense supplémentaire, et pas la seule. Section 1 : CanActivate — Protection des routes CanActivate est la garde la plus courante. Il s'exécute avant qu'Angular n'active une route et son composant associé. S'il renvoie false ou un UrlTree , la navigation est bloquée et l'utilisateur est redirigé. À partir d'Angular 15, l'approche recommandée est la fonction de garde ( CanActivateFn ) avec inject() au lieu de classes avec des interfaces obsolètes. Cela élimine le passe-partout et tire pleinement parti de l’injection de dépendances fonctionnelles. Garde d'authentification de base // core/guards/auth.guard.

ts import { inject } depuis '@angular/core'; importer { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } depuis '@angular/router' ; importer { AuthService } depuis '../services/auth.service' ; export const authGuard : CanActivateFn = (route : ActivatedRouteSnapshot, état : RouterStateSnapshot, ) => { const auth = inject (AuthService); const routeur = injecter(Routeur); if (auth.isAuthenticated()) { return true ; } // Préserve l'URL demandée pour la redirection après la connexion return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }, }); } ; Guard pour le rôle d'administrateur // core/guards/admin.guard.ts import { inject } from '@angular/core'; importer { CanActivateFn, Router } depuis '@angular/router' ; importer { AuthService } depuis '../services/auth.service' ; export const adminGuard : CanActivateFn = (route, état) => { const auth = inject(AuthService); const routeur = injecter(Routeur); if (!auth.isAuthenticated()) { return router.createUrlTree(['/login'], { queryParams : { returnUrl : state.url } }); } if (!auth.hasRole('admin')) { return router.createUrlTree(['/403']); } renvoie vrai ; } ; AuthService avec JWT et signaux // core/services/auth.service.ts import { Injectable, signal, calculé } from '@angular/core' ; importer { HttpClient } depuis '@angular/common/http' ; importer {Routeur} depuis '@angular/router' ; importer { tap } depuis 'rxjs/operators' ; interface TokenPayload { sous : chaîne ; email : chaîne ; rôle : 'admin' | 'utilisateur'; iat : numéro ; exp : nombre ; } fonction decodeJwt(token : string) : TokenPayload | null { try { const base64Payload = token.split('.')[1]; const décodé = atob(base64Payload.replace(/-/g, '+').replace(/_/g, '/')); renvoie JSON.parse (décodé) en tant que TokenPayload ; } catch {return null; } } @Injectable({ provideIn : 'root' }) classe d'exportation AuthService { private static readonly TOKEN_KEY = 'portfolio_token'; privé en lecture seule _payload = signal(this.readStoredPayload()); lecture seule currentUser = this._payload.

asReadonly(); en lecture seule isAdmin = calculated(() => this._payload()?.role === 'admin'); constructeur (http privé : HttpClient, routeur privé : routeur) {} login (e-mail : chaîne, mot de passe : chaîne) { return this.http .post<{ access_token : chaîne }>('/api/auth/login', { email, mot de passe }) .pipe(tap(({ access_token }) => { localStorage.setItem(AuthService.TOKEN_KEY, access_token); this._payload.set(decodeJwt(access_token)); } logout(): void { localStorage.removeItem(AuthService.TOKEN_KEY); this._payload.set(null); this.router.navigate(['/login']); } getToken(): string | = this.getToken(); if (!token) return false; const payload = decodeJwt(token); return payload.exp > Date.now() / 1000 + 30 : false; } hasRole(role: string): boolean { return this._payload()?.role === role; localStorage.getItem(AuthService.TOKEN_KEY); return token ? decodeJwt(token) : null; } } Section 2 : CanMatch — Sécurité pour les formulaires chargés paresseusement, Angular télécharge le package JavaScript du formulaire uniquement lorsque l'utilisateur accède à cette route. Si vous ne sécurisez pas le téléchargement, un utilisateur non autorisé peut toujours télécharger le code du tableau de bord d'administration en modifiant simplement l'URL. renvoie false , Angular n'évalue pas la route et ne télécharge jamais le bundle associé. // core/guards/can-match-admin.guard.ts import { inject } from '@angular/core'; import { CanMatchFn, Router } from '@angular/router'; import { AuthService } from '../services/auth.service'; = inject(AuthService); const router = inject(Router); if (auth.isAuthenticated() && auth.hasRole('admin')) return true; return router.createUrlTree(['/login'] };

ts import {Routes} depuis '@angular/router' ; importer { authGuard } depuis './core/guards/auth.guard' ; importer { adminGuard } depuis './core/guards/admin.guard' ; importer { canMatchAdmin } depuis './core/guards/can-match-admin.guard' ; export const APP_ROUTES : Routes = [ { chemin : 'admin', loadChildren : () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES), canActivate : [adminGuard], canMatch : [canMatchAdmin], }, { chemin : 'dashboard', loadComponent : () => import('./features/dashboard/dashboard.component').then(c => c.DashboardComponent), canActivate : [authGuard], }, { chemin : 'login', loadComponent : () => import('./features/auth/login.component').then(c => c.LoginComponent) }, { chemin : '', redirectTo : 'dashboard', pathMatch : 'complet' }, { chemin : '**', redirectTo : 'login' }, ]; Section 3 : CanDeactivate — Protection des formulaires non enregistrés Un scénario courant : l'utilisateur remplit un long formulaire et clique accidentellement sur un lien de navigation. Sans CanDeactivate , toutes les modifications sont perdues silencieusement. Avec ce regard, une demande de confirmation apparaît. Guard CanDeactivate Générique et réutilisable // core/guards/dirty-form.guard.ts import { CanDeactivateFn } from '@angular/router' ; interface d'exportation DirtyFormComponent { isDirty(): boolean; } export const dirtyFormGuard : CanDeactivateFn = composant => { if (!component.isDirty()) return true ; return window.confirm('Vous avez des modifications non enregistrées qui seront perdues. Continuer ?'); } ; Composant avec formulaire réactif //features/profile/profile-edit.component.ts import { Component, OnInit } from '@angular/core' ; importer { NonNullableFormBuilder, ReactiveFormsModule, Validators } depuis '@angular/forms' ; importer { DirtyFormComponent } depuis '../../core/guards/dirty-form.guard' ; @Component({ sélecteur : 'app-profile-edit', standalone : ​​true, importations : [ReactiveFormsModule], templateUrl : './profile-edit.component.html' }) classe d'exportation ProfileEditComponent implémente OnInit, DirtyFormComponent { readonly form = this.

fb.group({ displayName : ['', [Validators.required, Validators.minLength(2)]], bio : ['', Validators.maxLength(500)], }); privé enregistréSnapshot = ''; constructeur (fb privé : NonNullableFormBuilder) {} ngOnInit() : void { this.savedSnapshot = JSON.stringify(this.form.getRawValue()); } save() : void { this.savedSnapshot = JSON.stringify(this.form.getRawValue()); } isDirty() : boolean { return JSON.stringify(this.form.getRawValue()) !== this.savedSnapshot; } } Section 4 : Intercepteurs HTTP — Sécurité multicanal Les intercepteurs HTTP interceptent chaque requête HTTP sortante et chaque réponse entrante. Ils constituent l’endroit idéal pour ajouter le token JWT, gérer les actualisations automatiques et centraliser la logique de gestion des erreurs. Intercepteur d'authentification JWT // core/interceptors/auth.interceptor.ts import { HttpInterceptorFn, HttpErrorResponse } depuis '@angular/common/http' ; importer { injecter } depuis '@angular/core' ; importer { catchError, throwError } depuis 'rxjs' ; importer { AuthService } depuis '../services/auth.service' ; importer {Routeur} depuis '@angular/router' ; exporter const authInterceptor : HttpInterceptorFn = (req, next) => { const auth = inject(AuthService); const routeur = injecter(Routeur); const jeton = auth.getToken(); const authorizedReq = jeton ? req.clone({ setHeaders : { Autorisation : `Bearer ${token}` } }) : req; return next(authorizedReq).pipe( catchError((err: HttpErrorResponse) => { if (err.status === 401) { auth.logout(); router.navigate(['/login'], { queryParams: { Reason: 'session-expired' } }); } if (err.status === 403) { router.navigate(['/403']); } return throwError(() => err }), ); Interceptor for Global Loading Spinner // core/interceptors/loading.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; LoadingService } depuis '../services/loading.service' ; exporter const chargementInterceptor : HttpInterceptorFn = (req, next) => { if (req.

headers.has('X-Background-Request')) return next(req); const chargement = injecter (LoadingService); chargement.increment(); return next(req).pipe(finalize(() => chargement.decrement())); } ; Connexion à app.config.ts // app.config.ts import { ApplicationConfig } depuis '@angular/core' ; import { provideRouter, withComponentInputBinding } depuis '@angular/router' ; import { provideHttpClient, withInterceptors, withFetch } depuis '@angular/common/http' ; importer { APP_ROUTES } depuis './app.routes' ; importer { authInterceptor } depuis './core/interceptors/auth.interceptor' ; importer { chargementInterceptor } depuis './core/interceptors/loading.interceptor'; export const appConfig : ApplicationConfig = { fournisseurs : [ provideRouter(APP_ROUTES, withComponentInputBinding()), provideHttpClient(withFetch(), withInterceptors([loadingInterceptor, authInterceptor])), ], } ; Meilleures pratiques en matière de sécurité et de performances Double vérification du backend : les gardes protègent l'UX, mais chaque point de terminaison de l'API doit valider indépendamment le JWT. Ne comptez jamais uniquement sur le frontend. Jetons d'expiration courte : utilisez des JWT avec une expiration de 15 à 60 minutes combinées à des jetons d'actualisation à expiration longue. Ne placez pas de données sensibles dans le JWT : la charge utile est codée en Base64 et non chiffrée. Incluez uniquement l’identifiant, le rôle et la date limite. Configurez CORS correctement : n'utilisez pas origin : '*' en production. Utilisez CanMatch pour chaque module sensible chargé paresseux - empêche le téléchargement du bundle, pas seulement son affichage. Centralisez la gestion des erreurs HTTP dans l’intercepteur : évitez de dupliquer la logique de redirection de connexion dans chaque service ou composant. Conclusion La sécurité d'une application Angular est construite en couches. Route Guards ( CanActivate , CanMatch , CanDeactivate ) protègent la navigation et empêchent le chargement du code sensible. Les intercepteurs HTTP garantissent que chaque communication avec le backend se produit de manière authentifiée.

N'oubliez pas : la sécurité du frontend est la première ligne de défense pour l'utilisateur, mais la véritable sécurité réside dans le backend. Utilisez les deux niveaux et votre système sera robuste et professionnel.

💬 Notes des lecteurs

0 notes

Écrire une note

Partagez votre avis, une suggestion ou un compliment

Dernières notes

Aucune note pour le moment. Soyez le premier à commenter !