news 2026/6/24 5:27:31

用 NestJS 从零开发一个完整的小项目:图书管理系统(第七阶段:RBAC(Role Based Access Control)基于角色的权限控制)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 NestJS 从零开发一个完整的小项目:图书管理系统(第七阶段:RBAC(Role Based Access Control)基于角色的权限控制)
基于角色的权限控制

例如:

角色权限
admin增删改查
user只能查看
guest无权限

先理解 RBAC

当前:

登录 ↓ JWT ↓ 认证成功

只能证明:

你是谁

但是不知道:

你能干什么

RBAC解决的是:

你能访问哪些资源

最终效果

普通用户:

DELETE /books/1

返回:

{ "statusCode":403, "message":"Forbidden" }

管理员:

DELETE /books/1

成功:

{ "message":"删除成功" }

第一步:给 User 增加角色

修改:

src/users/entities/user.entity.ts

现在:

@Entity() export class User { @PrimaryGeneratedColumn() id:number; @Column({ unique:true, }) username:string; @Column() password:string; }

增加:

@Column({ default:'user', }) role:string;

完整:

@Entity() export class User { @PrimaryGeneratedColumn() id:number; @Column({ unique:true, }) username:string; @Column() password:string; @Column({ default:'user', }) role:string; }

数据库会变:

user
idusernamepasswordrole
1adminxxxadmin
2tomxxxuser

第二步:注册时默认 user

修改:

users.service.ts

创建用户:

const user = this.userRepository.create({ username, password, role:'user', });

后面可以做:

后台创建管理员

暂时手动修改数据库。

例如:

update user set role='admin' where id=1;

第三步:登录时携带角色

当前:

const payload = { sub:user.id, username:user.username, }

改:

const payload = { sub:user.id, username:user.username, role:user.role, }

JWT:

{ "sub":1, "username":"admin", "role":"admin" }

第四步:创建 Roles 装饰器

目录:

src/auth/decorators

新增:

roles.decorator.ts

内容:

import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = ( ...roles:string[] ) => SetMetadata( ROLES_KEY, roles, );

使用:

@Roles('admin')

等价:

roles:['admin']

第五步:创建 RolesGuard

目录:

src/auth/guards

新增:

roles.guard.ts

内容:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { ROLES_KEY } from '../decorators/roles.decorator'; import { JwtPayload } from '../interfaces/jwt-payload.interface'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext) { const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!roles) { return true; } const request = context.switchToHttp().getRequest<{ user: JwtPayload; }>(); const user = request.user; return roles.includes(user.role); } }

新增

src\auth\interfaces\jwt-payload.interface.ts
export interface JwtPayload { sub: number; username: string; role: string; }

逻辑:

读取 @Roles() ↓ 获取 request.user.role ↓ 比较 ↓ 通过 / 拒绝

第六步:注册全局 RolesGuard

打开:

app.module.ts

增加:

import { RolesGuard } from './auth/guards/roles.guard';

providers:

providers:[ AppService, { provide:APP_GUARD, useClass:JwtAuthGuard, }, { provide:APP_GUARD, useClass:RolesGuard, }, ]

执行顺序:

JwtAuthGuard ↓ RolesGuard ↓ Controller

第七步:给接口加角色限制

例如:

books.controller.ts

删除图书:

@Roles('admin') @Delete(':id') remove( @Param('id') id:string, ){ return this.booksService.remove( +id, ); }

查看图书:

@Get() findAll(){ return this.booksService.findAll(); }

不限制。


第八步:测试

用户

数据库:

role=user

登录:

{ "access_token":"xxx" }

请求:

DELETE /books/1

返回:

{ "statusCode":403, "message":"Forbidden resource" }

管理员

数据库:

role=admin

登录:

{ "access_token":"xxx" }

请求:

DELETE /books/1

成功。


进一步优化(推荐)

不要用字符串:

@Roles('admin')

创建:

// src/users/enums/role.enum.ts export enum Role { ADMIN='admin', USER='user', }

使用:

import { Role } from '../enums/role.enum';

User 实体:

@Column({ default: Role.USER, }) role: Role;

Roles 装饰器:

@Roles(Role.ADMIN)

你当前项目推荐结构

src ├── auth │ ├── decorators │ ├── guards │ ├── interfaces │ │ └── jwt-payload.interface.ts │ ├── users │ ├── entities │ │ └── user.entity.ts │ ├── enums │ │ └── role.enum.ts │ ├── books

role.enum.ts

export enum Role { ADMIN = 'admin', USER = 'user', }

user.entity.ts

import { Role } from '../enums/role.enum'; @Column({ type: 'enum', enum: Role, default: Role.USER, }) role: Role;

roles.decorator.ts

import { Role } from '../../users/enums/role.enum'; export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

books.controller.ts

@Roles(Role.ADMIN) @Delete(':id') remove(@Param('id') id: string) { return this.booksService.remove(+id); }

这样从现在开始,你整个 RBAC 系统就都是强类型的:

Role.ADMIN Role.USER

而不是:

'admin' 'user'

避免拼写错误导致权限失效。

RBAC完善

支持多角色

实际上你前面写的Roles()已经天然支持多角色。

因为:

export const Roles = (...roles: Role[]) => SetMetadata( ROLES_KEY, roles, );

这里:

...roles

就是剩余参数。


例如:

@Roles(Role.ADMIN)

得到:

['admin']

例如:

@Roles( Role.ADMIN, Role.USER, )

得到:

[ 'admin', 'user', ]

你的 Guard:

return roles.includes( user.role, );

已经支持多角色。

所以:

@Roles( Role.ADMIN, Role.USER, ) @Get() findAll() {}

表示:

admin 可以访问 或 user 可以访问

实现 CurrentUser 装饰器

目前你可能这样拿用户:

@Get() findAll( @Req() req, ){ console.log(req.user); }

不优雅。

Nest 推荐封装装饰器。


创建目录

src └── auth └── decorators └── current-user.decorator.ts

编写装饰器

import { createParamDecorator, ExecutionContext, } from '@nestjs/common'; import { Request } from 'express'; export const CurrentUser = createParamDecorator( ( data: unknown, ctx: ExecutionContext, ) => { const request = ctx .switchToHttp() .getRequest<Request>(); return request.user; }, );

给 Request.user 类型

否则会报:

Property 'user' does not exist on type Request

创建:

src └── types └── express.d.ts

内容:

import { JwtPayload } from '../auth/interfaces/jwt-payload.interface'; declare global { namespace Express { interface Request { user: JwtPayload; } } } export {};

完善 JwtPayload

你应该已经有:

src/auth/interfaces/jwt-payload.interface.ts

内容:

import { Role } from '../../users/enums/role.enum'; export interface JwtPayload { sub: number; username: string; role: Role; }

完善 JwtStrategy

现在:

validate( payload: JwtPayload, ){ return payload; }

Controller 使用

例如:

import { CurrentUser } from '../auth/decorators/current-user.decorator'; import { JwtPayload } from '../auth/interfaces/jwt-payload.interface';

@Get() findAll( @CurrentUser() user: JwtPayload, ){ console.log(user); return this.booksService.findAll(); }

请求:

GET /books Authorization: Bearer xxx

打印:

{ sub: 1, username: 'admin', role: 'admin' }

直接获取字段(高级写法)

改造装饰器:

export const CurrentUser = createParamDecorator( ( data: keyof JwtPayload, ctx: ExecutionContext, ) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; return data ? user[data] : user; }, );

然后:

获取整个用户:

@CurrentUser() user: JwtPayload

得到:

{ sub:1, username:'admin', role:'admin' }

只获取用户名:

@CurrentUser('username') username: string

得到:

admin

只获取角色:

@CurrentUser('role') role: Role

得到:

admin

测试示例

@Roles( Role.ADMIN, Role.USER, ) @Get('profile') profile( @CurrentUser() user: JwtPayload, ){ return user; }

请求:

GET /books/profile Authorization: Bearer xxx

返回:

{ "sub": 1, "username": "admin", "role": "admin" }

到这里,你的认证授权体系已经比较完整:

JWT ↓ JwtStrategy ↓ CurrentUser ↓ JwtAuthGuard ↓ RolesGuard ↓ RBAC

现在你的权限体系

JWT认证 ↓ JwtAuthGuard ↓ 获取用户 ↓ RolesGuard ↓ 角色校验 ↓ Controller

这已经是很多企业后台管理系统的基础权限架构了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/24 5:26:27

Human-in-the-Loop 场景应用

任务中断后继续# 第一步&#xff1a;开始任务&#xff0c;遇到INFO action result ask_agent_start_new_task(device_iddevice_id,task"去淘宝帮我选一个生日礼物",# ... ) # 返回&#xff1a;stop_reason"INFO_ACTION_NEEDS_REPLY", session_id"xxx…

作者头像 李华
网站建设 2026/6/24 5:11:56

量子计算中的GHZ态:原理、实现与优化策略

1. 量子纠缠与GHZ态基础解析量子纠缠是量子力学最奇特的现象之一&#xff0c;也是量子计算区别于经典计算的核心资源。当多个量子比特处于纠缠态时&#xff0c;它们之间的关联无法用经典概率论解释&#xff0c;这种非局域特性使得量子算法能够实现指数级加速。1.1 GHZ态的数学定…

作者头像 李华
网站建设 2026/6/24 5:11:06

想要找专业靠谱的东莞ERP财务数据治理咨询机构该怎么选

随着东莞制造、外贸企业数字化转型加速&#xff0c;ERP系统已经成为企业财务管控的核心工具&#xff0c;但不少企业因为前期财务体系不规范&#xff0c;ERP系统里积累了大量混乱、错配的财务数据&#xff0c;不仅影响日常核算效率&#xff0c;还拖慢了公司历史遗留税务问题解决…

作者头像 李华
网站建设 2026/6/24 5:09:08

CAAF框架:用确定性断言与状态锁定构建可靠AI代理系统

1. 项目概述&#xff1a;从“失控”到“可控”的AI代理进化之路最近在折腾AI代理&#xff08;AI Agent&#xff09;时&#xff0c;我遇到了一个几乎所有从业者都头疼的问题&#xff1a;不确定性。你精心设计了一个工作流&#xff0c;让一个代理去分析数据&#xff0c;另一个去生…

作者头像 李华
网站建设 2026/6/24 5:08:06

指令粒度如何影响具身智能体性能:从U型效应到实践策略

1. 从“把客厅打扫干净”到“拿起抹布擦桌子”&#xff1a;指令粒度如何塑造具身智能体最近在跟进具身智能领域的一些前沿进展&#xff0c;发现一个非常有意思且被很多人忽略的问题&#xff1a;我们给智能体的指令&#xff0c;到底应该多“粗”或多“细”&#xff1f;比如&…

作者头像 李华