Перейти к содержимому

17. Route Guards

Route Guards — это охранники маршрутов. Представь, что маршруты в твоём приложении — это двери в здании 🏢. Guards — это охрана: они проверяют, есть ли у пользователя пропуск, прежде чем пустить его. И не только “пустить”, но и “выпустить” (CanDeactivate) 🚪.


Пользователь хочет зайти на /admin/dashboard
[CanActivate: authGuard] ← авторизован?
[CanActivate: roleGuard] ← есть роль admin?
[Resolve: dashboardData] ← загружаем данные
Компонент отображается ✅

Guards позволяют:

  • Защитить маршруты от неавторизованных пользователей
  • Запросить подтверждение при уходе со страницы (несохранённые данные)
  • Загрузить данные до отображения компонента (Resolver)
  • Перенаправить пользователя на нужную страницу

Функциональный Guard (Angular 15+, рекомендуется):

auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true; // ✅ разрешаем навигацию
}
// Сохраняем URL для редиректа после логина
router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false; // ❌ блокируем навигацию
};
// Или возвращаем UrlTree (более элегантно!)
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
// Подключение в маршруте
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }

Классовый Guard (устарел, но встречается в старых проектах):

// ❌ Устаревший подход (Angular < 15)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (this.auth.isLoggedIn()) return true;
this.router.navigate(['/login']);
return false;
}
}

Guard с проверкой роли:

role.guard.ts
export const roleGuard = (requiredRole: string): CanActivateFn => {
return () => {
const auth = inject(AuthService);
const router = inject(Router);
const user = auth.currentUser();
if (!user) return router.createUrlTree(['/login']);
if (user.role !== requiredRole) return router.createUrlTree(['/forbidden']);
return true;
};
};
// Использование
{ path: 'admin', component: AdminComponent, canActivate: [authGuard, roleGuard('admin')] }

Идеально для форм с несохранёнными данными. Guard спрашивает пользователя перед уходом:

unsaved-changes.guard.ts
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (!component.hasUnsavedChanges()) return true;
return confirm('У вас есть несохранённые изменения. Уйти?');
// В реальном приложении лучше использовать Modal Dialog
};
// Компонент реализует интерфейс
@Component({...})
export class EditProfileComponent implements HasUnsavedChanges {
form = this.fb.group({ name: [''], email: [''] });
private savedValue = this.form.value;
hasUnsavedChanges(): boolean {
return JSON.stringify(this.form.value) !== JSON.stringify(this.savedValue);
}
save() {
this.savedValue = this.form.value;
this.http.put('/api/profile', this.form.value).subscribe();
}
}
// В маршруте
{
path: 'profile/edit',
component: EditProfileComponent,
canDeactivate: [unsavedChangesGuard]
}

👨‍👩‍👧 CanActivateChild: защита дочерних маршрутов

Заголовок раздела «👨‍👩‍👧 CanActivateChild: защита дочерних маршрутов»

Защищает сразу все дочерние маршруты родителя — не нужно добавлять guard к каждому:

export const adminGuard: CanActivateChildFn = (childRoute, state) => {
return inject(AuthService).hasAdminAccess()
? true
: inject(Router).createUrlTree(['/forbidden']);
};
// Применяется ко ВСЕМ children
{
path: 'admin',
component: AdminLayoutComponent,
canActivateChild: [adminGuard], // Защищает /admin/*, /admin/*/*, etc.
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: UsersComponent },
{ path: 'settings', component: SettingsComponent },
// ^ Все они защищены adminGuard!
]
}

CanMatch заменяет устаревший CanLoad. Он запрещает совпадение маршрута — если guard возвращает false, Angular переходит к следующему маршруту в конфигурации:

export const premiumGuard: CanMatchFn = () => {
return inject(UserService).isPremium();
};
// Конфигурация маршрутов
const routes: Routes = [
// Для premium пользователей — расширенный компонент
{
path: 'dashboard',
component: PremiumDashboardComponent,
canMatch: [premiumGuard]
},
// Для обычных пользователей — базовый компонент (URL тот же!)
{
path: 'dashboard',
component: BasicDashboardComponent
}
];

Resolve загружает данные ДО навигации — компонент уже получает готовые данные:

user.resolver.ts
export const userResolver: ResolveFn<User> = (route) => {
const id = route.paramMap.get('id')!;
const service = inject(UserService);
const router = inject(Router);
return service.getUser(id).pipe(
catchError(() => {
router.navigate(['/not-found']);
return EMPTY;
})
);
};
{ path: 'users/:id', component: UserComponent, resolve: { user: userResolver } }
// Компонент — данные уже есть!
export class UserComponent {
user = inject(ActivatedRoute).snapshot.data['user'] as User;
}

auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUser$ = new BehaviorSubject<User | null>(null);
isLoggedIn(): boolean {
return this.currentUser$.getValue() !== null;
}
currentUser(): User | null {
return this.currentUser$.getValue();
}
login(username: string, password: string): Observable<User> {
return this.http.post<User>('/api/login', { username, password }).pipe(
tap(user => {
this.currentUser$.next(user);
localStorage.setItem('token', user.token);
})
);
}
logout() {
this.currentUser$.next(null);
localStorage.removeItem('token');
this.router.navigate(['/login']);
}
}
// auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
// Запоминаем куда хотел попасть пользователь
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// login.component.ts — редирект после логина
@Component({...})
export class LoginComponent {
private returnUrl = inject(ActivatedRoute).snapshot.queryParams['returnUrl'] ?? '/';
login() {
this.auth.login(this.form.value).subscribe(() => {
this.router.navigateByUrl(this.returnUrl);
});
}
}

Навигация: /dashboard
1. CanDeactivate (текущего компонента) ← "Можно уйти?"
2. CanMatch (нового маршрута) ← "Маршрут совпадает?"
3. CanActivateChild (родителя) ← "Дочерний маршрут доступен?"
4. CanActivate (нового компонента) ← "Компонент доступен?"
5. Resolve (загрузка данных) ← "Данные готовы?"
6. Компонент активируется ✅
⚠️ Если несколько CanActivate:
canActivate: [authGuard, roleGuard('admin')]
→ Выполняются параллельно (если не зависят друг от друга)
→ Если любой вернёт false — навигация отменяется

Попробуйте концепцию в интерактивном редакторе: