【问题标题】:Multiple canActivate guards all run when first fails多个 canActivate 守卫在第一次失败时全部运行
【发布时间】:2017-03-28 03:46:26
【问题描述】:

我的路线有两个 canActivate 警卫(AuthGuardRoleGuard)。第一个 (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

在我看来,如果第一个守卫失败,运行下一个守卫是没有意义的。有没有办法强制执行这种行为?

【问题讨论】:

标签: angular typescript routing angular2-routing


【解决方案1】:

这是因为您返回的是 Promise&lt;boolean&gt; 而不仅仅是 boolean。如果您只返回一个布尔值,则不会检查RoleGuard。我猜这要么是angular2 中的错误,要么是异步请求的预期结果。

但是,您可以通过仅将 RoleGuard 用于需要特定 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
    });
  }
}

更新
在最新的 Angular 版本(当前为 v8.x)中——即使两个 Guard 都会返回 false——它们仍然会被执行。 (行为在不同的返回值之间保持一致)

【讨论】:

  • 感谢 PierreDuc 的建议。我意识到结合这两个守卫最终会在这种特定情况下工作,但我想避免这种解决方案,因为我有其他情况下这不起作用。如果返回布尔值会产生预期的行为,但返回 Promise 不会,那么我认为这是一个路由器错误。
  • 接受您的答案作为解决方案,因为这似乎是最合乎逻辑的解决方法,我会将其作为错误发布在 Angular git 上。再次感谢。
  • @revoxover 感谢您的接受。我尝试在 git 上查找您的问题以支持它,但找不到它
  • @Pierre - 谢谢,我会更新(一旦编辑被接受,我会投票 - 目前已锁定)
【解决方案2】:

我没有在互联网上找到更好的解决方案,但是,作为最佳答案,我决定只使用一个保护,包括使用 Rxjs mergeMap 连接的两个请求,以避免重复调用同一端点。 这是我的示例,如果您愿意,请避免使用 console.log,我使用它来确定首先触发了什么。

调用 1 个 getCASUsername 来验证用户身份(这里是你看不到的 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);
      });
  }
}

【讨论】:

    【解决方案3】:

    正如@PierreDuc Route 中的data 属性所提到的,可以使用Master Guard 来解决这个问题。

    问题

    首先,Angular 不支持串联调用守卫的功能。因此,如果第一个守卫是异步的并且正在尝试进行 ajax 调用,那么所有剩余的守卫甚至在守卫 1 中的 ajax 请求完成之前都会被触发。

    我遇到了类似的问题,这就是我解决它的方法 -


    解决方案

    这个想法是创建一个ma​​sterguard,让masterguard来处理其他guard的执行。

    在这种情况下,路由配置将包含主守卫作为唯一守卫

    要让主守卫知道特定路由要触发的守卫,请在Route 中添加data 属性。

    data 属性是一个键值对,允许我们在路由中附加数据。

    然后可以使用警卫中canActivate 方法的ActivatedRouteSnapshot 参数在警卫中访问数据。

    该解决方案看起来很复杂,但一旦将其集成到应用程序中,它将确保警卫的正常工作。

    以下示例解释了这种方法 -


    示例

    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】:

    目前有多个异步守卫(返回 Promise 或 Observable)将同时运行。我为此开了一个问题:https://github.com/angular/angular/issues/21702

    上述解决方案的另一种解决方法是使用嵌套路由:

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

    【讨论】:

    • 这超级干净,非常好理解哇。 @mick 干得好。
    • 对我来说效果很好,空路径让我有点困惑,所以这里有一个带有路径的示例: const routes: Routes = [ { path: '', component: SummaryComponent, canActivate: [MsalGuard ] },{路径:'管理',canActivate:[MsalGuard],子:[{路径:'',组件:AdministrationComponent,canActivate:[RoleGuard],数据:{角色:['Administrator']}}]}, { 路径:'未授权',组件:UnauthorizedComponent } ];
    【解决方案5】:

    从 Angular 8 开始,我可以做到这一点。此解决方案的灵感来自 @planet_hunter 的回答,但代码更少,并使用 observables 来完成本项目所需的繁重工作。

    使用您选择的名称创建一个守卫,它将按顺序处理所有守卫。

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

    在你的路由文件中,使用 data 属性来添加你想要按顺序运行的守卫(同步):

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

    我今天不得不提出这个解决方案,所以如果您有任何反馈,请发表评论,以便我改进这个答案。

    【讨论】:

    • 这是一个干净的解决方案!当您在父路由和子路由上都使用 SyncGuardHelper 时,它会中断吗?我觉得syncGuards 数组会被子路由覆盖,这会导致父守卫不被执行,而子守卫被执行两次。不过我可能错了!
    【解决方案6】:

    Angular 7.1 及更高版本已解决此问题。

    Guerd 现在有了优先权。
    可以在here in this great blog post找到有关其工作原理的详细说明。

    我从博文中引用以下示例:

    canActivate: [CanActivateRouteGuard, CanActivateRouteGuard2], 
    

    这将按如下方式工作:

    给定canActivate 数组中的所有守卫都是并行执行的,但是 路由器将等到任何具有更高优先级的守卫完成 在继续之前。所以在上面的例子中:

    • 即使CanActivateRouteGuard2 立即返回UrlTree
      路由器仍将等待CanActivateRouteGuard 解析 在开始新的导航之前。
    • 如果 CanActivateRouteGuard 返回 UrlTree:
      将获胜。
    • 如果返回 false:
      整个导航将失败(并且不会发生重定向)。
    • 如果它只是返回true:
      那么CanActivateRouteGuard2返回的UrlTree将被导航到。

    【讨论】:

    • 感谢您发布此后续内容,使用这些优先级是一个更简单的解决方案。
    猜你喜欢
    • 2021-09-13
    • 2018-01-02
    • 1970-01-01
    • 2018-08-31
    • 2017-07-26
    • 1970-01-01
    • 2011-09-02
    • 2020-03-28
    • 1970-01-01
    相关资源
    最近更新 更多