将参数传递给路由保护器

我正在开发一个有很多角色的应用程序,我需要使用警卫来阻止基于这些角色的应用程序的部分导航。我意识到我可以为每个角色创建单独的守卫类,但是我宁愿有一个类,我可以以某种方式传递一个参数。换句话说,我希望能够做类似的事情:

{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
canActivate: [RoleGuard.forRole('superUser')]
}

但是由于你所经过的只是你的警卫的类型名称,所以想不出办法来做到这一点。我是否应该咬紧牙关,为每个角色编写单独的警卫类,并打破使用单个参数化类型的优雅幻想?

68048 次浏览

不要使用 forRole(),你可以这样做:

{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
canActivate: [RoleGuard],
data: {roles: ['SuperAdmin', ...]}
}

在你的角色护卫中使用这个

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
: Observable<boolean> | Promise<boolean> | boolean  {


let roles = route.data.roles as Array<string>;
...
}

@ AluanHaddad 的解决方案是给出“无供应商”错误。这里有一个解决办法(感觉很脏,但我缺乏制作一个更好的方法的技巧)。

从概念上讲,我作为提供者注册由 roleGuard创建的每个动态生成的类。

因此,对于每个被检查的角色:

canActivate: [roleGuard('foo')]

你应该:

providers: [roleGuard('foo')]

然而,即使 roles参数相同,@AluanHaddad 的解决方案仍然会为每次对 roleGuard的调用生成新的类。使用 lodash.memoize它看起来像这样:

export var roleGuard = _.memoize(function forRole(...roles: string[]): Type<CanActivate> {
return class AuthGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<boolean>
| Promise<boolean>
| boolean {
console.log(`checking access for ${roles.join(', ')}.`);
return true;
}
}
});

注意,每个角色组合都生成一个新类,因此您需要注册为提供者 每个角色组合。例如:

你必须同时注册 canActivate: [roleGuard('foo')]canActivate: [roleGuard('foo', 'bar')]: providers[roleGuard('foo'), roleGuard('foo', 'bar')]

更好的解决方案是在 roleGuard中的全局提供程序集合中自动注册提供程序,但是正如我所说的,我缺乏实现这一点的技能。

以下是我对这个问题的看法,以及缺少供应商问题的可能解决方案。

在我的例子中,我们有一个以权限或权限列表作为参数的约束,但它同样有一个角色。

我们有一个课程来处理授权警卫,无论是否获得许可:

@Injectable()
export class AuthGuardService implements CanActivate {


checkUserLoggedIn() { ... }

这涉及到检查用户活动会话等等。

它还包含一个用于获取自定义权限保护的方法,该方法实际上取决于 AuthGuardService本身

static forPermissions(permissions: string | string[]) {
@Injectable()
class AuthGuardServiceWithPermissions {
constructor(private authGuardService: AuthGuardService) { } // uses the parent class instance actually, but could in theory take any other deps


canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
// checks typical activation (auth) + custom permissions
return this.authGuardService.canActivate(route, state) && this.checkPermissions();
}


checkPermissions() {
const user = ... // get the current user
// checks the given permissions with the current user
return user.hasPermissions(permissions);
}
}


AuthGuardService.guards.push(AuthGuardServiceWithPermissions);
return AuthGuardServiceWithPermissions;
}

这允许我们使用该方法来注册一些自定义保护基于权限参数在我们的路由模块:

....
{ path: 'something',
component: SomeComponent,
canActivate: [ AuthGuardService.forPermissions('permission1', 'permission2') ] },

forPermission中有趣的部分是 AuthGuardService.guards.push-这基本上确保了在任何时候调用 forPermissions来获得一个自定义保护类时,它也会将其存储在这个数组中。这在主类上也是静态的:

public static guards = [ ];

然后我们可以使用这个数组来注册所有的保护程序——这是可以的,只要我们确保在应用程序模块注册这些提供程序时,路由已经被定义,所有的保护程序类已经被创建(例如,检查导入顺序,并尽可能低地保持这些提供程序在列表中-有一个路由模块有帮助) :

providers: [
// ...
AuthGuardService,
...AuthGuardService.guards,
]

希望这个能帮上忙。

data和工厂功能相结合的另一种方法:

export function canActivateForRoles(roles: Role[]) {
return {data: {roles}, canActivate: [RoleGuard]}
}


export class RoleGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
: Observable<boolean> | Promise<boolean> | boolean  {
  

const roles = route.data.roles as Role[];
...
}
}


...


{ path: 'admin', component: AdminComponent, ...canActivateWithRoles([Role.Admin]) },


另一种解决方案是返回一个 InjectionToken并使用工厂方法:

export class AccessGuard {
static canActivateWithRoles(roles: string[]) {
return new InjectionToken<CanActivate>('AccessGuardWithRoles', {
providedIn: 'root',
factory: () => {
const authorizationService = inject(AuthorizationService);


return {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): <boolean | UrlTree > | Promise<boolean | UrlTree> | boolean | UrlTree {
return authorizationService.hasRole(roles);
}
};
},
});
}
}

像这样使用它:

canActivate: [AccessGuard.canActivateWithRoles(['ADMIN'])]

你可以这样写你的角色守卫:

export class RoleGuard {
static forRoles(...roles: string[]) {


@Injectable({
providedIn: 'root'
})
class RoleCheck implements CanActivate {
constructor(private authService: AuthService) { }
canActivate(): Observable<boolean> | Promise<boolean> | boolean {
const userRole = this.authService.getRole();


return roles.includes(userRole);
}
}


return RoleCheck;
}


}

如果您愿意,也可以在多个角色中使用它:

{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
canActivate: [RoleGuard.forRoles('superUser', 'admin', 'superadmin')]
}

对于 useFactoryproviders,有一种方法可以做到这一点:

const routes: Routes = [
{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
// Name can be whatever you want
canActivate: ['CanActiveSuperUserStuffGuard']
}
]

providers中,您需要添加以下内容:

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
{
provide: 'CanActiveSuperUserStuffGuard',
useFactory: () => new RoleGuard('superUser')
}
]
})
export class YourRoutingModule {
}

要做到这一点,您还需要改变您的保护删除 providedIn: 'root'的范围(只需留下 @Injectable()) ,并传递参数到构造函数如下(在您的保护文件) :

  constructor(@Inject('roleName') private readonly roleName: string) {
}

小心! 使用这种方法将为每个这样的声明创建一个新的 Guard 实例

从2022年开始,你可以使用 CanActivateFn (https://angular.io/api/router/CanActivateFn)。这个函数返回一个 CanActivateFn 实例:

// Returns a function which can act as a guard for a route
function requireAnyRole(...roles: Role[]): CanActivateFn {
return (ars: ActivatedRouteSnapshot, rss: RouterStateSnapshot) => {
// do some checks here and return true/false/observable
// can even inject stuff with inject(ClassOrToken)
}
}

然后你可以用它来定义路线

{
path: 'some/path',
component: WhateverComponent,
canActivate: [requireAnyRole(Role1, Role2, Role3)]
}