web-dev-qa-db-fra.com

Angular 4 gardes multiples - séquence d'exécution

J'ai 2 gardes, AuthGuard et AccessGuard dans l'application. AuthGuard protège toutes les pages comme son nom l'indique et stocke l'objet de session dans GlobalService et AccessGuard dépend de certaines données d'accès dans l'objet de session stockées par AuthGuard dans GlobalService.

Un problème survient lorsque AuthGuard retourne un observable puis qu'AccessGuard s'exécute simultanément pour vérifier l'objet de session qui n'est pas encore arrivé et le code casse. Existe-t-il un autre moyen de limiter l'exécution d'AccessGuard jusqu'à ce que l'objet de session arrive ou tout autre moyen de contourner cette condition de concurrence critique?

# Remarque Je n'ai pas fusionné la logique AccessGuard avec AuthGuard car seules certaines des routes doivent être vérifiées pour l'accès tandis que toutes les autres nécessitent une authentification. Par exemple, la page Comptes et la page DB sont accessibles à tous, mais la gestion des utilisateurs et le tableau de bord ont besoin de paramètres d'accès externes provenant de l'objet de session

export const routes: Routes = [
  {
    path: 'login',
    loadChildren: 'app/login/login.module#LoginModule',
  },
  {
    path: 'logout',
    loadChildren: 'app/logout/logout.module#LogoutModule',
  },
  {
    path: 'forget',
    loadChildren: 'app/forget/forget.module#ForgetModule',
  },{
    path: 'reset',
    loadChildren: 'app/reset/reset.module#ResetModule',
  },

    path: 'pages',
    component: Pages,
    children: [
      { path: '', redirectTo: 'db', pathMatch: 'full' },
      { path: 'db', loadChildren: 'app/pages/db/db.module#DbModule' },
      { path: 'bi', loadChildren: 'app/pages/dashboard/dashboard.module#DashboardModule', canActivate:[AccessableGuard] },
      { path: 'account', loadChildren: 'app/pages/account/account.module#AccountModule' },
      { path: 'um', loadChildren: 'app/pages/um/um.module#UserManagementModule', canActivate:[AccessableGuard] },
    ],
    canActivate: [AuthGuard]
  }
];

export const routing: ModuleWithProviders = RouterModule.forChild(routes);

# EDIT: Ajout des codes de garde

AuthGuard:

canActivate(route:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean{
  return new Observable<boolean>( observer => {
    this._dataService.callRestful('POST', params.SERVER.AUTH_URL + urls.AUTH.GET_SESSION).subscribe(
        (accessData) => {
          if (accessData['successful']) {
            observer.next(true);
            observer.complete();
            console.log("done");
          }
          else {
            observer.next(false);
            observer.complete();
          }
        });
  });
}

AccessableGuard:

canActivate(route:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean{        
if(this._dataService.getModulePermission(route.routeConfig.path.toUpperCase()) < 2){
        return false;
      }
      return true;
    }

# NOTE: _dataService est GlobalService qui stocke les autorisations d'accès depuis AuthGuard.

9
Akul Narang

Jetez un œil à ce Angular ( lien ). "Si vous utilisiez une API réelle, il pourrait y avoir un certain délai avant que les données à afficher Vous ne voulez pas afficher un composant vierge en attendant les données.

Il est préférable de pré-récupérer les données du serveur afin qu'elles soient prêtes au moment où l'itinéraire est activé. Cela vous permet également de gérer les erreurs avant le routage vers le composant ...

En résumé, vous souhaitez retarder le rendu du composant routé jusqu'à ce que toutes les données nécessaires aient été récupérées.

Vous avez besoin d'un résolveur. "

3

J'ai choisi un chemin différent --- Imbriquer mes gardes et en faire des dépendances les uns des autres.

J'ai un RequireAuthenticationGuard et un RequirePermissionGuard. Pour la plupart des itinéraires, ils doivent tous deux fonctionner, mais il y a un ordre spécifique dont j'ai besoin.

RequireAuthenticationGuard dépend de mes services authN pour vérifier si la session en cours est authentifiée.

RequirePermissionGuard dépend de mes services authZ pour vérifier si la session en cours est autorisée pour un itinéraire.

J'ajoute le RequireAuthenticationGuard en tant que dépendance constructeur de RequirePermissionGuard et je ne commence à vérifier les autorisations que si l'authentification a été déterminée.

require-authentication.guard.ts

constructor(
    private userSessionSerivce: UserSessionService) {}

canActivate(
    _route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
): Observable<boolean> {
    return this.validateAuthentication(state.url);
}

require-permission.guard.ts

constructor(
    private permissionService: PermissionService,
    /**
    * We use the RequireAuthenticationGuard internally
    * since Angular does not provide ordered deterministic guard execution in route definitions
    *
    * We only check permissions once authentication state has been determined
    */
    private requireAuthenticationGuard: RequireAuthenticatedGuard,
) {}

canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
): Observable<boolean> {
    const requiredPermissions: Permission[] = next.data.permissions || [];

    return this.requireAuthenticationGuard
        .canActivate(next, state)
        .pipe(
            mapTo(this.validateAuthorization(state.url, requiredPermissions)),
        );
}
9
seangwright

L'utilisation d'un Master Guard pour déclencher les gardes d'application peut faire l'affaire.

EDIT: ajout de l'extrait de code pour une meilleure compréhension.

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.

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.

6
planet_hunter

Il suffit de créer un master guard dont on injecte les sub guard, voici un exemple:

app.guard.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { GuardA } from '...';
import { GuardB } from '...';

@Injectable({
    providedIn: 'root',
})
export class AppGuard implements CanActivate {

    constructor(
        // inject your sub guards
        private guardA: GuardA,
        private guardB: GuardB,
    ) {
    }

    public async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        for (const guard of this.getOrderedGuards()) {
            if (await guard.canActivate(next, state) === false) {
                return false;
            }
        }
        return true;
    }

 // -> Return here the sub guards in the right order
    private getOrderedGuards(): CanActivate[] {
        return [
            this.guardA,
            this.guardB,
        ];
    }
}

Puis dans votre app-routing.module.ts

const routes: Routes = [
    {
        path: 'page',
        loadChildren: './pages.module#PageModule',
        canActivate: [AppGuard],
    }
];

Bien sûr, vous devez gérer vos modules afin que les protections soient fournies (comprendre l'injectable) dans votre AppGuard.

0
Flavien Volken