多个 canActivate 守卫在第一次失败时全部运行
Posted
技术标签:
【中文标题】多个 canActivate 守卫在第一次失败时全部运行【英文标题】:Multiple canActivate guards all run when first fails 【发布时间】:2017-03-28 03:46:26 【问题描述】:我的路线有两个 canActivate
警卫(AuthGuard
和 RoleGuard
)。第一个 (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<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 false;
//... your role guard check code goes here
);
更新
在最新的 Angular 版本(当前为 v8.x)中——即使两个 Guard 都会返回 false
——它们仍然会被执行。 (行为在不同的返回值之间保持一致)
【讨论】:
感谢 PierreDuc 的建议。我意识到结合这两个守卫最终会在这种特定情况下工作,但我想避免这种解决方案,因为我有其他情况下这不起作用。如果返回布尔值会产生预期的行为,但返回 Promise我没有在互联网上找到更好的解决方案,但是,作为最佳答案,我决定只使用一个保护,包括使用 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 请求完成之前都会被触发。
我遇到了类似的问题,这就是我解决它的方法 -
解决方案
这个想法是创建一个masterguard,让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中搜索插件时全显示无结果的问题