Introducción: la seguridad es un requisito, no una opción Las aplicaciones modernas de Angular manejan datos confidenciales: perfiles de usuario, paneles de empresa, paneles de administración. Dejar estas secciones desprotegidas expone su aplicación a acceso no autorizado, robo de tokens y violaciones de privacidad. Angular proporciona un sistema de seguridad de enrutamiento integrado y componible: Route Guards e Interceptores HTTP. Esta guía le muestra cómo construir un sistema de seguridad en capas utilizando herramientas modernas de Angular 17+. Analizaremos cada mecanismo en detalle, con ejemplos de código real listos para su uso en producción. Temas: #Angular #Guards #Interceptor #Desarrollo web #Desarrollo frontend Arquitectura de seguridad del lado frontend Antes de entrar en el código, es esencial comprender cómo se estratifica la seguridad en una aplicación Angular: Nivel 1 – Guardias de ruta: controlan si el usuario puede navegar a una ruta determinada incluso antes de que se cree el componente. Nivel 2: interceptores HTTP: agregue tokens de autenticación a cada solicitud saliente y maneje las respuestas de error (401, 403) globalmente. Nivel 3 – Validación del backend: el backend siempre debe validar la autenticación y autorización. La seguridad frontend es una defensa adicional, no la única. Sección 1: CanActivate: protección de rutas CanActivate es la protección más común. Se ejecuta antes de que Angular active una ruta y su componente asociado. Si devuelve false o UrlTree, se bloquea la navegación y se redirige al usuario. Desde Angular 15 en adelante, el enfoque recomendado es la función de protección ( CanActivateFn ) con inject() en lugar de clases con interfaces obsoletas. Esto elimina el texto repetitivo y aprovecha al máximo la inyección de dependencia funcional. Guardia de autenticación básica // core/guards/auth.guard.
ts importar {inyectar} desde '@angular/core'; importar {CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot} desde '@angular/router'; importar {AuthService} desde '../services/auth.service'; exportar const authGuard: CanActivateFn = (ruta: ActivatedRouteSnapshot, estado: RouterStateSnapshot,) => { const auth = inject(AuthService); enrutador constante = inyectar (enrutador); if (auth.isAuthenticated()) { devolver verdadero; } // Conserva la URL solicitada para la redirección después de iniciar sesión return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url }, }); }; Guardia para el rol de administrador // core/guards/admin.guard.ts import { inject } from '@angular/core'; importar {CanActivateFn, Router} desde '@angular/router'; importar {AuthService} desde '../services/auth.service'; exportar const adminGuard: CanActivateFn = (ruta, estado) => { const auth = inject(AuthService); enrutador constante = inyectar (enrutador); if (!auth.isAuthenticated()) { return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); } if (!auth.hasRole('admin')) { return router.createUrlTree(['/403']); } devuelve verdadero; }; AuthService con JWT y señales // core/services/auth.service.ts import {Inyectable, señal, calculada} desde '@angular/core'; importar {HttpClient} desde '@angular/common/http'; importar {Enrutador} desde '@angular/router'; importar {toque} desde 'rxjs/operadores'; interfaz TokenPayload { sub: cadena; correo electrónico: cadena; rol: 'administrador' | 'usuario'; iat: número; exp: número; } función decodeJwt(token: cadena): TokenPayload | nulo { intentar { const base64Payload = token.split('.')[1]; const decodificado = atob(base64Payload.replace(/-/g, '+').replace(/_/g, '/')); devolver JSON.parse (decodificado) como TokenPayload; } atrapar {retorno nulo; } } @Injectable({ provideIn: 'root' }) export class AuthService {privado estático de solo lectura TOKEN_KEY = 'portfolio_token'; privado de solo lectura _payload = señal(this.readStoredPayload()); usuario actual de solo lectura = this._payload.
asReadonly(); solo lectura isAdmin = calculado(() => this._payload()?.role === 'admin'); constructor (http privado: HttpClient, enrutador privado: enrutador) {} iniciar sesión (correo electrónico: cadena, contraseña: cadena) { return this.http .post<{ access_token: string }>('/api/auth/login', { correo electrónico, contraseña }) .pipe(tap(({ access_token }) => { localStorage.setItem(AuthService.TOKEN_KEY, access_token); this._payload.set (decodeJwt (access_token)); } cerrar sesión (): void { localStorage.removeItem (AuthService.TOKEN_KEY); this._payload.set (null); isAuthenticated(): boolean { const token = this.getToken(); if (!token) devuelve falso; const payload = decodeJwt(token); return payload.exp > Date.now() / 1000 + 30 : false } hasRole(role: string): boolean { return this._payload()?.role === rol; TokenPayload | null { const token = localStorage.getItem(AuthService.TOKEN_KEY); return token ? decodeJwt(token): null; } } Sección 2: CanMatch: seguridad para formularios cargados de forma diferida, Angular descarga el paquete de JavaScript del formulario solo cuando el usuario navega a esa ruta, un usuario no autorizado aún puede descargar el código del panel de administración. la URL. CanMatch resuelve este problema: si devuelve false, Angular no evalúa la ruta y nunca descarga el paquete asociado. // core/guards/can-match-admin.guard.ts import { inject } from '@angular/core'; import { CanMatchFn, Router } from '@angular/router'; CanMatchFn = () => { const auth = inject(AuthService); const router = inject(Router); if (auth.isAuthenticated() && auth.hasRole('admin')) return router.createUrlTree(['/login'] });
ts import {Rutas} desde '@angular/router'; importar { authGuard } desde './core/guards/auth.guard'; importar { adminGuard } desde './core/guards/admin.guard'; importar {canMatchAdmin} desde './core/guards/can-match-admin.guard'; export const APP_ROUTES: Rutas = [ { ruta: 'admin', loadChildren: () => import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES), canActivate: [adminGuard], canMatch: [canMatchAdmin], }, { ruta: 'tablero', loadComponent: () => import('./features/dashboard/dashboard.component').then(c => c.DashboardComponent), canActivate: [authGuard], }, { ruta: 'iniciar sesión', loadComponent: () => import('./features/auth/login.component').then(c => c.LoginComponent) }, { ruta: '', redirigir a: 'dashboard', pathMatch: 'completo' }, { ruta: '**', redirigir a: 'iniciar sesión' }, ]; Sección 3: CanDeactivate: protección de formularios no guardados Un escenario común: el usuario está completando un formulario largo y accidentalmente hace clic en un enlace de navegación. Sin CanDeactivate, todos los cambios se pierden silenciosamente. Con esta mirada, aparece una solicitud de confirmación. Guard CanDeactivate Genérico y Reutilizable // core/guards/dirty-form.guard.ts import { CanDeactivateFn } from '@angular/router'; interfaz de exportación DirtyFormComponent { isDirty(): booleano; } export const dirtyFormGuard: CanDeactivateFn = componente => { if (!component.isDirty()) return true; return window.confirm('Tiene cambios no guardados que se perderán. ¿Continuar?'); }; Componente con formulario responsivo // características/perfil/profile-edit.component.ts import { Componente, OnInit } de '@angular/core'; importar { NonNullableFormBuilder, ReactiveFormsModule, Validators } desde '@angular/forms'; importar {DirtyFormComponent} desde '../../core/guards/dirty-form.guard'; @Component({ selector: 'app-profile-edit', standalone: true, imports: [ReactiveFormsModule], templateUrl: './profile-edit.component.html' }) clase de exportación ProfileEditComponent implementa OnInit, DirtyFormComponent { formulario de solo lectura = this.
fb.group({ displayName: ['', [Validators.required, Validators.minLength(2)]], biografía: ['', Validators.maxLength(500)], }); instantánea guardada privada = ''; constructor(FB privado: NonNullableFormBuilder) {} ngOnInit(): void { this.savedSnapshot = JSON.stringify(this.form.getRawValue()); } guardar(): void { this.savedSnapshot = JSON.stringify(this.form.getRawValue()); } isDirty(): boolean { return JSON.stringify(this.form.getRawValue()) !== this.savedSnapshot; } } Sección 4: Interceptores HTTP: seguridad entre canales Los interceptores HTTP interceptan cada solicitud HTTP saliente y cada respuesta entrante. Son el lugar ideal para agregar el token JWT, gestionar actualizaciones automáticas y centralizar la lógica de manejo de errores. Interceptor de autenticación JWT // core/interceptors/auth.interceptor.ts import { HttpInterceptorFn, HttpErrorResponse } de '@angular/common/http'; importar {inyectar} desde '@angular/core'; importar {catchError, throwError} desde 'rxjs'; importar {AuthService} desde '../services/auth.service'; importar {Enrutador} desde '@angular/router'; exportar const authInterceptor: HttpInterceptorFn = (req, siguiente) => { const auth = inject(AuthService); enrutador constante = inyectar (enrutador); token constante = auth.getToken(); const autorizadoReq = token? req.clone({ setHeaders: { Autorización: `Portador ${token}` } }): req; return next(authorizedReq).pipe( catchError((err: HttpErrorResponse) => { if (err.status === 401) { auth.logout(); router.navigate(['/login'], { queryParams: { motivo: 'sesión caducada' } }); } if (err.status === 403) { router.navigate(['/403']); } return throwError(() => err }), }; Interceptor para carga global // core/interceptors/loading.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; LoadingService } de '../services/loading.service'; exportar const loadingInterceptor: HttpInterceptorFn = (req, siguiente) => { if (req.
headers.has('X-Background-Request')) regresa siguiente(req); carga constante = inyectar (LoadingService); cargando.incremento(); return next(req).pipe(finalize(() => cargando.decrement())); }; Iniciar sesión app.config.ts // app.config.ts import { ApplicationConfig } from '@angular/core'; importar {proporcionarRouter, withComponentInputBinding} desde '@angular/router'; importar {proporcionarHttpClient, withInterceptors, withFetch} desde '@angular/common/http'; importar {APP_ROUTES} desde './app.routes'; importar { authInterceptor } desde './core/interceptores/auth.interceptor'; importar {loadingInterceptor} desde './core/interceptores/loading.interceptor'; exportar const appConfig: ApplicationConfig = { proveedores: [ provideRouter(APP_ROUTES, withComponentInputBinding()), provideHttpClient(withFetch(), withInterceptors([loadingInterceptor, authInterceptor])), ], }; Mejores prácticas de seguridad y rendimiento Doble verificación de backend: los guardias protegen la UX, pero cada punto final de API debe validar de forma independiente el JWT. Nunca confíes únicamente en la interfaz. Tokens de vencimiento corto: use JWT con vencimiento de 15 a 60 minutos combinados con tokens de actualización de vencimiento prolongado. No coloque datos confidenciales en el JWT: la carga útil está codificada en Base64, no cifrada. Incluya solo identificación, función y fecha límite. Configure CORS correctamente: no utilice origin: '*' en producción. Utilice CanMatch para cada módulo sensible con carga diferida: evita que el paquete se descargue, no solo se muestre. Centralice el manejo de errores HTTP en el interceptor: evite duplicar la lógica de redireccionamiento de inicio de sesión en cada servicio o componente. Conclusión La seguridad de una aplicación Angular se construye en capas. Los protectores de ruta (CanActivate, CanMatch, CanDeactivate) protegen la navegación y evitan que se cargue código confidencial. Los interceptores HTTP garantizan que cada comunicación con el backend se produzca de forma autenticada.
Recuerde: la seguridad del frontend es la primera línea de defensa para el usuario, pero la verdadera seguridad reside en el backend. Utilice ambos niveles y su sistema será robusto y profesional.