Создайте Route Guard для управления разрешениями
Многим приложениям приходится обрабатывать различные типы разрешений пользователей, таких как администраторы, менеджеры… В этом задании мы узнаем, как использовать защиту в Angular для ограничения доступа к определенным страницам на основе разрешений пользователя.
Мы начинаем вызов со следующего приложения. У нас есть список кнопок для входа в качестве разных пользователей, у каждого из которых есть свой набор разрешений. У нас также есть кнопка для входа в приложение. В зависимости от разрешений пользователя мы будем отображать соответствующую панель инструментов.
Angular предоставляет список встроенных охранников для защиты наших маршрутов: canLoad
, canActivate
, canDeactivate
, canActivateChild
и canMatch
.
Для решения этой задачи мы будем использовать защиту canMatch
, которая идеально подходит для нашего варианта использования.
Мы можем написать один и тот же маршрут несколько раз. Если первый маршрут в файле совпадает, router
перейдет к этому маршруту. В противном случае он проверит следующий. Давайте посмотрим, как работает canMatch
на очень простом примере:
{
path: 'enter',
canMatch: [() => false],
loadComponent: () => import('./dashboard/writer-reader.component'),
},
{
path: 'enter',
canMatch: [() => true],
loadComponent: () => import('./dashboard/client.component'),
},
{
path: 'enter',
loadComponent: () => import('./dashboard/everyone.component'),
},
В этом примере, если пользователь переходит в enter
, router
попытается сопоставить первый маршрут. Однако защита canMatch
возвращает false, поэтому router
попытается использовать второй маршрут, который возвращает true. Наконец, router
перейдет к ClientComponent
и не будет выполнять последующие маршруты.
Теперь давайте применим наши знания к нашим потребностям. Сначала нам нужно создать внедряемый сервис для реализации нашей логики защиты для управления разрешениями пользователей.
Вот реализация:
@Injectable({ providedIn: 'root' })
export class HasPermissionGuard implements CanMatch {
private router = inject(Router);
private userStore = inject(UserStore);
canMatch(route: Route): Observable<boolean | UrlTree> {
const accessRolesList: Role[] = route.data?.['roles'] ?? [];
const isAdmin: boolean = route.data?.['isAdmin'] ?? false;
return this.hasPermission$(isAdmin, accessRolesList);
}
private hasPermission$(isAdmin: boolean, accessRolesList: Role[]) {
return this.userStore.isUserLoggedIn$.pipe(
mergeMap((hasUser) => {
if (hasUser) {
if (isAdmin) {
return this.userStore.isAdmin$.pipe(map(Boolean));
} else if (accessRolesList.length > 0) {
return this.userStore
.hasAnyRole(accessRolesList)
.pipe(map(Boolean));
}
return of(false);
} else {
return of(this.router.parseUrl('no-user'));
}
})
);
}
}
Мы извлекаем roles
и свойства isAdmin
из атрибута данных маршрута.
Затем мы сначала проверяем, вошел ли пользователь в систему. Если нет, мы переходим на страницу no-user
.
Если пользователь вошел в систему, мы сравниваем роли пользователя с ролями, установленными на маршруте, чтобы определить, должен ли маршрутизатор перейти к этому маршруту или проверить следующий.
Как только наша защита как услуга будет завершена, мы можем создать наши маршруты следующим образом:
{
path: 'enter',
canMatch: [HasPermissionGuard],
data: {
isAdmin: true,
},
loadComponent: () => import('./dashboard/admin.component'),
},
{
path: 'enter',
canMatch: [HasPermissionGuard],
data: {
roles: ['MANAGER'],
},
loadComponent: () => import('./dashboard/manager.component'),
},
{
path: 'enter',
canMatch: [HasPermissionGuard],
data: {
roles: ['WRITER', 'READER'],
},
loadComponent: () => import('./dashboard/writer-reader.component'),
},
{
path: 'enter',
canMatch: [HasPermissionGuard],
data: {
roles: ['CLIENT'],
},
loadComponent: () => import('./dashboard/client.component'),
},
{
path: 'enter',
loadComponent: () => import('./dashboard/everyone.component'),
},
Начиная с Angular v.14.2 мы можем использовать функциональное программирование. Кроме того, защита как услуга будет объявлена устаревшей в версии 15.2.
Это дает нам следующую функцию. Чтобы внедрить наш сервис с помощью системы внедрения зависимостей Angular, теперь мы можем воспользоваться функцией inject
. (Примечание: функция inject
работает только внутри контекста зависимости)
export const hasAdminPermission = (isAdmin: boolean, accessRolesList: Role[]) => {
const userStore = inject(UserStore);
const router = inject(Router);
return userStore.isUserLoggedIn$.pipe(
mergeMap((hasUser) => {
if (hasUser) {
if (isAdmin) {
return userStore.isAdmin$.pipe(map(Boolean));
} else if (accessRolesList.length > 0) {
return userStore.hasAnyRole(accessRolesList).pipe(map(Boolean));
}
return of(false);
} else {
return of(router.parseUrl('no-user'));
}
})
);
};
Поскольку мы используем функцию, мы можем разделить нашу логику на две отдельные функции.
export const isAdmin = () => {
const userStore = inject(UserStore);
const router = inject(Router);
return userStore.isUserLoggedIn$.pipe(
mergeMap((hasUser) =>
iif(
() => hasUser,
userStore.isAdmin$.pipe(map(Boolean)),
of(router.parseUrl('no-user'))
)
)
);
};
export const hasRole = (accessRolesList: Role[]) => {
const userStore = inject(UserStore);
const router = inject(Router);
return userStore.isUserLoggedIn$.pipe(
mergeMap((hasUser) =>
iif(
() => hasUser,
userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
of(router.parseUrl('no-user'))
)
)
);
};
Таким образом, код легче понять, легче поддерживать и меньше подвержен ошибкам. Если мы хотим использовать обе функции для защиты одного маршрута, мы можем это сделать, так как защита принимает массив в качестве входных данных.
Теперь мы можем реорганизовать наше определение route
.
{
path: 'enter',
canMatch: [() => isAdmin()],
loadComponent: () => import('./dashboard/admin.component'),
},
{
path: 'enter',
canMatch: [() => hasRole(['MANAGER'])],
loadComponent: () => import('./dashboard/manager.component'),
},
{
path: 'enter',
canMatch: [() => hasRole(['WRITER', 'READER'])],
loadComponent: () => import('./dashboard/writer-reader.component'),
},
{
path: 'enter',
canMatch: [() => hasRole(['CLIENT'])],
loadComponent: () => import('./dashboard/client.component'),
},
{
path: 'enter',
loadComponent: () => import('./dashboard/everyone.component'),
},
- Лучший разработчик eXperience
- Менее подвержен ошибкам (больше нет необходимости в свойстве
data
типаany
) - Меньше стандартного кода
- Легче читать
- Более ремонтопригодный
Еще одно улучшение
Использование функции inject внутри функции может привести к ошибкам, если кто-то захочет повторно использовать функцию вне контекста зависимости. Чтобы избежать этого, мы можем установить две внедренные зависимости в качестве необязательных параметров:
export const hasRole = (accessRolesList: Role[], userStore = inject(UserStore), router = inject(Router)) => {
return userStore.isUserLoggedIn$.pipe(
mergeMap((hasUser) =>
iif(
() => hasUser,
userStore.hasAnyRole(accessRolesList).pipe(map(Boolean)),
of(router.parseUrl('no-user'))
)
)
);
};
Это дает еще одно преимущество: теперь вы можете легко протестировать свою функцию без использования TestBed.