NestJS RBAC Tutorial: Role-Based Access Control with Guards
本教程用 node-casbin 为 NestJS API 加上基于角色的访问控制(RBAC)。每个路由用装饰器声明所需的权限,全局守卫询问 Casbin 当前用户是否拥有该权限;角色之间可以继承,所以 admin 自动拥有 editor 能做的一切。
权限以资源和操作命名(articles、delete),而不是 URL 路径,这与 NestJS 的控制器很契合。基于路径的规则见 Express 版本。
1. 安装
从一个 NestJS 项目(nest new my-app)开始,加上 Casbin:
npm install casbin
2. 编写模型
保存为项目根目录下的 model.conf:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
r = sub, obj, act:每个请求由用户、资源和操作组成。g = _, _:用户可以被分配到角色,角色也可以被分配到其他角色。- 当用户直接或通过继承的角色拥有策略规则中的角色时,
g(r.sub, p.sub)为真。
3. 编写策略
保存为 policy.csv:
p, viewer, articles, read
p, editor, articles, create
p, editor, articles, update
p, admin, articles, delete
g, editor, viewer
g, admin, editor
g, alice, admin
g, bob, editor
g, carol, viewer
第一部分只授予每个角色本层新增的权限。第二部分构建层级:editor 同时也是 viewer,admin 同时也是 editor。最后一部分把用户分配到角色。结果如下:
| 用户 | 角色 | read | create | update | delete |
|---|---|---|---|---|---|
| alice | admin | 是 | 是 | 是 | yes |
| bob | 编辑器 | 是 | 是 | yes | 否 |
| carol | viewer | 是 | 否 | 否 | no |
| dave | (无) | 否 | 否 | 否 | no |
4. 添加权限装饰器
src/permission.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const PERMISSION_KEY = 'permission';
export interface Permission {
resource: string;
action: string;
}
// Declares which permission a route needs, for example @RequirePermission('articles', 'delete').
export const RequirePermission = (resource: string, action: string) =>
SetMetadata(PERMISSION_KEY, { resource, action } as Permission);
5. 添加守卫
守卫从路由的元数据中读取所需权限,然后询问 Casbin。没有装饰器的路由保持公开。
src/casbin.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Enforcer } from 'casbin';
import { Request } from 'express';
import { Permission, PERMISSION_KEY } from './permission.decorator';
export const ENFORCER = 'CASBIN_ENFORCER';
@Injectable()
export class CasbinGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
@Inject(ENFORCER) private readonly enforcer: Enforcer,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const permission = this.reflector.getAllAndOverride<Permission>(PERMISSION_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!permission) {
return true; // Routes without @RequirePermission are public.
}
const request = context.switchToHttp().getRequest<Request>();
// Replace this with the user your authentication guard puts on the request, such as request.user.
const user = request.header('X-User') ?? '';
const allowed = await this.enforcer.enforce(user, permission.resource, permission.action);
if (!allowed) {
throw new ForbiddenException();
}
return true;
}
}
备注
示例用 X-User 请求头保持简短。 Casbin 只负责授权。实际应用中,认证守卫(例如基于 @nestjs/passport 的守卫)会先运行并设置 request.user;从那里读取用户名或用户 ID。
6. 在控制器中使用
src/articles.controller.ts
import { Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { RequirePermission } from './permission.decorator';
@Controller('articles')
export class ArticlesController {
@Get()
@RequirePermission('articles', 'read')
findAll() {
return { articles: [] };
}
@Post()
@RequirePermission('articles', 'create')
create() {
return { created: true };
}
@Put(':id')
@RequirePermission('articles', 'update')
update(@Param('id') id: string) {
return { updated: id };
}
@Delete(':id')
@RequirePermission('articles', 'delete')
remove(@Param('id') id: string) {
return { deleted: id };
}
}
7. 注册 enforcer 和守卫
enforcer 由一个工厂 provider 创建一次,并注入到守卫中。用 APP_GUARD 注册守卫,它就会作用于应用中的每个路由。
src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { newEnforcer } from 'casbin';
import { ArticlesController } from './articles.controller';
import { CasbinGuard, ENFORCER } from './casbin.guard';
@Module({
controllers: [ArticlesController],
providers: [
{
provide: ENFORCER,
useFactory: () => newEnforcer('model.conf', 'policy.csv'),
},
{
provide: APP_GUARD,
useClass: CasbinGuard,
},
],
})
export class AppModule {}
8. 试一试
npm run start
curl -i -X DELETE -H "X-User: alice" http://localhost:3000/articles/1 # 200, admins can delete
curl -i -X DELETE -H "X-User: bob" http://localhost:3000/articles/1 # 403, editors cannot
curl -i -X PUT -H "X-User: bob" http://localhost:3000/articles/1 # 200, editors can update
curl -i -X POST -H "X-User: carol" http://localhost:3000/articles # 403, viewers are read-only
curl -i -H "X-User: carol" http://localhost:3000/articles # 200
curl -i -H "X-User: dave" http://localhost:3000/articles # 403, no role
9. 在运行时管理角色
在任何需要的地方(例如管理服务中)用 @Inject(ENFORCER) 注入 enforcer:
await enforcer.addRoleForUser('dave', 'editor'); // dave becomes an editor
await enforcer.getImplicitRolesForUser('alice'); // [ 'admin', 'editor', 'viewer' ]
await enforcer.getImplicitPermissionsForUser('bob');
// [ [ 'editor', 'articles', 'create' ], [ 'editor', 'articles', 'update' ], [ 'viewer', 'articles', 'read' ] ]
getImplicitPermissionsForUser 很适合做一个“我能做什么”的接口,供前端显示或隐藏按钮。完整列 表见 RBAC API。
Comments are powered by giscus and GitHub Discussions. They load from giscus.app and GitHub only after you click the button below.