多个 canActivate 守卫在第一次失败时全部运行

Posted

技术标签:

【中文标题】多个 canActivate 守卫在第一次失败时全部运行【英文标题】:Multiple canActivate guards all run when first fails 【发布时间】: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 7.1 及更高版本开始,这不再是问题。检查我的answer with a reference to a nice blog post on the topic here 【参考方案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将被导航到。

【讨论】:

感谢您发布此后续内容,使用这些优先级是一个更简单的解决方案。

以上是关于多个 canActivate 守卫在第一次失败时全部运行的主要内容,如果未能解决你的问题,请参考以下文章

路由守卫

按顺序执行多个异步路由守卫

解决IDEA在Marketplace中搜索插件时全显示无结果的问题

在 Laravel 中使用多个守卫时,在注册/登录后设置默认守卫

如何使用选择快照?

如何处理警卫中显示的模态