Angular 5/6: защита маршрута (защита маршрута) без перенаправления на ошибочный маршрут

У меня немного огурца. Я использую защиту маршрута (реализуя интерфейс CanActivate), чтобы проверить, предоставлен ли пользователю доступ к определенному маршруту:

const routes: Routes = [
    {
        path: '',
        component: DashboardViewComponent
    },
    {
        path: 'login',
        component: LoginViewComponent
    },
    {
        path: 'protected/foo',
        component: FooViewComponent,
        data: {allowAccessTo: ['Administrator']},
        canActivate: [RouteGuard]
    },
    {
        path: '**',
        component: ErrorNotFoundViewComponent
    }
];

Теперь он отлично работает для защиты маршрута '/protected/foo' от активации, но я хотел бы сообщить пользователю, что маршрут, к которому он пытается получить доступ, запрещен (аналогично 403 Forbidden, который вы можете получить с сервера).

Проблема: Как я могу показать пользователю это специальное представление ошибок, не перенаправляя его на маршрут ошибок, какой из множества источников, которые я нашел, является предпочтительным? И как мне по-прежнему использовать мой RouteGuard, фактически не загружая запрещенный маршрут, потому что, если я проверяю доступ внутри моего FooViewComponent и отображаю другое представление, это как бы побеждает точку наличия RouteGuard в первую очередь.

В идеале я хотел бы, чтобы мой RouteGuard не только возвращал false в методе canActivate(), но и полностью заменял компонент, скажем, ErrorForbiddenViewComponent. Но я понятия не имею, как это сделать, или это возможно. Любые альтернативы?

Вот так теперь выглядит мой route guard:

import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {AuthService} from '../services/auth.service';

@Injectable()
export class RouteGuard implements CanActivate {

    constructor(
        private router: Router,
        private auth: AuthService
    ) {}

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const { auth, router } = this;
        const { allowAccessTo } = next.data;
        const identity = auth.getIdentity();
        if (
            identity &&
            allowAccessTo.indexOf(identity.role)
        ) {
            // all good, proceed with activating route
            return true;
        }
        if (identity) {
            // TODO show ErrorForbiddenViewComponent instead of redirecting
            console.log('403 Forbidden >>', next);
        }
        else { 
            // not logged in: redirect to login page with the return url
            const [returnUrl, returnQueryParams] = state.url.split('?');
            console.log('401 Unauthorised >>', returnUrl, returnQueryParams, next);
            router.navigate(['/login'], {queryParams: {returnUrl, returnQueryParams}});
        }
        return false;
    }
}

Поэтому я просто предотвращаю загрузку маршрута, но не перенаправляю. Я перенаправляю только незарегистрированных посетителей на маршрут входа.

Рассуждение:

  • Маршруты должны отражать определенное состояние приложения — посещение URL-адреса маршрута должно воссоздавать это состояние.
  • Наличие маршрутов ошибок (кроме 404 Not Found) означает, что ваше приложение может фактически воссоздавать состояния ошибок. Это не имеет смысла, так как зачем вам сохранять состояние ошибки как состояние вашего приложения? В целях отладки следует использовать журналы (консольные или серверные), повторный просмотр страницы с ошибкой (т.е. обновление страницы) может помешать этому.
  • Кроме того, перенаправляя приложение на маршрут ошибок, оно должно предоставить пользователю некоторое представление об ошибке. В этом отношении либо какой-либо параметр необходимо будет передать через URL-адрес, либо (что гораздо хуже) сохранить состояние ошибки в какой-либо службе ошибок и получить его при доступе к маршруту ошибки.
  • Кроме того, игнорирование RouteGuard и простая загрузка компонента и проверка доступа внутри него могут привести к загрузке некоторых дополнительных зависимостей, которые в любом случае не будут использоваться (поскольку пользователю это не разрешено), что делает всю ленивую загрузку намного сложнее.

У кого-нибудь есть какое-то решение для этого? Мне также интересно, как получилось, что после столь долгого существования Angular 2+ ни у кого раньше не было такой ситуации? У всех нормально с перенаправлением?

Также имейте в виду, что хотя в настоящее время я использую FooViewComponent синхронно, это может измениться в будущем!


person Ivan Hušnjak    schedule 22.05.2018    source источник
comment
Как насчет использования ComponentFactory, как описано в blog.mgechev.com/2015/09/30/ и заменить сам компонент на AccessForbidden   -  person Tarun Lalwani    schedule 29.05.2018
comment
@TarunLalwani Если вы хотите заработать эти 200 повторений, дайте ответ. Я также должен предупредить вас, что функциональность NG2 давно устарела, поэтому для NG5/6 этот код нуждается в серьезной переработке. С другой стороны, я смотрю на динамическую загрузку компонентов - возможно, это дает лучшее понимание   -  person Ivan Hušnjak    schedule 30.05.2018
comment
Я не угловой разработчик, поэтому разместил направление   -  person Tarun Lalwani    schedule 30.05.2018
comment
dzone.com/articles/implementing-guard-in-angular-5- приложение   -  person kumbhani bhavesh    schedule 30.08.2018


Ответы (4)


Когда-то я работал над подобной проблемой.

Делюсь своим stackblitz poc, где я создал -

  • Аутентифицированный компонент (с защитой)
  • Компонент входа
  • Охранник разрешений
  • Маршрут (/auth маршрут предоставляется с PermissionGuardService охраной)

Охранник оценивает тип пользователя и соответствующим образом обрабатывает перенаправление/ошибку.

Варианты использования -

  • Пользователь не вошел в систему (shows a toast with log in message)
  • Пользователь не является администратором (shows a toast with unauthorised message)
  • Пользователь является администратором (show a toast with success messaage)

Я сохранил пользователя в локальном хранилище.

РЕДАКТИРОВАТЬ - ДЕМО введите здесь описание изображения

Дайте мне знать, если вам нужна специальная обработка, и я обновлю кодовую базу.

Ваше здоровье!

person planet_hunter    schedule 01.06.2018
comment
Ваше решение основано на специальной коммуникационной службе (ToasterService), которая сохраняет сообщение об ошибке между перенаправлениями. Это то, чего я пытаюсь избежать. Это сообщение не является постоянным состоянием приложения (его временным), и перенаправление в то же место только для отображения того же сообщения об ошибке также является довольно хакерским способом решения этой проблемы. Если я могу предложить вам перенаправлять незарегистрированных пользователей только через PermissionGuard и использовать AuthenticatedComponent для фактической блокировки доступа (Запрещено) - person Ivan Hušnjak; 04.06.2018

Посмотрев на пример angular2 предоставлено Таруном Лалвани в комментариях к вопросу и после более глубокого изучения статьи о динамическом загрузчике компонентов в документах Angular мне удалось применить его к моему коду:

Я больше не использую свой RouteGuard при указании маршрутов:

{
     path: 'protected/foo',
     component: FooViewComponent,
     data: {allowAccessTo: ['Administrator']}, // admin only
     canActivate: [RouteGuard]
},

Вместо этого я создал специальный RouteGuardComponent и вот как я его использую:

{
    path: 'protected/foo',
    component: RouteGuardComponent,
    data: {component: FooViewComponent, allowAccessTo: ['Administrator']}
},

Это код RouteGuardComponent:

@Component({
    selector: 'app-route-guard',
    template: '<ng-template route-guard-bind-component></ng-template>
    // note the use of special directive ^^
})
export class RouteGuardComponent implements OnInit {

    @ViewChild(RouteGuardBindComponentDirective)
    bindComponent: RouteGuardBindComponentDirective;
    // ^^ and here we bind to that directive instance in template

    constructor(
        private auth: AuthService,
        private route: ActivatedRoute,
        private componentFactoryResolver: ComponentFactoryResolver
    ) {
    }

    ngOnInit() {
        const {auth, route, componentFactoryResolver, bindComponent} = this;
        const {component, allowAccessTo} = route.snapshot.data;
        const identity = auth.getIdentity();
        const hasAccess = identity && allowAccessTo.indexOf(identity.role);
        const componentFactory = componentFactoryResolver.resolveComponentFactory(
            hasAccess ?
               component : // render component
               ErrorForbiddenViewComponent // render Forbidden view
        );
        // finally use factory to create proper component
        routeGuardBindComponentDirective
            .viewContainerRef
            .createComponent(componentFactory);
    }

}

Кроме того, для этого требуется определить специальную директиву (я уверен, что это можно сделать каким-то другим способом, но я только что применил этот пример динамического компонента из документации Angular):

@Directive({
    selector: '[route-guard-bind-component]'
})
export class RouteGuardBindComponentDirective {
    constructor(public viewContainerRef: ViewContainerRef) {}
}

Это не полный ответ на мой собственный вопрос (но это начало), поэтому, если кто-то предоставит что-то лучшее (например, способ по-прежнему использовать canActivate и возможность ленивой загрузки), я обязательно приму это во внимание.

person Ivan Hušnjak    schedule 30.05.2018

Ваш RouteGuard может внедрить любой сервис, который вы используете для модальных окон, а .canActivate() может открыть модальное окно без перенаправления, чтобы информировать пользователя, не нарушая текущее состояние приложения.

Для этого мы используем toastr и его оболочку angular, так как он создает немодальное всплывающее окно, которое самозакрывается через столько секунд, что кнопки OK/Отмена не нужны.

person Ron Newcomb    schedule 01.06.2018
comment
Мне лично не нравится всплывающее окно, потому что оно также вызывает проблемы, когда пользователь приходит извне, т. е. щелкает ссылку в письме. Но я понимаю, куда вы идете (проверьте ответ @planet_hunter). Так же, как и мой, он не универсален, и он либо подойдет вам, либо нет. У меня всплывающее окно не работает - person Ivan Hušnjak; 04.06.2018

Я недавно столкнулся с той же проблемой. В конце концов, мне не удалось сделать это с помощью CanActivate guard, поэтому я реализовал логику авторизации в компоненте, который содержит <router-outlet>.

Вот его шаблон:

<div class="content">
  <router-outlet *ngIf="(accessAllowed$ | async) else accessDenied"></router-outlet>
</div>
<ng-template #accessDenied>
  <div class="message">
    <mat-icon>lock</mat-icon>
    <span>Access denied.</span>
  </div>
</ng-template>

И его исходный код:

import { ActivatedRoute, ActivationStart, Router } from '@angular/router';
import { filter, switchMap, take } from 'rxjs/operators';
import { merge, Observable, of } from 'rxjs';
import { Component } from '@angular/core';

@Component({
  selector: 'app-panel-content',
  templateUrl: './content.component.html',
  styleUrls: ['./content.component.scss'],
})
export class PanelContentComponent {

  /**
   * A stream of flags whether access to current route is permitted.
   */
  accessAllowed$: Observable<boolean>;

  constructor(
    permissions: UserPermissionsProviderContract, // A service for accessing user permissions; implementation omitted
    route: ActivatedRoute,
    router: Router,
  ) {
    const streams: Observable<boolean>[] = [];

    /*
    The main purpose of this component is to replace `<router-outlet>` with "Access denied" 
    message, if necessary. Such logic will be universal for all possible route components, and 
    doesn't require any additional components - you will always have at least one component with
    `<router-outlet>`.

    This component contains `<router-outlet>`, which by definition means that all possible authorisable 
    routes are beneath it in the hierarchy.
    This implicates that we cannot listen to `route.data` observable of `ActivatedRoute`, because the route 
    itself in this component will always be the parent route of the one we need to process. 

    So the only real (the least hacky, IMO) solution to access data of child routes is to listen to
    router events.
    However, by the time an instance of this component is constructed, all routing events will have been 
    triggered. This is especially important in case user loads the page on this route.

    To solve that, we can merge two streams, the first one of which will be a single access flag 
    for **activated route**, and the second will be a stream of flags, emitted from router 
    events (e.g. caused by user navigating through app).

    This approach requires that the authorised route is bottom-most in the hierarchy, because otherwise the 
    last value emitted from the stream created from router events will be `true`.
    */

    const deepestChild = this.findDeepestTreeNode(route);
    const currentData = deepestChild.routeConfig.data;

    // `data.authActions` is just an array of strings in my case
    if (currentData && 
        currentData.authActions && 
        Array.isArray(currentData.authActions) && 
        currentData.authActions.length > 0) {

      streams.push(
        // `hasPermissions(actions: strings[]): Observable<boolean>`
        permissions.hasPermissions(currentData.authActions).pipe(take(1))
      );

    } else {
      // If the route in question doesn't have any authorisation logic, simply allow access
      streams.push(of(true));
    }

    streams.push(router.events
      .pipe(
        filter(e => e instanceof ActivationStart),
        switchMap((event: ActivationStart) => {
          const data = event.snapshot.data;

          if (data.authActions && 
            Array.isArray(currentData.authActions) && 
            data.authActions.length > 0) {

            return permissions.hasPermissions(data.authActions);
          }

          return of(true);
        }),
      ));

    this.accessAllowed$ = merge(...streams);
  }

  /**
   * Returns the deepest node in a tree with specified root node, or the first 
   * encountered node if there are several on the lowest level.
   * 
   * @param root The root node.
   */
  findDeepestTreeNode<T extends TreeNodeLike>(root: T): T {
    const findDeepest = (node: T, level = 1): [number, T] => {
      if (node.children && node.children.length > 0) {
        const found = node.children.map(child => findDeepest(child as T, level + 1));
        found.sort((a, b) => a[0] - b[0]);

        return found[0];

      } else {
        return [level, node];
      }
    };

    return findDeepest(root)[1];
  }

}

interface TreeNodeLike {
    children?: TreeNodeLike[];
}

Я объяснил подход в комментариях к исходному коду, но вкратце: получить доступ к данным авторизации в route.data с помощью событий маршрутизатора и заменить <router-outlet> сообщением об ошибке, если доступ запрещен.

person mrnateriver    schedule 27.09.2019