web-dev-qa-db-fra.com

Plusieurs protections canActivate s'exécutent toutes lorsque le premier échoue

J'ai un itinéraire avec deux canActivate gardes (AuthGuard et RoleGuard). Le premier (AuthGuard) vérifie si l'utilisateur est connecté et, sinon, redirige vers la page de connexion. Le second vérifie si l'utilisateur a un rôle défini qui est autorisé à afficher la page et, sinon, redirige vers la page non autorisée.

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);
}

Le problème est que lorsque j'accède à l'itinéraire et que je ne suis pas connecté, j'appuie sur AuthGuard, qui échoue et indique au routeur de naviguer vers /login. Cependant, même si le AuthGuard a échoué, le RoleGuard s'exécute quand même, puis accède à /unauthorized.

À mon avis, il est inutile d'exécuter le prochain garde si le premier échoue. Existe-t-il un moyen d'imposer ce comportement?

27
revoxover

Cela est dû au fait que vous renvoyez un Promise<boolean> au lieu d'un boolean. Si vous deviez simplement renvoyer un booléen, il ne vérifierait pas le RoleGuard. Je suppose que c'est soit un bug dans angular2 ou un résultat attendu de demandes asynchrones.

Vous pouvez cependant résoudre ce problème avec votre exemple en utilisant uniquement RoleGuard pour les URL où un certain Role est requis, car je suppose que vous devez être connecté pour avoir un rôle. Dans ce cas, vous pouvez changer votre RoleGuard en ceci:

@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 Promise.resolve(false);
            }
            //... your role guard check code goes here
        });
}
25
PierreDuc

Comme mentionné par @PierreDuc data, la propriété dans la classe Route avec un Master Guard peut être utilisée pour résoudre ce problème.

Problème

Tout d'abord, angular ne prend pas en charge la fonction pour appeler les gardes en tandem. Donc, si le premier garde est asynchrone et essaie de faire des appels ajax, tous les gardes restants seront renvoyés avant même achèvement de la demande ajax en garde 1.

J'ai fait face au problème similaire et c'est ainsi que je l'ai résolu -


Solution

L'idée est de créer un master guard et de laisser le master guard gérer l'exécution des autres gardes.

Le configuration de routage dans ce cas, contiendra garde maître comme seul garde.

Pour informer le maître garde des gardes à déclencher pour des routes spécifiques, ajoutez une propriété data dans Route.

La propriété data est une paire de valeurs clés qui nous permet de joindre des données aux itinéraires.

Les données sont ensuite accessibles dans les gardes en utilisant le paramètre ActivatedRouteSnapshot de la méthode canActivate dans la garde.

La solution semble compliquée mais elle assurera le bon fonctionnement des gardes une fois qu'elle sera intégrée à l'application.

L'exemple suivant explique cette approche -


Exemple

1. Constantes Objet pour mapper tous les gardes d'application -

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

2. Application Guard -

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);
            }
        });
    }
}

. Configuration de routage -

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. Master Guard -

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);
    }
}

Défis

L'un des défis de cette approche est la refactorisation du modèle de routage existant. Cependant, cela peut être fait en plusieurs fois car les modifications sont incessantes.

J'espère que ça aide.

3
planet_hunter

Je n'ai pas trouvé de meilleure solution sur Internet, mais, en utilisant comme meilleure réponse, je décide de n'utiliser qu'un seul garde, y compris les deux demandes concaténées à l'aide de Rxjs mergeMap, ceci pour éviter les appels en double vers le même point de terminaison. Voici mon exemple, évitez le console.log si vous le souhaitez, je l'utilisais pour être sûr de ce qui a été déclenché en premier.

1 getCASUsername est appelé pour authentifier l'utilisateur (voici un console.log (1) que vous ne pouvez pas voir)
2 Nous avons le nom d'utilisateur
3 Ici, je fais une deuxième demande qui sera déclenchée après la première en utilisant la réponse (vrai)
4 En utilisant le nom d'utilisateur renvoyé, j'obtiens les rôles pour cet utilisateur

Avec cela, j'ai la solution pour la séquence d'appels et pour éviter les appels en double. Peut-être que cela pourrait fonctionner pour vous.

@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);
      });
  }
}
2
Rodrigo

Actuellement, plusieurs gardes asynchrones (renvoyant Promise ou Observable) s'exécuteront en même temps. J'ai ouvert un problème pour cela: https://github.com/angular/angular/issues/21702

Une autre solution à la solution décrite ci-dessus consiste à utiliser des routes imbriquées:

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