我正在开发一个具有很多角色的应用程序,我需要使用警卫来阻止导航到基于这些角色的应用程序的某些部分。我意识到我可以为每个角色创建单独的保护类,但宁愿有一个我可以以某种方式传递参数的类。换句话说,我希望能够做类似的事情:
{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
canActivate: [RoleGuard.forRole('superUser')]
}
但是由于您传递的只是您的守卫的类型名称,因此无法想出一种方法来做到这一点。我是否应该硬着头皮为每个角色编写单独的保护类,并打破我对单一参数化类型的优雅幻想?
您可以这样做,而不是使用
forRole()
:
{
path: 'super-user-stuff',
component: SuperUserStuffComponent,
canActivate: [RoleGuard],
data: {roles: ['SuperAdmin', ...]}
}
并在您的 RoleGuard 中使用它
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
: Observable<boolean> | Promise<boolean> | boolean {
let roles = route.data.roles as Array<string>;
...
}
自 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)]
}
这是我对此的看法以及针对缺少提供商问题的可能解决方案。
在我的例子中,我们有一个守卫,它以权限或权限列表作为参数,但它与角色是一样的。
我们有一个类来处理经过或未经许可的授权守卫:
@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]) },
你可以这样写你的角色守卫:
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')]
}
@AluanHaddad 的解决方案给出了“无提供者”错误。这是一个解决方案(感觉很脏,但我缺乏制作更好的解决方案的技能)。
从概念上讲,我将由
roleGuard
创建的每个动态生成的类注册为提供者。
因此对于检查的每个角色:
canActivate: [roleGuard('foo')]
你应该:
providers: [roleGuard('foo')]
但是,@AluanHaddad 的解决方案将按原样为每次调用
roleGuard
生成新类,即使 roles
参数相同。使用 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;
}
}
});
注意,每个角色组合都会生成一个新类,因此您需要注册为每个角色组合的提供者。 IE。如果你有:
canActivate: [roleGuard('foo')]
和 canActivate: [roleGuard('foo', 'bar')]
您必须同时注册:providers[roleGuard('foo'), roleGuard('foo', 'bar')]
更好的解决方案是在
roleGuard
内部的全局提供者集合中自动注册提供者,但正如我所说,我缺乏实现这一点的技能。
另一种解决方案可能是返回
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'])]
有一种方法可以用
useFactory
和 providers
来实现:
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 {
}
为了完成这项工作,您还需要更改 Guard 的范围,删除
providedIn: 'root'
(只需保留 @Injectable()
)并将参数传递到构造函数中,如下所示(在您的 Guard 文件中):
constructor(@Inject('roleName') private readonly roleName: string) {
}
!!!意识到 !!! 使用这种方法将为每个此类声明创建一个新的守卫实例
要添加一个好的 Angular 16 方法
{
path: 'compliance',
component: ComplianceMainComponent,
canActivate: [permissionGuard(ManagerPermission.COMPLIANCE)],
},
使用另一个接受参数并返回 Guard 的函数来扭曲 Guard
export const permissionGuard = (permission: ManagerPermission) =>
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree => {
const store = inject(Store);
return store.select(fromStore.getManagerPermissions).pipe(
take(1),
map((permissions) => permissions.includes(permission)),
);
};