Angular 权限管理

Angular 权限管理的两种解决方案

在做后台管理系统的时候,权限管理应该是必备的功能点了。这一节我们介绍两种方案来确定用户权限。

首先,我们面板是这个样子,先让大家有一个基础印象:

准备工作

  1. 首先我们通过 cli工具新建了一个 heroes模块,所有工作我们都将在这个模块中完成;
  2. 其次新建了 heroes-addheroes-listheroes-loginheroes-modify四个页面模块,来分别实现不同的功能;
  3. 最后通过子路由的方式配置了项目的路由信息,以便让项目跑起来。
  4. 封装一些常用的方法为服务,以便多处使用:
  5. 添加请求拦截器,为已登录用户每次的请求头添加 token。(拦截器请参照7.2节介绍)

使用路由守卫控制权限

目前我们项目的状态是:无论用户是否登录,或者登录用户的权限如何,都能直接进行新增、修改、删除等操作。显然,这不是我们想要的。

所以,我们可以通过路由守卫来控制权限。

我们先给角色分配一下权限:

  • superadmin: 拥有所有权限;
  • admin: 只有修改权限,没有删除、新增权限;
  • user: 只有查看权限,没有操作权限。

给路由配置添加角色(roles数组):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const routes: Routes = [
{
path: 'heroes',
component: HeroesComponent,
children: [
{ path: 'list', component: HeroesListComponent},
{
path: 'login',
loadChildren: () => import('./heroes-login/heroes-login.module').then(m => m.HeroesLoginModule),
canActivate: [LoginAuthGuard]
},
{
path: 'add',
loadChildren: () => import('./heroes-add/heroes-add.module').then(m => m.HeroesAddModule),
canActivate: [AuthGuard],
data: {roles: ['superadmin']}
},
{
path: 'modify/:id',
loadChildren: () => import('./heroes-modify/heroes-modify.module').then(m => m.HeroesModifyModule),
canActivate: [AuthGuard],
data: {roles: ['superadmin', 'admin']}
},
{ path: '', redirectTo: 'list', pathMatch: 'full' }
]
}
];

新建一个 auth守卫

1
ng g g demos/heroes/guards/auth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// auth.guard.ts
// ...
export class AuthGuard implements CanActivate {
constructor(
private userServe: UserService,
private router: Router,
private accountServe: AccoutService,
private windowServe: WindowService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
// 获取即将进入路由的角色配置
const roles: string[] = route.data.roles;
return this.userServe.user$.pipe(
switchMap(user => {
// 判断用户是否登录
if (user) {
// 匹配用户角色与路由权限配置
if (roles.includes(user.role)) {
return of(true);
} else {
this.windowServe.alert('没有权限');
return of(false);
}
}
// 未登录,去登录,拦截进入下个路由
this.accountServe.redirectTo = state.url;
this.windowServe.alert('请先登录');
this.router.navigateByUrl('/heroes/login').then();
return of(false);
})
);
}
}

这样,我们就能大概实现拦截功能:

tips: 艾科–user 莫甘娜–superadmin 卡特–admin

但是你会发现,我们还有个删除功能没做权限管理。
一般情况下,删除应该是不会跳转路由的,所以,我们需要另辟蹊径来处理。

通过指令控制权限

我们想要的结果其实就是:根据角色,页面上只展示有权限的按钮或其他跟权限有关的入口。

1
ng g d demos/heroes/directives/auth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// auth.directive.ts
import {Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
import {UserService} from '../services/user.service';

@Directive({
selector: '[appAuth]'
})
// 实现 OnChanges 接口
export class AuthDirective implements OnChanges{
// 输入属性传值,获取有权限的角色
@Input('appAuth') roles: string[] = [];
hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private userServe: UserService
) {}
// 在 ngOnChanges 阶段才能拿到输入属性的传值
ngOnChanges(changes: SimpleChanges): void {
if (this.roles.length) {
this.userServe.user$.subscribe(res => {
// 没匹配到角色
if (this.roles.includes(res?.role)){
this.createView();
} else {
this.viewContainer.clear();
this.hasView = false;
}
});
} else {
this.createView();
}
}
// 创建视图
private createView(): void {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
}

使用指令:

页面表现:

至此,我们就实现了通过角色来进行权限管理的全部功能。

通过动态配置权限实现权限管理

在实际工作中,我们可能还会遇到这样的情况:用户的角色是不固定的,所拥有的权限也是动态配置的。这样的情况,我们如果采用上面的方式来做权限,那势必会经常修改我们页面上所配置的角色。所以,针对这样的情况就要采取另一种方式。

我们打算通过页面名与后台传入的权限进行 viewnewdeleteedit等相应的权限控制。

为了演示,我们将会新建四个 normalskillgradelevel组件。normal是没有被权限控制的,所有用户都可以访问。

假如每个登录用户信息是这样的:

1
2
3
4
5
6
7
8
{
"name": "卡特",
"rights": {
"skill": ["edit", "new"],
"grade": ["view"]
},
...
}

上面表示:用户‘卡特’没有访问 level页面的权限,可以在 skill页面编辑、新建,在 grade页面只能查看。

我们还是通过结构性指令来实现,如果没有权限,完全不显示对应入口的功能。

1
ng g d demos/heroes/directives/rights

对应需要控制的页面入口,我们通过传入页面名进行控制:

页面中需要控制的操作入口,通过传入操作类型来进行控制:

方案确定,只差实现指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// rights.directive.ts
import {Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';
import {UserService} from '../services/user.service';
import { Router} from '@angular/router';

@Directive({
selector: '[appRights]'
})
export class RightsDirective implements OnChanges{
// 输入属性传值,获取配置
@Input('appRights') rights = '';
hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private userServe: UserService,
private router: Router
) {}

ngOnChanges(changes: SimpleChanges): void {
const pageName = this.getPageName(this.router.url);
if (this.rights) {
this.userServe.user$.subscribe(res => {
if (res?.rights) {
if (
res.rights[this.rights] || /* 匹配页面入口 */
(res.rights[pageName] && res.rights[pageName].includes(this.rights)) /* 匹配页面操作入口 */
) {
this.createView();
}
} else {
this.clearView();
}
});
} else {
this.clearView();
}
}
// 创建视图
private createView(): void {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
}
// 清除视图
private clearView(): void {
this.viewContainer.clear();
this.hasView = false;
}
// 通过URL获取页面名
private getPageName(url: string): string {
const str = url.split('/').pop();
if (str.includes('?')) {
return str.split('?')[0];
} else if (str.includes('#')){
return str.split('#')[0];
}
return str;
}
}

来看效果:

想要的效果已经实现,通过页面名来匹配可能不是最好的解决方式,因为这样必须要求页面名是唯一的,如有更好的解决方案,欢迎私信~

其实这里还遇到一个问题:

权限管理必定会配合着路由懒加载,但是懒加载的组件是不需要在任何模块中 declarations数组中引入的,如果没有引入组件,那么指令就不会在子模块中的组件中生效,会报错。
所以,最后的解决方式就是在提供指令的模块中同时引入懒加载路由的组件。不用担心,懒加载依然有意义。

总结

1. 在比较固定角色的情况下,采取“路由守卫 + 结构性指令”方案是不错的选择,相反的话第二种方式则更推荐;
2. 权限管理必定会配合着路由懒加载。

权限管理的处理方式可能还有其他方案,如果你的更好,请告诉我~


欢迎关注我的公众号,公众号将第一时间更新angular教程:


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!