多个canActivate防护装置在第一次失败时全部运行

时间:2016-11-14 13:27:32

标签: angular typescript routing angular2-routing

我的路线有两个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

在我看来,如果第一个失败,那么运行下一个后卫是毫无意义的。有没有办法强制执行此行为?

6 个答案:

答案 0 :(得分:22)

这是因为您返回的是Promise<boolean>,而不仅仅是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 Promise.resolve(false);
            }
            //... your role guard check code goes here
        });
}

答案 1 :(得分:2)

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

调用1个getCASUsername来验证用户(继承人看不到的console.log(1))
2我们有userName
3这里我正在做第二个请求,它将在第一个请求后使用响应(true)被触发 4使用返回的userName我获得该用户的角色

有了这个,我就有了呼叫序列和避免重复呼叫的解决方案。也许它可以为你工作。

@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 :(得分:2)

正如data类中的@PierreDuc Route属性以及 Master Guard 所述,可用于解决此问题。

问题

首先,angular不支持同时召唤警卫的功能。因此,如果第一个警卫是异步的并且正在尝试进行ajax调用,那么即使在警卫1中完成ajax请求之前,所有剩余的警卫也会被解雇。

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

解决方案

我们的想法是创建一个主守卫,并让主守卫处理其他守卫的执行。

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

要让主人知道要为特定路线触发的警卫,请在data中添加Route属性。

data属性是一个键值对,允许我们使用路径附加数据。

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

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

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

实施例

<强> 1。常量对象以映射所有应用程序保护 -

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。路由配置 -

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

挑战

这种方法的挑战之一是重构现有的路由模型。但是,它可以部分完成,因为更改不会中断。

我希望这会有所帮助。

答案 3 :(得分:2)

从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
]

我今天必须提出这个解决方案,因此,如果您有任何反馈意见,请发表评论,以便我改善此答案。

答案 4 :(得分:1)

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

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

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

答案 5 :(得分:0)

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

人们现在有了优先感。
有关其工作原理的详细说明,请参见here in this great blog post

我引用博客文章中的以下示例:

canActivate: [CanActivateRouteGuard, CanActivateRouteGuard2], 

工作方式如下:

给定canActivate数组中的所有防护均并行执行,但 路由器将等待,直到具有更高优先级的所有防护完成 在继续之前。因此,在上面的示例中:

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