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

🔐 Sicurezza nei router di Angular: Guards e Interceptor in azione

Introduzione: La Sicurezza è un Requisito, Non un'Opzione

Le applicazioni Angular moderne gestiscono dati sensibili: profili utente, cruscotti aziendali, pannelli di amministrazione. Lasciare queste sezioni prive di protezione espone l'applicazione a accessi non autorizzati, furto di token e violazioni della privacy. Angular fornisce un sistema di sicurezza del routing integrato e componibile: i Route Guards e gli HTTP Interceptors.

Questa guida illustra come costruire un sistema di protezione a più livelli usando gli strumenti moderni di Angular 17+. Vedremo ogni meccanismo in dettaglio, con esempi di codice reali pronti all'uso in produzione.

Argomenti: #Angular #Guards #Interceptor #WebDevelopment #FrontendDevelopment

Architettura della Sicurezza Lato Frontend

Prima di entrare nel codice, è fondamentale capire come si stratifica la sicurezza in un'applicazione Angular:

  • Livello 1 – Route Guards: controllano se l'utente può navigare verso una certa rotta prima ancora che il componente venga creato.
  • Livello 2 – HTTP Interceptors: aggiungono token di autenticazione a ogni richiesta in uscita e gestiscono le risposte di errore (401, 403) globalmente.
  • Livello 3 – Backend Validation: il backend deve sempre rivalidare autenticazione e autorizzazione. La sicurezza frontend è una difesa aggiuntiva, non l'unica.

Sezione 1: CanActivate — Proteggere le Rotte

CanActivate è il guard più comune. Viene eseguito prima che Angular attivi una rotta e il suo componente associato. Se restituisce false o un UrlTree, la navigazione viene bloccata e l'utente viene reindirizzato.

Da Angular 15 in poi, l'approccio raccomandato è la guard function (CanActivateFn) con inject() invece delle classi con interfacce deprecate. Questo elimina il boilerplate e sfrutta pienamente la dependency injection funzionale.

Guard di Autenticazione Base

// core/guards/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
) => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  if (auth.isAuthenticated()) {
    return true;
  }

  // Preserva l'URL richiesto per redirigere dopo il login
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url },
  });
};

Guard per il Ruolo Admin

// core/guards/admin.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const adminGuard: CanActivateFn = (route, state) => {
  const auth   = inject(AuthService);
  const router = inject(Router);

  if (!auth.isAuthenticated()) {
    return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
  }
  if (!auth.hasRole('admin')) {
    return router.createUrlTree(['/403']);
  }
  return true;
};

AuthService con JWT e Signals

// core/services/auth.service.ts
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { tap } from 'rxjs/operators';

interface TokenPayload {
  sub: string; email: string; role: 'admin' | 'user'; iat: number; exp: number;
}

function decodeJwt(token: string): TokenPayload | null {
  try {
    const base64Payload = token.split('.')[1];
    const decoded = atob(base64Payload.replace(/-/g, '+').replace(/_/g, '/'));
    return JSON.parse(decoded) as TokenPayload;
  } catch { return null; }
}

@Injectable({ providedIn: 'root' })
export class AuthService {
  private static readonly TOKEN_KEY = 'portfolio_token';
  private readonly _payload = signal<TokenPayload | null>(this.readStoredPayload());
  readonly currentUser = this._payload.asReadonly();
  readonly isAdmin = computed(() => this._payload()?.role === 'admin');

  constructor(private http: HttpClient, private router: Router) {}

  login(email: string, password: string) {
    return this.http
      .post<{ access_token: string }>('/api/auth/login', { email, password })
      .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 | null { return localStorage.getItem(AuthService.TOKEN_KEY); }

  isAuthenticated(): boolean {
    const token = this.getToken();
    if (!token) return false;
    const payload = decodeJwt(token);
    return payload ? payload.exp > Date.now() / 1000 + 30 : false;
  }

  hasRole(role: string): boolean { return this._payload()?.role === role; }
  private readStoredPayload(): TokenPayload | null {
    const token = localStorage.getItem(AuthService.TOKEN_KEY);
    return token ? decodeJwt(token) : null;
  }
}

Sezione 2: CanMatch — Sicurezza per i Moduli Lazy-Loaded

Con i moduli lazy-loaded, Angular scarica il bundle JavaScript del modulo solo quando l'utente naviga verso quella rotta. Se non si protegge il caricamento, un utente non autorizzato può comunque scaricare il codice della dashboard admin semplicemente modificando l'URL.

CanMatch risolve questo problema: se restituisce false, Angular non valuta la rotta e non scarica mai il bundle associato.

// core/guards/can-match-admin.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const canMatchAdmin: CanMatchFn = () => {
  const auth   = inject(AuthService);
  const router = inject(Router);
  if (auth.isAuthenticated() && auth.hasRole('admin')) return true;
  return router.createUrlTree(['/login']);
};

Configurazione delle Rotte con CanActivate + CanMatch

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard }     from './core/guards/auth.guard';
import { adminGuard }    from './core/guards/admin.guard';
import { canMatchAdmin } from './core/guards/can-match-admin.guard';

export const APP_ROUTES: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
    canActivate: [adminGuard],
    canMatch:    [canMatchAdmin],
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./features/dashboard/dashboard.component').then(c => c.DashboardComponent),
    canActivate: [authGuard],
  },
  { path: 'login', loadComponent: () => import('./features/auth/login.component').then(c => c.LoginComponent) },
  { path: '',   redirectTo: 'dashboard', pathMatch: 'full' },
  { path: '**', redirectTo: 'login' },
];

Sezione 3: CanDeactivate — Proteggere i Form Non Salvati

Uno scenario frequente: l'utente sta compilando un lungo form e per errore clicca su un link di navigazione. Senza CanDeactivate, tutte le modifiche vengono perse silenziosamente. Con questo guard, appare una richiesta di conferma.

Guard CanDeactivate Generico e Riutilizzabile

// core/guards/dirty-form.guard.ts
import { CanDeactivateFn } from '@angular/router';

export interface DirtyFormComponent { isDirty(): boolean; }

export const dirtyFormGuard: CanDeactivateFn<DirtyFormComponent> = component => {
  if (!component.isDirty()) return true;
  return window.confirm('Hai modifiche non salvate che andranno perse. Continuare?');
};

Componente con Form Reattivo

// features/profile/profile-edit.component.ts
import { Component, OnInit } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { DirtyFormComponent } from '../../core/guards/dirty-form.guard';

@Component({ selector: 'app-profile-edit', standalone: true, imports: [ReactiveFormsModule], templateUrl: './profile-edit.component.html' })
export class ProfileEditComponent implements OnInit, DirtyFormComponent {
  readonly form = this.fb.group({
    displayName: ['', [Validators.required, Validators.minLength(2)]],
    bio:         ['', Validators.maxLength(500)],
  });
  private savedSnapshot = '';
  constructor(private fb: 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; }
}

Sezione 4: HTTP Interceptors — Sicurezza Trasversale

Gli HTTP Interceptors intercettano ogni richiesta HTTP in uscita e ogni risposta in arrivo. Sono il luogo ideale per aggiungere il token JWT, gestire i refresh automatici e centralizzare la logica di gestione degli errori.

Interceptor di Autenticazione JWT

// core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth   = inject(AuthService);
  const router = inject(Router);
  const token  = auth.getToken();

  const authorizedReq = token
    ? req.clone({ setHeaders: { Authorization: `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 per Spinner di Caricamento Globale

// core/interceptors/loading.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { finalize } from 'rxjs/operators';
import { LoadingService } from '../services/loading.service';

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.headers.has('X-Background-Request')) return next(req);
  const loading = inject(LoadingService);
  loading.increment();
  return next(req).pipe(finalize(() => loading.decrement()));
};

Registrazione in app.config.ts

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http';
import { APP_ROUTES }         from './app.routes';
import { authInterceptor }    from './core/interceptors/auth.interceptor';
import { loadingInterceptor } from './core/interceptors/loading.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(APP_ROUTES, withComponentInputBinding()),
    provideHttpClient(withFetch(), withInterceptors([loadingInterceptor, authInterceptor])),
  ],
};

Best Practices per Sicurezza e Performance

  • Doppia verifica backend: i guard proteggono l'UX, ma ogni endpoint API deve validare autonomamente il JWT. Non fare mai affidamento solo sul frontend.
  • Token con scadenza breve: usa JWT con scadenza 15-60 minuti combinati con refresh token a lunga scadenza.
  • Non mettere dati sensibili nel JWT: il payload è codificato in Base64, non cifrato. Includi solo ID, ruolo e scadenza.
  • Configura CORS correttamente: non usare origin: '*' in produzione.
  • Usa CanMatch per ogni modulo lazy-loaded sensibile: impedisce il download del bundle, non solo la visualizzazione.
  • Centralizza la gestione degli errori HTTP nell'interceptor: evita di duplicare la logica di redirect al login in ogni service o componente.

Conclusione

La sicurezza di un'applicazione Angular si costruisce in strati. I Route Guards (CanActivate, CanMatch, CanDeactivate) proteggono la navigazione e impediscono il caricamento di codice sensibile. Gli HTTP Interceptors garantiscono che ogni comunicazione con il backend avvenga in modo autenticato.

Ricorda: la sicurezza frontend è la prima linea di difesa per l'utente, ma la vera sicurezza risiede nel backend. Usa entrambi i livelli e il tuo sistema sarà robusto e professionale.

💬 Note dei lettori

0 note

Scrivi una nota

Condividi la tua opinione, un suggerimento o un complimento

Ultime note

Nessuna nota ancora. Sii il primo a commentare!