Nestjs框架: 基于裝飾器與Guards的完成RBAC權限系統設計與實現

概述

  • 在現代權限管理系統中,RBAC(基于角色的訪問控制)是廣泛采用的一種模型
  • RBAC 核心思想是通過角色來管理用戶權限
  • 通過角色綁定用戶、資源和權限,實現細粒度的訪問控制
  • 為了實現這一目標,我們需要在數據庫中設計合理的模型結構
  • 并在代碼中通過裝飾器的方式對模塊(controller)和路由(route)進行權限標記
  • 通過裝飾器為每個模塊設置唯一標識符(如字符串)
  • 使用 Reflector 機制讀取模塊和路由上的元信息
  • 從而實現權限的動態控制與驗證

在這里插入圖片描述

  • Guard 通過 Reflector 讀取模塊和路由的信息,在元信息上設置唯一的字符串
  • 之后,我們要讀取用戶角色,訪問 UserRepo, 進而訪問用戶實體,聯合查詢出對應的角色和權限

Schema 完整版


這里基于 Prisma 演示

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-initgenerator client {provider = "prisma-client-js"output   = "./clients/postgresql"
}datasource db {provider = "postgresql"url      = env("DATABASE_URL")
}model User {id       Int        @id @default(autoincrement())username String     @uniquepassword StringuserRole UserRole[]@@map("users")
}model Role {id          Int        @id @default(autoincrement())name        String     @unique // 角色名稱(如:admin、editor) description String? // 角色描述 createdAt   DateTime   @default(now())updatedAt   DateTime   @updatedAtusers       UserRole[] // 關聯用戶(多對多) rolePermissions RolePermissions[]  // 關聯權限(多對多) @@map("roles")
}model UserRole {userId IntroleId Intuser   User @relation(fields: [userId], references: [id], onDelete: Cascade)role   Role @relation(fields: [roleId], references: [id], onDelete: Cascade)@@id([userId, roleId]) // 聯合主鍵 @@map("user_roles") // 數據庫表名 
}model Permission {id          Int      @id     @default(autoincrement())name        String   @unique  // ctrlName + routeName, 如:user:createaction      String   // crudm description String?RolePermissions RolePermissions[] // role 和 perm 是 一對多的關系@@map("permissions")
}model RolePermissions {roleId        IntpermissionId  Introle          Role        @relation(fields: [roleId], references: [id])permission    Permission  @relation(fields: [permissionId], references: [id])@@id([roleId, permissionId])@@map("role_permissions")
}

我們同步到遠程數據庫 $ npx prisma db push

創建enum

在 src/common/enum/actions.enum.ts

export enum Action {Manage = 'MANAGE', // 后續會存到數據庫中Create = 'CREATE',Delete = 'DELETE',Update = 'UPDATE',Read = 'READ',
}

這些枚舉后續會存入數據庫中

創建裝飾器

在 src/common/decorators/role-permission.decorator.ts

// decorator 是為后面的模塊和路由打標簽的import { SetMetadata } from '@nestjs/common';
import { Action } from '../enum/actions.enum';
import { Reflector } from '@nestjs/core';export const PERMISSION_KEY = 'permission';// 累積屬性
const accumulateMetadata = (key: string, permission: string): any => {return (target: any,propertyKey: string | symbol,descriptor?: TypedPropertyDescriptor<any>) => {const reflector = new Reflector();// 針對方法的 functionif (descriptor?.value) {const existingPermissions = reflector.get(key, descriptor.value) || [];const newPermissions = [...existingPermissions, permission];SetMetadata(key, newPermissions)(target, propertyKey, descriptor);} else {// 針對類的 constructorconst existingPermissions = reflector.get(key, target) || [];const newPermissions = [...existingPermissions, permission];SetMetadata(key, newPermissions)(target);}}
}export const Permission = (permission: string) =>accumulateMetadata(PERMISSION_KEY, permission);export const Create = () =>accumulateMetadata(PERMISSION_KEY, Action.Create.toLowerCase());export const Update = () =>accumulateMetadata(PERMISSION_KEY, Action.Update.toLowerCase());export const Delete = () =>accumulateMetadata(PERMISSION_KEY, Action.Delete.toLowerCase());export const Read = () =>accumulateMetadata(PERMISSION_KEY, Action.Read.toLowerCase());

當多個裝飾器疊加使用時,需要確保它們不會被覆蓋,而是以數組形式保存
該方法解決了裝飾器疊加時的覆蓋問題;
支持多個權限標簽在同一個路由或模塊上共存;
示例:@read() @update() 會生成 [‘read’, ‘update’] 的權限數組

創建相關 Guard

$ nest g guard common/guards/role-permission --flat --no-spec

這里生成了 src/common/guards/role-permission.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { PERMISSION_KEY } from '../decorators/role-permission.decorator';@Injectable()
export class RolePermissionGuard implements CanActivate {constructor(private reflector: Reflector) {}canActivate(context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {const classPermission = this.reflector.get<string>(PERMISSION_KEY, context.getClass())const handlerPermission = this.reflector.get<string>(PERMISSION_KEY, context.getHandler())console.log('classPermission', classPermission); // userconsole.log('handlerPermission', handlerPermission); // readreturn true;}
}

通過上面我們知道

  • Guard 通過 Reflector 獲取裝飾器信息
  • 讀取 controller 和 route 上的權限裝飾器信息
  • 返回 true(允許訪問)或拋出異常(拒絕訪問)

后續,我們可以

  • 獲取當前用戶的角色信息
  • 比對用戶權限與訪問路徑權限是否匹配
  • 權限拼接格式為 modulename:action,如 user:create
  • 用戶權限數據應從數據庫中聯合查詢角色與權限表獲取
  • Guard 的核心邏輯是權限比對,確保用戶具有訪問對應路由的權限

對控制器應用裝飾器和Guards

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Permission, Read, Update } from '@/common/decorators/role-permission.decorator';
import { RolePermissionGuard } from '@/common/guards/role-permission.guard';@Controller('user')
@UseGuards(RolePermissionGuard)
@Permission('user')
@Permission('user1')
export class UserController {@Read()@Update()@Get('/test')async test(): Promise<any> {return 'ok';}
}

我們測試一下

curl --request GET \--url http://localhost:3000/user/test

我們看到程序中輸出

classPermission [ 'user1', 'user' ]
handlerPermission [ 'update', 'read' ]

可以看到這里效果出來了,實現了裝飾器的累加,并且支持方法和類的篩選

完成 role 模塊


1 ) 通過 $ nest g res role --no-spec 生成 role 資源模塊,因為我們用到的是 Prisma,所以里面的 entities 可以刪除

2 )role/dto/create-role.dto.ts

import { IsNotEmpty, IsOptional, IsString } from 'class-validator';// 按照 schema 來寫 dto
export class CreateRoleDto {@IsNotEmpty()@IsString()name: string;@IsOptional()@IsString()description?: string;
}

3 )role.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, Optional } from '@nestjs/common';
import { RoleService } from './role.service';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';@Controller('role')
export class RoleController {constructor(private readonly roleService: RoleService) {}@Post()create(@Body() createRoleDto: CreateRoleDto) {return this.roleService.create(createRoleDto);}@Get()findAll(@Query('page', new ParseIntPipe( {optional: true })) page: number,@Query('limit', new ParseIntPipe( {optional: true })) limit: number,) {return this.roleService.findAll(page, limit);}@Get(':id')findOne(@Param('id') id: string) {return this.roleService.findOne(+id);}@Patch(':id')update(@Param('id') id: string, @Body() updateRoleDto: UpdateRoleDto) {return this.roleService.update(+id, updateRoleDto);}@Delete(':id')remove(@Param('id') id: string) {return this.roleService.remove(+id);}
}

4 ) role.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { PrismaClient } from 'prisma-postgresql'; // 注意這里
import { PRISMA_DB_CLIENT } from '@/database/database.constants';@Injectable()
export class RoleService {constructor(@Inject(PRISMA_DB_CLIENT) private prismaClient: PrismaClient) {}create(createRoleDto: CreateRoleDto) {return this.prismaClient.role.create({data: createRoleDto,});}findAll(page: number = 1, limit: number = 10) {const skip = (page - 1) * limit;return this.prismaClient.role.findMany({skip,take: limit,})}findOne(id: number) {return this.prismaClient.role.findUnique({ where: { id }});}update(id: number, updateRoleDto: UpdateRoleDto) {return this.prismaClient.role.update({where: { id },data: updateRoleDto,});}remove(id: number) {return this.prismaClient.role.delete({ where: {id }})}
}

之后,可以自行自行測一下上面的 crud 功能

完成 permission 模塊


和上面的 role 模塊類似,$ nest g res permission --no-spec

之后,基于 Permission 的 Model 寫相關 dto 和 service 并改造 controller

寫完之后,自己測試一下

完成 User 模塊的改造


1 ) user/dto/create-user.dto.ts

import { IsArray, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";export class CreateUserDto {@IsString()@IsNotEmpty()username: string;@IsString()@IsNotEmpty()password: string;@IsArray()@IsInt({ each: true })@IsOptional()roleIds: number[];
}

2 ) user.controller.ts

import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { Permission, Read, Update } from '@/common/decorators/role-permission.decorator';
import { RolePermissionGuard } from '@/common/guards/role-permission.guard';
import { CreateUserDto } from './dto/create-user.dto';@Controller('user')
@UseGuards(RolePermissionGuard)
@Permission('user')
@Permission('user1')
export class UserController {constructor(private userRepository: UserRepository,) {}@Post()// @Serialize(PublicUserDto) // 這個后續添加create(@Body() createUserDto: CreateUserDto) {return this.userRepository.create(createUserDto);}
}

3 ) .env 配置一條默認角色

# Role 默認角色
ROLE_ID=4

確保數據庫 role 表有這一條數據

4 )user/repository/repository.prisma

import { Inject } from '@nestjs/common';
import { PRISMA_DB_CLIENT } from '@/database/database.constants';
import { UserAdapter } from '../user.interfaces';
import { PrismaClient } from 'prisma-postgresql';
import { ConfigService } from '@nestjs/config';export class UserPrismaRepository implements UserAdapter {constructor(@Inject(PRISMA_DB_CLIENT) private prismaClient: PrismaClient,private configService: ConfigService) {}async create(userObj: any): Promise<any> {// 讀取默認角色信息const DEFAULT_ROLE_ID = +this.configService.get('ROLE_ID');// 判斷角色信息是否在數據庫中// 在,則寫入角色與用戶關系表,并創建用戶return await this.prismaClient.$transaction(async (prisma: PrismaClient) => {const roleIds = userObj?.roleIds ?? [DEFAULT_ROLE_ID];const validRoleIds = [];for (const roleId of roleIds) {const role = await prisma.role.findUnique({  where: { id: roleId } });if (role) {validRoleIds.push(roleId);}}if (validRoleIds.length) {validRoleIds.push(DEFAULT_ROLE_ID);}delete userObj['roleIds']; // 這個 schema 中沒有定義,會報錯return prisma.user.create({data: {...userObj,userRole: {create: validRoleIds.map(( roleId ) => ({ roleId })),},}})});}
}

5 )測試

發送

curl --request POST \--url http://localhost:3000/user \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2' \--data '{"username":"toimc2","password": "123456","roleIds": []
}'

響應

{"id": 3,"username": "toimc2","password": "$argon2id$v=19$m=65536,t=3,p=4$NdfibfeW7LDZsLIgSjZQdw$hi2j5VRPUtGIBCmzXXEl8Ps0lnNbfvc6xuCEwaJanj0"
}

這樣,就創建成功了, 可同步驗證 users 和 user_roles 表數據是否同步
關于密碼脫敏的問題,可以考慮 攔截器處理

好,以上完成了創建 users 的時,關聯了 role

改造 role 創建的時候,關聯 permission


1 ) role/dto/create-role.dto

import { CreatePermissionDto } from '@/permission/dto/create-permission.dto';
import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { Type }  from 'class-transformer';// 補充
interface PermissionType {id?: number;name: string;action: string;description?: string;
}// 按照 schema 來寫 dto
export class CreateRoleDto {@IsNotEmpty()@IsString()name: string;@IsOptional()@IsString()description?: string;// 補充@IsOptional()@IsArray()@Type(() => CreatePermissionDto)permissions: PermissionType[];
}

2 ) permission/dto/create-permission.dto

import { IsNotEmpty, IsOptional, IsString } from 'class-validator';// 按照 schema 來寫 dto
export class CreatePermissionDto {@IsNotEmpty()@IsString()name: string;@IsString()action: string;@IsOptional()@IsString()description?: string;
}

3 )main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';async function bootstrap() {const app = await NestFactory.create(AppModule);app.enableShutdownHooks();// 全局類校驗管道app.useGlobalPipes(new ValidationPipe({// true - 去除類上不存在的字段,false 則不會去除whitelist: true,// transform: 自動轉換請求對象到 DTO 實例transform: true,transformOptions: {// 允許類轉換器隱式轉換字段類型,如將字符串轉換為數字等enableImplicitConversion: true,}}))await app.listen(process.env.PORT ?? 3000);
}bootstrap();

4 ) role/role.service

import { Inject, Injectable } from '@nestjs/common';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { PRISMA_DATABASE } from '@/database/databse.constants';
import { PrismaClient } from 'prisma/clients/postgresql';@Injectable()
export class RoleService {constructor(@Inject(PRISMA_DATABASE) private prismaClient: PrismaClient) {}async create(createRoleDto: CreateRoleDto) {return await this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { permissions, ...restData } = createRoleDto;return prisma.role.create({data: {...restData,RolePermissions: {create: permissions.map((permission) => ({permission: {connectOrCreate: {where: {name: permission.name,},create: {...permission,},},},})),},},});},);}findAll(page: number, limit: number) {const skip = (page - 1) * limit;return this.prismaClient.role.findMany({skip,take: limit,});}// 這里關聯一下permissionfindOne(id: number) {return this.prismaClient.role.findUnique({ where: { id },include: {rolePermissions: {include: {permission: true,}}}});}update(id: number, updateRoleDto: UpdateRoleDto) {return this.prismaClient.role.update({where: { id },data: updateRoleDto,});}remove(id: number) {return this.prismaClient.role.delete({ where: { id } });}
}

5 ) 測試

創建請求

curl --request POST \--url http://localhost:3000/role \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2' \--data '{"name": "普通用戶6","description":"測試","permissions": [{"name": "log:read","action": "read"},{"name": "log:update","action": "update"}]
}'

創建響應

{"id": 5,"name": "普通用戶6","description": "測試","createdAt": "2025-08-22T13:43:52.596Z","updatedAt": "2025-08-22T13:43:52.596Z"
}

查詢請求

curl --request GET \--url http://localhost:3000/role/5 \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2'

查詢響應

{"id": 5,"name": "普通用戶6","description": "測試","createdAt": "2025-08-22T13:43:52.596Z","updatedAt": "2025-08-22T13:43:52.596Z","rolePermissions": [{"roleId": 5,"permissionId": 2,"permission": {"id": 2,"name": "log:read","action": "read","description": null}},{"roleId": 5,"permissionId": 3,"permission": {"id": 3,"name": "log:update","action": "update","description": null}}]
}

6 )后續可以再控制器上加上一個 序列化的 dto 對數據進行簡化,如:

role/dto/public-role.dto

import { Transform } from 'class-transformer';
import { CreateRoleDto } from "./create-role.dto";export class PublicRoleDto extends CreateRoleDto {@Transform(( { value } ) => value.map((permission) => permission.permission.name))rolePermissions: any[]
}

role/role.controller

@Get(':id')
@Serialize(PublicRoleDto)
findOne(@Param('id') id: string) {return this.roleService.findOne(+id);
}

響應結果就會簡化為

{"id": 5,"name": "普通用戶6","description": "測試","createdAt": "2025-08-22T13:43:52.596Z","updatedAt": "2025-08-22T13:43:52.596Z","rolePermissions": ["log:read","log:update"]
}

繼續改造User,基于User查詢Role, 基于Role查詢Permission


1 ) 改造 user/user.interfaces

export interface UserAdapter {findAll(page?:number, limit?: number): Promise<any[]>;findOne(username: string): Promise<any[]>;create(userObj: any): Promise<any>;update(userObj: any): Promise<any>;delete(id: string | number): Promise<any>;
}

2 )改造 repositories

user/repositories/repository.mongoose

findAll(page: number = 1, limit: number = 10): Promise<any[]> {return this.userModel.find().limit(limit).skip((page - 1) * limit).exec();
}findOne(username: string): Promise<any[]> {return this.userModel.findOne({ username });
}

user/repositories/repository.prisma

findAll(page: number = 1, limit: number = 10): Promise<any[]> {const skip = (page - 1) * limit;return this.prismaClient.user.findMany({ skip, take: limit });
}
findOne(username: string): Promise<any[]> {return this.prismaClient.user.findMany( {where : { username }} );
}

user/repositories/repository.typeorm

findAll(page: number = 1, limit: number = 10): Promise<any[]> {return this.userRepository.find({ skip: (page - 1) * limit, take: limit});
}
findOne(username: string): Promise<any> {// return this.userRepository.findOne({where: { username }});return this.userRepository.findOneBy({ username });
}

3 )改造 user.repository

findAll(page?: number, limit?: number): Promise<any[]> {const client = this.getRepository();return client.findAll(page, limit);
}
findOne(username: string): Promise<any[]> {const client = this.getRepository();return client.findOne(username);
}

4 ) 改造 user.controller

@Get()
findAll(@Query('page', new ParseIntPipe({ optional: true})) page,@Query('limit', new ParseIntPipe({optional: true})) limit,
) {return this.userRepository.findAll(page, limit);
}@Get(':username')
findOne(@Param('username') username: string) {return this.userRepository.findOne(username);
}

5 )測試一下

請求

curl --request GET \--url http://localhost:3000/user/wang1 \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2'

響應

{"id": 4,"username": "wang1","password": "$argon2id$v=19$m=65536,t=3,p=4$5e2iqzVvSXK6e66adpsA+A$8dC09rixIdJiJMK5PjOkLlDr4VreYb9wq2WoB2KSsjo","userRole": [{"userId": 4,"roleId": 4,"role": {"id": 4,"name": "x管理","description": "測試","createdAt": "2025-08-15T14:31:17.392Z","updatedAt": "2025-08-15T14:31:17.392Z","rolePermissions": []}}]
}

5 ) 上面響應比較繁瑣, 可以進行序列化操作

改造通用 dto: auth/dto/public-user.dto

import { Exclude, Expose, Transform } from 'class-transformer';
import { SignUserDto } from './signin-user.dto';export class PublicUserDto extends SigninUserDto {@Expose()id: string;@Expose()username: string;@Expose()password: string;@Transform(({ value }) => {return value.map((item) => ({name: item.role.name,id: item.role.id,permissions: item.role.rolePermissions,}));})@Expose({name: 'userRole'}) // 這里 將原來的 userRole 改了一個別名: rolesroles: any; // 這里是 別名
}

更新 user.controller 應用序列化

@Get(':username')
@Serialize(PublicUserDto)
findOne(@Param('username') username: string) {return this.userRepository.findOne(username);
}

重新請求后的響應

{"id": 4,"username": "wang1","roles": [{"name": "x管理","id": 4,"permissions": []}}]
}

User 模塊用戶更新的改造


1 ) 新建 user/dto/update-user.dto

import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { CreateRoleDto } from '@/role/dto/create-role.dto';
import { IsArray, IsInt, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';export class UpdateUserDto extends PartialType(CreateUserDto) {@IsInt()@IsOptional()id?: number;@IsArray()@Type(() => CreateRoleDto)roles?: any[]
}

2 ) user/user.controller

@Patch()
update(@Body() updateUserDto: UpdateUserDto) {// return updateUserDto; // 測試的時候使用return this.userRepository.update(updateUserDto);
}@Delete(':id')
delete(@Param('id') id: string) {return this.userRepository.delete(id);
}

3 )改造 /role/dto/create-role.dto.ts

import { CreatePermissionDto } from '@/permission/dto/create-permission.dto';
import { Type, Transform, plainToInstance } from 'class-transformer';
import { IsNotEmpty, IsOptional, IsString, IsArray } from 'class-validator';interface PermissionType {id?: number;name: string;action: string;description?: string;
}export class CreateRoleDto {@IsNotEmpty()@IsString()name: string;@IsOptional()@IsString()description?: string;@IsOptional()@IsArray()// permissions 兩種類型: 1.string[] 2. 對象[]// 當傳遞為對象 -> 直接轉換成對應的 dto 實例@Type(() => CreatePermissionDto) // 這里是偷懶的寫法,如果傳遞的和類型不匹配,會直接走到后面的 Transform// 這里支持2種類型@Transform(( { value }) => {return value.map((item) => {if (typeof item === 'string') {// 當傳遞為 string -> split -> { name, action } 對象數組const parts = item.split(":");return plainToInstance(CreatePermissionDto, {name: item,action: parts[1] || '',})}return plainToInstance(CreatePermissionDto, item);})})permissions: PermissionType[] | string[];
}

或 基于官網文檔,有個高級的Type用法來支持多個類型
參考:https://github.com/typestack/class-transformer#providing-more-than-one-type-option

import { Type, Transform, plainToInstance } from 'class-transformer';
import { IsNotEmpty, IsOptional, IsString, IsArray } from 'class-validator';interface PermissionType {id?: number;name: string;action: string;description?: string;
}abstract class Permission {abstract type: string;
}class StringPermission extends Permission {type = 'string';value: string;
}class DetailedPermission extends Permission {type = 'detailed';name: string;action: string;
}export class CreateRoleDto {@IsNotEmpty()@IsString()name: string;@IsOptional()@IsString()description?: string;@IsOptional()@IsArray()// permissions 兩種類型: 1.string[] 2. 對象[]// 當傳遞為對象 -> 直接轉換成對應的 dto 實例@Type(() => Permission, {discriminator: {property: 'type',subTypes: [{ value: StringPermission, name: 'string' },{ value: DetailedPermission, name: 'detail' },]}})permissions: PermissionType[] | string[];
}

上面兩種 達到的效果一致

4 )改造 user/repository/repository.prisma

async update(userObj: any): Promise<any> {return await this.prismaClient.$transaction(async (prisma: PrismaClient) => {const { id, username, password, roles, ...rest } = userObj;// 更新的 where 條件const whereCond = id ? { id }: { username };let updateData: any = {};if (password) {const newHashPass = await argon2.hash(password);updateData.password = newHashPass;}updateData = { ...updateData, ...rest };const roleIds = [];// 角色權限的更新,放置在前await Promise.all(roles.map(async (role) => {roleIds.push(role.id); // 存儲 idconst { permissions, ...restRole }  = role;await prisma.role.update({where: { id: role.id },data: {...restRole,rolePermissions: {deleteMany: {},create: (permissions || []).map(permission => ({permission: {connectOrCreate: {where: {name: permission.name,},create: permission}}}))}}})}))// 用戶角色更新const updatedUser = await prisma.user.update({where: whereCond,data: {...updateData,userRole: {deleteMany: {},create: roleIds.map((roleId) => ({ roleId })),}},include: {userRole: true,},});return updatedUser;})
}delete(id: number): Promise<any> {return this.prismaClient.user.delete({ where: { id }})
}

5 ) 同步改造 user/dto/update-user.dto

import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
import { CreateRoleDto } from '@/role/dto/create-role.dto';
import { IsArray, IsInt, IsString, ValidateIf } from 'class-validator';
import { Type } from 'class-transformer';export class UpdateUserDto extends PartialType(CreateUserDto) {@IsInt()@ValidateIf((o) => !o.username)id?: number;@IsString()@ValidateIf((o) => !o.id)username?: string;@IsArray()@Type(() => CreateRoleDto)roles?: any[]
}

這樣,id 和 username 至少傳一個,否則報錯

6 ) 來測試一下

第一種方式

請求

curl --request PATCH \--url http://localhost:3000/user \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2' \--data '{"id": 4,"roles": [{"id": 5,"name": "普通用戶6","permissions": [{ "name": "user:read", "action": "read"}]}]
}'

響應

{"id": 4,"username": "wang1","password": "$argon2id$v=19$m=65536,t=3,p=4$5e2iqzVvSXK6e66adpsA+A$8dC09rixIdJiJMK5PjOkLlDr4VreYb9wq2WoB2KSsjo","userRole": [{"userId": 4,"roleId": 5}]
}

第二種方式

請求

{"username": "wang1","password": "$argon2id$v=19$m=65536,t=3,p=4$5e2iqzVvSXK6e66adpsA+A$8dC09rixIdJiJMK5PjOkLlDr4VreYb9wq2WoB2KSsjo","roles": [{"id": 5,"name": "普通用戶6","permissions": ["user:read","log:read","user:update"]}]
}

響應

{"id": 4,"username": "wang1","userRole": [{"userId": 4,"roleId": 5}]
}

可見兩種方式沒有區別

7 )響應中的一些敏感數據,用序列化的方式來排除

新建 user/dto/pbulic-update-user.dto

import { PartialType } from '@nestjs/mapped-types';
import { IsArray } from 'class-validator';
import { Exclude, Expose, Transform } from 'class-transformer';
import { UpdateUserDto } from './update-user.dto';export class PublicUpdateUserDto extends PartialType(UpdateUserDto) {@Exclude()password?: string;@IsArray()@Expose({ name: 'userRole'} )@Transform((value) => value.map((item) => item.roleId))roleIds: number[]
}

控制器上應用序列化 user/user.controller

 @Patch()@Serialize(PublicUpdateUserDto)update(@Body() updateUserDto: UpdateUserDto) {// return updateUserDto; // 測試的時候使用return this.userRepository.update(updateUserDto);}

重新請求,發現響應被優化了

{"id": 4,"username": "wang1","roleIds": [5]
}

8 ) 更多擴展

如果響應中要包含 permission 信息
可以在 repository.prisma 中繼續進行改造,此處不再贅述
下面就可以在 Guard 上 基于 UserRepo 中查詢出來的信息,添加判斷了

9 )守衛的處理

之前打通了守衛和路由和模塊的關聯關系,可以讀取路由和模塊上面的唯一標識
通過用戶的id和username來獲取用戶的角色和權限
我們之前寫過一個 role-permission.guard , 如果這個 guard 要使用 userRepository 的話
需要這個 guard 包含 userModule, 否則沒辦法使用 user里面的各類服務的

9.1 改造 user/user.repository

在 @Module 中加入

exports: [UserRepository],

這樣,就可以在DI系統中,在guard中使用UserRepository
很多模塊都需要使用到 UserModule,那么就可以把這個模塊設置為 @Global()

@Global() // 這里
@Module({imports: [TypeOrmModule.forFeature([User], TYPEORM_DB_CLIENT),MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),],controllers: [UserController],exports: [UserRepository],providers: [UserTypeOrmRepository,UserPrismaRepository,UserMongooseRepository,UserRepository]
})
export class UserModule {}

但并不意味著 全局模塊 設置的 越多越好,因為程序在首次加載的時候,要初始化這些實例的,是需要耗費資源的,全局模塊是很多地方都需要的模塊才可以設置為全局模塊

9.2 改造 user.controller

在 findAll 方法上加入 @Read() 的裝飾器

@Get()
@Read() // 加上這個
findAll(@Query('page', new ParseIntPipe({ optional: true})) page,@Query('limit', new ParseIntPipe({optional: true})) limit,
) {return this.userRepository.findAll(page, limit);
}

現在需要 user:read 的權限才能訪問這個 findAll 接口

9.3 請求一下試試

獲取用戶列表

請求

curl --request GET \--url http://localhost:3000/user/ \--header 'content-type: application/json' \--header 'x-tenant-id: prisma2'

響應

[{"id": 4,"username": "wang1","password": "$argon2id$v=19$m=65536,t=3,p=4$5e2iqzVvSXK6e66adpsA+A$8dC09rixIdJiJMK5PjOkLlDr4VreYb9wq2WoB2KSsjo"}
]

9.4 改造之前的 guard:src/common/guards/role-permission.guard.ts

上面請求之后,會打印出 read, 我們現在需要拼裝一下并完成后面的邏輯

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { PERMISSION_KEY } from '../decorators/role-permission.decorator';
import { UserRepository } from '@/user/user.repository';
import { RoleService } from '@/role/role.service';
import { ConfigService } from '@nestjs/config';@Injectable()
export class RolePermissionGuard implements CanActivate {constructor(private reflector: Reflector,private userRepository: UserRepository,private roleService: RoleService,private configService: ConfigService,) {}async canActivate(context: ExecutionContext,): Promise<boolean> {const classPermission = this.reflector.get(PERMISSION_KEY, context.getClass())const handlerPermission = this.reflector.get(PERMISSION_KEY, context.getHandler())// console.log('classPermission', classPermission); // user// console.log('handlerPermission', handlerPermission); // readconst cls = classPermission instanceof Array ? classPermission.join('') : classPermission;const handler = handlerPermission instanceof Array ? handlerPermission.join('') : handlerPermission;const right = `${cls}:${handler}`;// console.log(right);const req = context.switchToHttp().getRequest(); // 這里 控制器中的 Guard 掛載了,被當前這個guard拿到const { username } = req.user;const user = await this.userRepository.findOne(username);// console.log(user);if (!user) return false;const roleIds = user.userRole.map(o => o.roleId);// 如果 whitelist 中的用戶對應的 roleId 直接返回 trueconst whitelist = this.configService.get('ROLE_ID_WHITELIST');if (whitelist) {const whitelistArr = whitelist.split(',');// console.log('whitelistArr: ', whitelistArr);// 判斷whiltelistArr中包含roleIds中的數據,則返回trueif (whitelistArr.some(id => roleIds.includes(+id))) return true;}const permissions = await this.roleService.findAllByIds(roleIds);// console.log('permissions: ', permissions);// 處理數據const permArr = permissions.map(o => o.rolePermissions.map(one => one.permission.name)).reduce((acc, cur)  => {return [...new Set([...acc, ...cur])];}, []);// console.log('permArr', permArr);return permArr.some(o => o === right);}
}

9.5 改造 user 控制器

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { Permission, Read, Update } from '@/common/decorators/role-permission.decorator';
import { RolePermissionGuard } from '@/common/guards/role-permission.guard';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PublicUpdateUserDto } from './dto/public-update-user.dto';
// import { AuthGuard } from '@nestjs/passport';
import { AdminGuard } from '@/common/guards/admin.guard';
import { JwtGuard } from '@/common/guards/jwt.guard';
import { Public } from '@/common/decorators/public.decorator';@Controller('user')
// @UseGuards(AuthGuard('jwt'), AdminGuard, RolePermissionGuard)
// @UseGuards(JwtGuard, AdminGuard, RolePermissionGuard)
@UseGuards(JwtGuard, RolePermissionGuard)
@Permission('user')
// @Permission('user1') // 后期再支持
export class UserController {constructor(private userRepository: UserRepository,) {}@Get()// @Read()@Update()findAll(@Query('page', new ParseIntPipe({ optional: true})) page,@Query('limit', new ParseIntPipe({optional: true})) limit,) {return this.userRepository.findAll(page, limit);}
}

9.6 改造 role/role.service.ts 添加

 findAllByIds(ids: number[]) {return this.prismaClient.role.findMany({where: {id: {in: ids,},},include: {rolePermissions: {include: {permission: true,}}}})}

并將 role.service 暴露出來,在 role.module 中配置

import { Module } from '@nestjs/common';
import { RoleService } from './role.service';
import { RoleController } from './role.controller';@Module({controllers: [RoleController],providers: [RoleService],exports: [RoleService], // 這里
})
export class RoleModule {}

并在 user.module 中使用

@Global()
@Module({imports: [// ... ,RoleModule, // user Module 是全局模塊,這里也會可以直接使用],// ...
})
export class UserModule {}

9.7 添加 .env 配置,如下,這里是測試數據

# Role 默認角色
ROLE_ID=4
ROLE_ID_WHITELIST=4,5 # 超級管理員

9.6 之后通過 signup 接口獲取token之后,填入訪問用戶列表的接口進行訪問,可驗證上述權限已生效

總結

  • 我們完成了基于 NestJS 框架的 RBAC(Role-Based Access Control,基于角色的訪問控制)權限系統的完整實現
  • 整個實現過程雖然較為復雜,但涉及的技術細節豐富,具有極高的工程實踐價值

1 ) RBAC 的核心邏輯與架構設計

RBAC 的核心思想是將權限通過角色進行抽象,實現用戶與權限之間的間接關聯
傳統權限模型中若直接將用戶與權限綁定,會導致權限管理復雜度呈指數級上升
尤其是在用戶數量龐大、權限種類繁多的系統中。

通過引入“角色”這一中間層,我們可以將具有相同權限的用戶歸類,從而實現權限的集中管理。例如:

  • 用戶 A 屬于“管理員”角色;
  • “管理員”角色擁有“創建用戶”、“刪除文章”等權限;
  • 其他用戶如果也被賦予該角色,則自動繼承這些權限。

2 ) 基于 AOP 的裝飾器設計與路由權限控制

為了提升代碼的可維護性與擴展性,我們在 NestJS 中充分運用了 AOP(面向切面編程) 的思想,通過自定義裝飾器來實現權限控制的模塊化管理

  1. 模塊標識裝飾器的設計
    我們為每個 Controller 設計了一個唯一的標識符(字符串),用于標識該模塊。這種設計避免了因模塊名或路由路徑變更而導致數據庫權限配置失效的問題。

示例代碼如下:

@RoleGuard('user_module') // 標識該模塊為 "user_module"
@Controller('users')
export class UserController {}
  1. 路由權限裝飾器的設計
    在 Controller 的具體方法上,我們進一步設置了權限標識,例如:
@Permission('read_user') // 表示訪問該路由需要 "read_user" 權限 
@Get(':id')
findOne(@Param('id') id: string) {return this.userService.findOne(id);
}

這種設計使權限控制粒度精確到具體的接口方法,便于后續權限的動態配置與管理

3 ) 權限數據的動態加載與角色管理

在實際項目中,權限信息通常存儲在數據庫中,而不是硬編碼在程序中。我們實現了從數據庫動態加載用戶角色與權限的功能,并對以下關鍵操作進行了處理:

  • 用戶角色的讀取:包括單用戶多角色、多用戶多角色的場景
  • 權限的增刪改查:特別是更新操作中,涉及嵌套邏輯的處理
  • 關聯關系的管理:如用戶與角色、角色與權限之間的一對多關系。

在更新角色或權限時,我們采用的策略是先刪除原有關聯,再重新插入新數據,以確保數據一致性
但需注意的是,某些情況下(如權限本身不發生變化),我們不需要刪除權限實體,只需更新關聯關系。

4 ) 白名單機制與超級用戶權限的實現

為了應對系統初始化或特殊用戶訪問所有接口的需求,我們引入了 白名單機制(Whitelist)。

  • 白名單角色可以訪問系統中所有接口
  • 無需逐個配置權限
  • 可用于超級管理員、系統初始化用戶等特殊場景

這部分邏輯可定義在權限守衛(Guard)中:

const whitelistRoles = ['admin', 'super_admin'];if (whitelistRoles.includes(userRole)) {return true; // 白名單角色直接放行
}

5 ) 權限控制的擴展方向 —— 基于策略的權限(Policy-Based Access Control)

當前我們實現的 RBAC 權限控制是基于模塊與路由的“決策級”權限控制,但其顆粒度仍不夠精細
未來可以擴展為 基于策略的權限控制(PBAC),進一步提升權限管理的靈活性

PBAC 的核心思想:

  • 在方法內部根據具體數據字段進行權限控制;
  • 實現“數據級”而非“接口級”的權限控制;
  • 例如,限制用戶只能查看自己創建的文章,或只能編輯特定字段。

這種控制方式在 NestJS 官方文檔中也有提及,常見實現方式之一是與 CASL(Capability-based Access Control Library) 集成

示例邏輯如下:

@UseGuards(CaslGuard)
@Get(':id')
findOne(@Param('id') id: string, @User() user: User) {const article = this.articleService.findOne(id);if (!userCanAccess(user, article)) {throw new ForbiddenException('無權訪問該文章');}return article;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/921418.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/921418.shtml
英文地址,請注明出處:http://en.pswp.cn/news/921418.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

機器學習如何精準預測高值

一、概念理解“機器學習對于高值的預測保守”&#xff0c;這是建模里很常見的現象&#xff0c;尤其在生態、氣候、遙感這類數據分布高度偏斜的場景。通常可以從以下幾個角度理解&#xff1a;1. 數據分布與樣本稀缺在訓練集里&#xff0c;高值樣本往往非常少&#xff0c;遠低于中…

蜂窩物聯網模組:智能門禁產品上的關鍵部件

隨著物聯網技術的快速發展&#xff0c;蜂窩物聯網模組正逐步成為智能門禁系統的關鍵通信組件。蜂窩模組憑借其廣覆蓋、高可靠性和低功耗特性&#xff0c;正從傳統門禁系統的補充角色轉變為智能門禁的核心通信組件&#xff0c;尤其在智慧社區、商業樓宇和政府機構等場景中展現出…

[光學原理與應用-417]:非線性光學 - 線性光學(不引發頻率的變化)與非線性光學(引發頻率變化)的異同

一、定義與物理機制&#xff1a;線性響應 vs 非線性響應線性光學定義&#xff1a;光與物質相互作用時&#xff0c;介質的極化強度與入射光電場強度呈線性關系&#xff08;P?0?χ(1)E&#xff09;&#xff0c;輸出光強與輸入光強成正比&#xff08;Iout?∝Iin?&#xff09;-…

深入探討AI在三大核心測試場景中的應用

隨著人工智能&#xff08;AI&#xff09;技術的迅猛發展&#xff0c;軟件測試領域正經歷深刻變革。傳統手動測試和基于規則的自動化測試已難以應對日益復雜的系統架構與海量用戶行為。AI測試通過引入機器學習、自然語言處理、計算機視覺等技術&#xff0c;顯著提升了測試效率、…

[linux倉庫]性能加速的隱形引擎:深度解析Linux文件IO中的緩沖區奧秘

&#x1f31f; 各位看官好&#xff0c;我是egoist2023&#xff01; &#x1f30d; Linux Linux is not Unix &#xff01; &#x1f680; 今天來學習C語言緩沖區和內核緩存區的區別以及緩存類型。 &#x1f44d; 如果覺得這篇文章有幫助&#xff0c;歡迎您一鍵三連&#xff0c…

一、計算機的數據存儲

計算機的世界只有0和1。 1.1 進制 十進制整數->二進制整數&#xff1a;除2倒取余二進制->十進制&#xff1a;權值相加法 結論&#xff1a;1位8進制值 3位二進制值&#xff0c;1位十六進制值 4位二進制值 public class JinZhiDemo {public static void main(String[]…

SpringBoot集成XXL-JOB保姆教程

第一步&#xff1a; 下載xxl-job源碼到本地&#xff0c;地址如下&#xff1a; xxl-job: 一個分布式任務調度平臺&#xff0c;其核心設計目標是開發迅速、學習簡單、輕量級、易擴展。現已開放源代碼并接入多家公司線上產品線&#xff0c;開箱即用。 第二步&#xff1a; 創建…

Debezium日常分享系列之:Debezium 3.2.2.Final發布

Debezium日常分享系列之&#xff1a;Debezium 3.2.2.Final發布Debezium CoreConnector啟動時出現難以理解的錯誤臨時阻塞快照失敗可能導致數據丟失的問題修復Debezium for OracleDebezium CoreConnector 啟動時出現難以理解的錯誤 我們解決了一個問題&#xff0c;即連接器會因…

Zoom AI 技術架構研究:聯合式方法與多模態集成

一、研究背景與概述 在當今數字化轉型加速的背景下,人工智能技術正深刻改變企業協作與溝通方式。作為全球領先的視頻會議平臺,Zoom 已從單純的通信工具轉型為全面的生產力平臺,而其 AI 技術架構是這一轉變的核心驅動力。本報告將深入分析 Zoom 的 AI 技術架構,特別是其創新…

排序-快速排序 O(n log n)

快排&#xff1a;1、設定一個中間值 q[ lr >>1 ] , 讓左右區間來比較2、左邊通過 i 依次比較&#xff0c;如果比這個中間值小&#xff0c;就繼續 , 直到不符合3、右邊通過 j-- 依次比較&#xff0c;如果比這個中間值大&#xff0c;就繼續 &#xff0c;直到不符合4、兩邊…

【Proteus仿真】定時器控制系列仿真——LED小燈閃爍/流水燈/LED燈帶控制/LED小燈實現二進制

目錄 0案例視頻效果展示 0.1例子1&#xff1a;基于AT89C51單片機的定時器控制小燈閃爍 0.2例子2&#xff1a;基于AT89C51單片機的定時器T0流水燈 0.3例子3&#xff1a;基于AT89C51單片機的定時器控制LED燈帶 0.4例子4&#xff1a;基于AT89C51單片機的定時器控制LED閃爍 0…

進階向:密碼生成與管理工具

密碼生成與管理工具&#xff1a;從零開始的完全指南在現代數字生活中&#xff0c;密碼是保護個人信息和賬戶安全的第一道防線。隨著網絡服務的普及&#xff0c;每個人平均需要管理數十個不同賬戶的密碼。一個強大且獨特的密碼通常應包含12個以上字符&#xff0c;混合大小寫字母…

解決 Gitee 中 git push 因郵箱隱私設置導致的失敗問題

解決 Gitee 中 git push 因郵箱隱私設置導致的失敗問題 在使用 Git 向 Gitee 遠程倉庫推送代碼時&#xff0c;可能會遇到因郵箱隱私設置引發的 git push 失敗情況。最近我就碰到了&#xff0c;現在把問題現象、原因和解決方法分享出來。 一、錯誤現象 執行 git push -u origin …

Flutter的三棵樹

“三棵樹”是 Flutter 渲染和構建UI的核心機制&#xff0c;理解它們對于掌握 Flutter 至關重要。這三棵樹分別是&#xff1a; Widget 樹 Element 樹 RenderObject 樹 它們協同工作&#xff0c;以實現 Flutter 的高性能渲染和高效的響應式編程模型。 Flutter 是聲明式的UI&…

同一臺nginx中配置多個前端項目的三種方式

目錄 第一種方式:配置多個二級域名 第二種方式:配置端口轉發(不推薦) 第三種方式:同一個server中基于location配置(重點講解) 第一種方式:配置多個二級域名 一個域名下面申請多個二級域名,每個二級域名配置一個vue前端項目,這個很好配置,在這里不再詳細說明。 …

第二家公司雖然用PowerBI ,可能更適合用以前的QuickBI

第二家公司雖然用PowerBI &#xff0c;可能更適合用以前的QuickBI現在回想一下&#xff0c;第二家公司數據源是MySQL &#xff0c;常規報表是用excel報表&#xff0c;另外還做了一張能發布到web的看板供運營使用。基于基本情況&#xff0c;quickbi 的早期版本是合適的&#xff…

STM32 USBx Device HID standalone 移植示例 LAT1466

關鍵字&#xff1a;USBx&#xff0c; Device, HID&#xff0c;standalone 1.設計目的 目前 USBx Device standalone 的官方示例較少&#xff0c;不過使用 STM32CubeMX 可以快速地生成 USBx Device 相關類的示例工程&#xff0c;會很方便大家的開發。這里以 NUCLEO-H563 為例&…

python創建并寫入excel文件

大家好&#xff0c;這里是七七&#xff0c;今天來跟大家分享一個python創建并寫入一個excel文件的小例子&#xff0c;話不多說&#xff0c;開始介紹。首先我們來看一下這一小段代碼。import openpyxl# 創建一個新的 Excel 工作簿workbook openpyxl.Workbook()# 獲取當前活動的…

react native 出現 FATAL EXCEPTION: OkHttp Dispatcher

react native 出現 FATAL EXCEPTION: OkHttp Dispatcher 報錯信息FATAL EXCEPTION: OkHttp DispatcherProcess: , PID: 8868java.lang.NoSuchMethodError: No virtual method toString(Z)Ljava/lang/String; in class Lokhttp3/Cookie; or its super classes (declaration of o…

sentinel實現控制臺與nacos數據雙向綁定

有兩種方式可以實現&#xff1a;Springboot客戶端做相應配置&#xff08;推薦&#xff09;修改sentinel-dashboard的源碼一、Springboot客戶端做相應配置&#xff08;推薦&#xff09;1、添加依賴<dependency><groupId>com.alibaba.csp</groupId><artifac…