4-6 进阶RBAC角色权限实现:用户创建&关联角色
概述
本节实现用户创建时关联角色的核心逻辑,包括:DTO 设计与 Transform 转换、Prisma 嵌套创建(connectOrCreate)、用户 Repository 封装,以及通过 username 查询用户及其角色的联合查询。
RBAC 角色权限模型
RBAC(Role-Based Access Control)的核心思想是通过角色来关联用户和权限:
用户(User)──(多对多)── 角色(Role)──(一对多)── RolePermission ──(多对一)── 权限(Permission)
UserRole 关联表 中间表
text
一个用户可以拥有多个角色,一个角色可以拥有多个权限。这种多对多关系通过中间表(UserRole、RolePermission)实现。
Prisma Schema 定义
model User {
id Int @id @default(autoincrement())
username String @unique
password String
userRoles UserRole[] @relation("userRoles")
@@map("users")
}
model Role {
id Int @id @default(autoincrement())
name String @unique
description String?
users UserRole[] @relation("roleUsers")
rolePermissions RolePermission[]
@@map("roles")
}
model UserRole {
userId Int @map("user_id")
roleId Int @map("role_id")
role Role @relation("roleUsers", fields: [roleId], references: [id])
user User @relation("userRoles", fields: [userId], references: [id])
@@id([userId, roleId])
@@map("user_roles")
}
model Permission {
id Int @id @default(autoincrement())
name String @unique
action String
rolePermissions RolePermission[]
@@map("permissions")
}
model RolePermission {
roleId Int @map("role_id")
permissionId Int @map("permission_id")
role Role @relation(fields: [roleId], references: [id])
permission Permission @relation(fields: [permissionId], references: [id])
@@id([roleId, permissionId])
@@map("role_permissions")
}
prisma
创建用户 DTO 设计
DTO 结构定义
// user/dto/create-user.dto.ts
import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator';
import { Type, Transform } from 'class-transformer';
export class CreateUserRoleDto {
@IsString()
name: string;
@IsOptional()
@IsArray()
permissions?: CreatePermissionDto[];
}
export class CreatePermissionDto {
@IsString()
name: string;
@IsOptional()
@IsString()
action?: string;
}
export class CreateUserDto {
@IsString()
username: string;
@IsString()
password: string;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateUserRoleDto)
roles?: CreateUserRoleDto[];
}
typescript
Transform 处理 permission 字符串数组
当客户端传入字符串数组形式的 permissions 时,需要通过 Transform 转换为对象结构:
// 方式一:客户端传入对象数组
{ "roles": [{ "name": "普通用户", "permissions": [{ "name": "user_read", "action": "read" }] }] }
// 方式二:客户端传入字符串数组(需要 Transform)
{ "roles": [{ "name": "普通用户", "permissions": ["user_read", "user_create"] }] }
typescript
// Transform:字符串数组 → 对象数组
@Transform(({ value }) =>
value?.map((item: string) => {
const parts = item.split('_');
return {
name: item, // name 保持完整(唯一标识)
action: parts[1] || '', // action 从 name 中提取
};
}) || []
)
permissions?: CreatePermissionDto[];
typescript
Repository 层封装
UserRepository
// user/user.prisma.repository.ts
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class UserPrismaRepository {
constructor(private prismaClient: PrismaClient) {}
// 通过 username 查询用户及其角色
async findByUsername(username: string) {
return this.prismaClient.user.findFirst({
where: { username },
include: {
userRoles: {
include: {
role: true, // 包含角色信息
},
},
},
});
}
// 创建用户
async create(data: any) {
return this.prismaClient.user.create({
data,
include: {
userRoles: true,
},
});
}
}
typescript
模块注册
// user/user.module.ts
import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Module({
providers: [UserService, UserPrismaRepository, PrismaClient],
exports: [UserService, UserPrismaRepository],
})
export class UserModule {}
typescript
用户创建与角色关联
UserService.create 实现
// user/user.service.ts
import { Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
import { UserPrismaRepository } from './user.prisma.repository';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UserService {
constructor(private userRepository: UserPrismaRepository) {}
async create(createUserDto: CreateUserDto) {
const { password, roles, ...rest } = createUserDto;
// 密码哈希
const hashedPassword = await argon2.hash(password);
const user = await this.userRepository.create({
...rest,
password: hashedPassword,
userRoles: {
create: (roles || []).map((role) => ({
role: {
connectOrCreate: {
where: { name: role.name },
create: {
name: role.name,
description: role.name,
},
},
},
// 角色关联权限(嵌套创建)
rolePermissions: {
create: (role.permissions || []).map((permission) => ({
permission: {
connectOrCreate: {
where: { name: permission.name },
create: {
name: permission.name,
action: permission.action || '',
},
},
},
})),
},
})),
},
});
return user;
}
}
typescript
connectOrCreate 嵌套创建流程
创建用户
├── 创建 UserRole 关联
│ └── connectOrCreate Role(角色存在则连接,不存在则创建)
│ └── 创建 RolePermission 关联
│ └── connectOrCreate Permission(权限存在则连接,不存在则创建)
text
关键 API 对比:
| Prisma API | 用途 | 说明 |
|---|---|---|
connect | 连接已有关联 | 关联必须已存在,否则报错 |
create | 创建新关联 | 始终创建新记录 |
connectOrCreate | 连接或创建 | 存在则连接,不存在则创建(推荐) |
set | 替换所有关联 | 先清除所有再设置新的(用于更新) |
通过 username 查询用户及角色权限
Guard 中需要通过 username 查询用户及其完整的角色权限信息:
// user.prisma.repository.ts
async findByUsername(username: string) {
return this.prismaClient.user.findFirst({
where: { username },
include: {
userRoles: {
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true, // 完整的权限链路
},
},
},
},
},
},
},
});
}
typescript
查询结果结构:
{
id: 1,
username: "tom",
userRoles: [
{
roleId: 2,
role: {
name: "超级管理员",
rolePermissions: [
{ permission: { name: "user:read", action: "read" } },
{ permission: { name: "user:create", action: "create" } }
]
}
}
]
}
typescript
响应序列化
创建用户后,需要通过 DTO 序列化排除敏感信息:
// user/dto/public-user.dto.ts
import { Exclude, Expose, Type } from 'class-transformer';
export class PublicUserDto {
id: number;
username: string;
@Exclude()
password: string;
@Expose({ name: 'userRoles' })
@Transform(({ value }) => value?.map((item: any) => item.roleId) || [])
roleIds: number[];
}
typescript
// user.controller.ts
@Post()
@SerializeOptions({ type: PublicUserDto })
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
typescript
测试数据示例
// 创建用户 - 关联已有角色和新权限
POST /user
{
"username": "tom",
"password": "123456",
"roles": [
{
"name": "普通用户",
"permissions": [
{ "name": "user:read", "action": "read" },
{ "name": "user:create", "action": "create" }
]
}
]
}
json
RBAC 模型通过角色这一中间层,实现了灵活的权限管理——增加新权限只需关联到角色,不需要逐个修改用户。connectOrCreate 确保了角色和权限的幂等创建。
↑