Несколько предупреждений canActivate запускаются при первом сбое

У меня есть маршрут с двумя охранниками canActivate (AuthGuard и RoleGuard). Первая (AuthGuard) проверяет, вошел ли пользователь в систему, а если нет, перенаправляется на страницу входа. Второй проверяет, имеет ли пользователь определенную роль, которая разрешена для просмотра страницы, а если нет, перенаправляет на неавторизованную страницу.

canActivate: [ AuthGuard, RoleGuard ]
...
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        ...
        this.router.navigate(['/login']);
        resolve(false);
}

export class RoleGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        ...
        this.router.navigate(['/unauthorized']);
        resolve(false);
}

Проблема в том, что когда я получаю доступ к маршруту, и я не вошел в систему, я нажимаю AuthGuard, который терпит неудачу и говорит маршрутизатору перейти к /login. Однако, несмотря на то, что AuthGuard не удалось, RoleGuard работает в любом случае, а затем переходит на /unauthorized.

По-моему, бессмысленно запускать следующего охранника, если первое не удается. Есть ли способ обеспечить соблюдение этого поведения?

Ответ 1

Это связано с тем, что вы возвращаете Promise<boolean> вместо просто boolean. Если вы просто вернете логическое значение, он не проверит RoleGuard. Я думаю, это либо ошибка в angular2, либо ожидаемый результат асинхронных запросов.

Однако вы можете решить эту проблему с помощью своего примера, используя только RoleGuard для URL-адресов, для которых требуется определенный Role, потому что я полагаю, что вы должны войти в систему, чтобы иметь роль. В этом случае вы можете изменить свой RoleGuard на это:

@Injectable()
export class RoleGuard implements CanActivate {
  constructor(private _authGuard: AuthGuard) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
    return this._authGuard.canActivate(route, state).then((auth: boolean) => {
      if(!auth) {
        return false;
      }
      //... your role guard check code goes here
    });
  }
}

Ответ 2

Я был в той же ситуации несколько недель назад, поэтому решил написать несколько "хакерских" решений для достижения этого, используя атрибут data объекта определения маршрута.

Идея состоит в том, чтобы определить вашу собственную охрану (назовем ее MainGuard), которая будет читать ваш атрибут данных (например, guardsList) с массивом охранников внутри функции MainGuard.canActivate и выполнять их (вызывая guardsList[i].canActive) один за другим в цикле. Вам нужно будет использовать приложение Injector, чтобы иметь возможность называть canActivate для данного защитника.

Он немного усложняется, если вы хотите поддерживать Observable, Promise, а plain boolean защищает все под одним массивом guardsList, так как вам нужно подписаться и дождаться завершения и т.д.

Я реализовал этот "хак" как Angular Library, который позволяет вам сделать что-то вроде этого:

const appRoutes: Routes = [
{
    ...
    canActivate: [SequentialGuards],
    data: { GUARDS_SEQ: [Guard1, Guard2, Guard3] }
    ...
}]

В случае неудачи Guard1 Guard2 и Guard3 не будут вызываться.

Ответ 3

Как упоминалось в свойстве @PierreDuc data в классе Route, наряду с Master Guard, можно использовать для решения этой проблемы.

Проблема

Прежде всего, angular не поддерживает функцию вызова охранников в тандеме. Поэтому, если первая защита асинхронна и пытается выполнить вызовы ajax, все остальные охранники будут запущены даже до завершения запроса ajax в защите 1.

Я столкнулся с подобной проблемой, и именно так я ее решил -


Решение

Идея состоит в том, чтобы создать главный защитник и позволить главному охраннику обрабатывать выполнение других охранников.

Конфигурация маршрутизации в этом случае будет содержать master guard как единственный защитник.

Чтобы мастер-охранник узнал о срабатывании охранников для определенных маршрутов, добавьте свойство data в Route.

Свойство data - это пара ключевых значений, которая позволяет нам связывать данные с маршрутами.

Затем данные могут быть доступны в охранниках с использованием параметра ActivatedRouteSnapshot метода canActivate в защите.

Решение выглядит сложным, но оно обеспечит надлежащую работу охранников после его интеграции в приложение.

В следующем примере объясняется этот подход -


Пример

1. Константы Объект, чтобы отобразить все защитные устройства -

export const GUARDS = {
    GUARD1: "GUARD1",
    GUARD2: "GUARD2",
    GUARD3: "GUARD3",
    GUARD4: "GUARD4",
}

2. Защита приложения -

import { Injectable } from "@angular/core";
import { Guard4DependencyService } from "./guard4dependency";

@Injectable()
export class Guard4 implements CanActivate {
    //A  guard with dependency
    constructor(private _Guard4DependencyService:  Guard4DependencyService) {}

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        return new Promise((resolve: Function, reject: Function) => {
            //logic of guard 4 here
            if (this._Guard4DependencyService.valid()) {
                resolve(true);
            } else {
                reject(false);
            }
        });
    }
}

3. Конфигурация маршрутизации -

import { Route } from "@angular/router";
import { View1Component } from "./view1";
import { View2Component } from "./view2";
import { MasterGuard, GUARDS } from "./master-guard";
export const routes: Route[] = [
    {
        path: "view1",
        component: View1Component,
        //attach master guard here
        canActivate: [MasterGuard],
        //this is the data object which will be used by 
        //masteer guard to execute guard1 and guard 2
        data: {
            guards: [
                GUARDS.GUARD1,
                GUARDS.GUARD2
            ]
        }
    },
    {
        path: "view2",
        component: View2Component,
        //attach master guard here
        canActivate: [MasterGuard],
        //this is the data object which will be used by 
        //masteer guard to execute guard1, guard 2, guard 3 & guard 4
        data: {
            guards: [
                GUARDS.GUARD1,
                GUARDS.GUARD2,
                GUARDS.GUARD3,
                GUARDS.GUARD4
            ]
        }
    }
];

4. Мастер Гвардии -

import { Injectable } from "@angular/core";
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from "@angular/router";

//import all the guards in the application
import { Guard1 } from "./guard1";
import { Guard2 } from "./guard2";
import { Guard3 } from "./guard3";
import { Guard4 } from "./guard4";

import { Guard4DependencyService } from "./guard4dependency";

@Injectable()
export class MasterGuard implements CanActivate {

    //you may need to include dependencies of individual guards if specified in guard constructor
    constructor(private _Guard4DependencyService:  Guard4DependencyService) {}

    private route: ActivatedRouteSnapshot;
    private state: RouterStateSnapshot;

    //This method gets triggered when the route is hit
    public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {

        this.route = route;
        this.state = state;

        if (!route.data) {
            Promise.resolve(true);
            return;
        }

        //this.route.data.guards is an array of strings set in routing configuration

        if (!this.route.data.guards || !this.route.data.guards.length) {
            Promise.resolve(true);
            return;
        }
        return this.executeGuards();
    }

    //Execute the guards sent in the route data 
    private executeGuards(guardIndex: number = 0): Promise<boolean> {
        return this.activateGuard(this.route.data.guards[guardIndex])
            .then(() => {
                if (guardIndex < this.route.data.guards.length - 1) {
                    return this.executeGuards(guardIndex + 1);
                } else {
                    return Promise.resolve(true);
                }
            })
            .catch(() => {
                return Promise.reject(false);
            });
    }

    //Create an instance of the guard and fire canActivate method returning a promise
    private activateGuard(guardKey: string): Promise<boolean> {

        let guard: Guard1 | Guard2 | Guard3 | Guard4;

        switch (guardKey) {
            case GUARDS.GUARD1:
                guard = new Guard1();
                break;
            case GUARDS.GUARD2:
                guard = new Guard2();
                break;
            case GUARDS.GUARD3:
                guard = new Guard3();
                break;
            case GUARDS.GUARD4:
                guard = new Guard4(this._Guard4DependencyService);
                break;
            default:
                break;
        }
        return guard.canActivate(this.route, this.state);
    }
}

Вызовы

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

Надеюсь, это поможет.

Ответ 4

Я не нашел лучшего решения в Интернете, но, используя в качестве путеводителя лучший ответ, я решил использовать только один защитник, включая оба конкатенированных запроса с использованием Rxjs mergeMap, чтобы избежать дублирования вызовов на одну и ту же конечную точку. В этом примере, избегайте console.log, если вы хотите, я использовал его, чтобы быть уверенным в том, что было вызвано первым.

1 getCASUsername вызывается для аутентификации пользователя (heres console.log(1), который вы не видите)
2 У нас есть имя пользователя
3 Здесь я делаю второй запрос, который будет запускаться после первого, используя ответ (true)
4 Используя возвращаемое имя пользователя, я получаю роли для этого пользователя

У меня есть решение для последовательности вызовов и для избежания дублированных вызовов. Возможно, это может сработать для вас.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private AuthService  : AuthService,
              private AepApiService: AepApiService) {}

  canActivate(): Observable<boolean> {
    return this.AepApiService.getCASUsername(this.AuthService.token)
      .map(res => {
        console.log(2, 'userName');
        if (res.name) {
          this.AuthService.authenticateUser(res.name);
          return true
        }
      })
      .mergeMap( (res) => {
        console.log(3, 'authenticated: ' + res);
        if (res) {
          return this.AepApiService.getAuthorityRoles(this.AuthService.$userName)
            .map( res => {
              console.log(4, 'roles');
              const roles = res.roles;

              this.AuthService.$userRoles = roles;

              if (!roles.length) this.AuthService.goToAccessDenied();

              return true;
            })
            .catch(() => {
              return Observable.of(false);
            });
        } else {
          return Observable.of(false);
        }
      })
      .catch(():Observable<boolean> => {
        this.AuthService.goToCASLoginPage();
        return Observable.of(false);
      });
  }
}

Ответ 5

В настоящее время существует несколько асинхронных охранников (возвращающих Promise или Observable) одновременно. Я открыл для этого проблему: https://github.com/angular/angular/issues/21702

Другим обходным решением описанного выше решения является использование вложенных маршрутов:

{
  path: '',
  canActivate: [
    AuthGuard,
  ],
  children: [
    {
      path: '',
      canActivate: [
        RoleGuard,
      ],
      component: YourComponent
      // or redirectTo
      // or children
      // or loadChildren
    }
  ]
}