Multiple canActivate guards all run when first fails

前端 未结 6 1266
借酒劲吻你
借酒劲吻你 2020-12-02 15:32

I have a route with two canActivate guards (AuthGuard and RoleGuard). The first (AuthGuard) checks to see if the user is

相关标签:
6条回答
  • 2020-12-02 16:01

    As of Angular 8 I am able to do this. This solution was inspired by @planet_hunter's answer but with less code and uses observables for the heavy lifting which was a requirement for this project.

    Create a guard with your name of choice that will handle running all guards in order.

    @Injectable({
        providedIn: 'root'
    })
    export class SyncGuardHelper implements CanActivate {
        public constructor(public injector: Injector) {
        }
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
            return from(route.data.syncGuards).pipe(concatMap((value) => {
                const guard = this.injector.get(value);
                const result = guard.canActivate(route, state);
                if (result instanceof Observable) {
                    return result;
                } else if (result instanceof Promise) {
                    return from(result);
                } else {
                    return of(result);
                }
            }), first((x) => x === false || x instanceof UrlTree, true));
        }
    }
    

    In your routes file use the data property to add the guards you want to run in order (synchronously):

    const routes: Routes = [
        {
            path: '',
            component: MyComponent,
            canActivate: [SyncGuardHelper],
            data: {
                syncGuards: [
                    Guard1,
                    Guard2,
                    Guard3
                ]
            }
        },
        // other routes
    ]
    

    I had to come up with this solution today so if you have any feedback please leave a comment so I can improve this answer.

    0 讨论(0)
  • 2020-12-02 16:06

    As mentioned by @PierreDuc data property in Route Class along with a Master Guard can be used to solve this problem.

    Problem

    First of all, angular doesn't support the feature to call the guards in tandem. So if first guard is asynchronous and is trying to make ajax calls, all the remaining guards will get fired even before completion of the ajax request in guard 1.

    I faced the similar problem and this is how I solved it -


    Solution

    The idea is to create a master guard and let the master guard handle the execution of other guards.

    The routing configuration in this case, will contain master guard as the only guard.

    To let master guard know about the guards to be triggered for specific routes, add a data property in Route.

    The data property is a key value pair that allows us to attach data with the routes.

    The data can then be accessed in the guards using ActivatedRouteSnapshot parameter of canActivate method in the guard.

    The solution looks complicated but it will assure proper working of guards once it is integrated in the application.

    Following example explains this approach -


    Example

    1. Constants Object to map all application guards -

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

    3. Routing Configuration -

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

    Challenges

    One of the challenges in this approach is refactoring of existing routing model. However, it can be done in parts as the changes are non-breaking.

    I hope this helps.

    0 讨论(0)
  • 2020-12-02 16:07

    This is due to the fact you are returning a Promise<boolean> instead of just a boolean. If you were to just return a boolean it wouldn't check the RoleGuard. I would guess this is either a bug in angular2 or an expected result of async requests.

    You can however resolve this with your example by only using RoleGuard for urls where a certain Role is required, because I guess you need to be logged in to have a role. In that case you can change your RoleGuard to this:

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

    Update
    In the latest Angular version (currently v8.x) - Even if both Guard will return false - they will still be both executed. (behavior was aligned between different return values)

    0 讨论(0)
  • 2020-12-02 16:13

    This issue is resolved in Angular 7.1 and above.

    Guerds have now a sense of priority.
    A detailed explanation on how it works can be found here in this great blog post.

    I quote the following example from the blog post:

    canActivate: [CanActivateRouteGuard, CanActivateRouteGuard2], 
    

    Which will be working as follows:

    All guards in a given canActivate array are executed in parallel, but the router will wait until any guards with a higher priority to finish before moving on. So in the above example:

    • Even if CanActivateRouteGuard2 returns a UrlTree immediately:
      the router will still wait for CanActivateRouteGuard to resolve before initiating a new navigation.
    • If CanActivateRouteGuard returns a UrlTree:
      that will win.
    • If it returns false:
      the entire navigation fails (and no redirects happen).
    • If it simply returns true:
      then the UrlTree returned by CanActivateRouteGuard2 will be navigated to.
    0 讨论(0)
  • 2020-12-02 16:14

    I didn't find a better solution on the internet, but, using as guide the best answer I decide to use only one guard including both requests concatenated using Rxjs mergeMap, this to avoid duplicated calls to the same endpoint. Here my example, avoid the console.log if you want to, I was using it to be sure of what is been triggered first.

    1 getCASUsername is called to authenticate the user (heres a console.log(1) that you can't see)
    2 We have the userName
    3 Here I'm doing a second request that will be triggered after the first one using the response (true)
    4 Using the returned userName I get the roles for that user

    With this I have the solution for call sequence and for avoiding duplicated calls. Maybe it could work for you.

    @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);
          });
      }
    }
    
    0 讨论(0)
  • 2020-12-02 16:24

    Currently having multiple async guards(returning Promise or Observable) will run at the same time. I opened a issue for this: https://github.com/angular/angular/issues/21702

    Another workaround to the described solution above is to use nested routes:

    {
      path: '',
      canActivate: [
        AuthGuard,
      ],
      children: [
        {
          path: '',
          canActivate: [
            RoleGuard,
          ],
          component: YourComponent
          // or redirectTo
          // or children
          // or loadChildren
        }
      ]
    }
    
    0 讨论(0)
提交回复
热议问题