需求
我们的业务需求比较的简单 实现一个 如图的RBAC系统
- 实现user的管理 CRUD
- 实现role 的管理 CRUD
- 实现permission 权限组 的管理 CRUD
- 实现 menu 管理 CRUD
- 实现 button 管理 CRUD
user可以关联到role role 可以关联到权限组 ,权限组可以关联到具体的权限上
分析和设计
首先是表结构
重点和难点
关于我到底要不要在 独立的表 entity中添加附加字段 以关联到 中间表去
不添加到 独立的表 entity上
// 假设我们有role和user 它们是多对多关系,它们需要一个中间表user-role
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => UserRole, userRole => userRole.user)
userRoles: UserRole[];
}
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => UserRole, userRole => userRole.role)
userRoles: UserRole[];
}
@Entity()
export class UserRole {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, user => user.userRoles, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => Role, role => role.userRoles, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'role_id' })
role: Role;
}
添加上去
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
@Entity()
export class UserRole {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, user => user.userRoles, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: User;
@ManyToOne(() => Role, role => role.userRoles, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'role_id' })
role: Role;
}
这两种写法都是可以的,但是会有一些区别,
- 如果不添加entity 你需要这样查询
const userRoles = await UserRole.find({ where: { user: user1 } });
const roles = userRoles.map(userRole => userRole.role);
- 如果你添加了 那吗它的查询三这样的
const role = await Role.findOne({ where: { id: 1 }, relations: ['userRoles', 'userRoles.user'] });
const users = role.userRoles.map(userRole => userRole.user);
OneToMany 关系装饰器的作用是在实体之间建立一对多的关系,并且在数据库中生成外键约束来维护这种关系。如果您在 User 和 Role 实体中都定义了 userRoles 字段,并在这些字段上使用了 OneToMany 装饰器,那么您就可以通过这些字段来查询与用户和角色相关的中间表记录。
如果您想要在实体之间建立一对多的关系并方便地查询相关记录,可以在实体中定义相应的字段并使用 OneToMany 装饰器。但是,如果您只需要简单地存储多对多关系,并且不需要查询相关记录,那么您可以像我之前展示的那样,在中间表中存储这些关系。
关于自定义中间表的CRUD
复合主键的问题
问题和解决方案 来源以CHAT_GPT
Joney: Role-Permission 中 它与 Role 及 Permission 是多对多关系?
GPT: 是的,Role-Permission 中的关系是多对多关系。一个角色可以拥有多个权限,而一个权限也可以被多个角色拥有。因此, Role-Permission 表将角色和权限联系在一起,并充当它们之间的中间表,以便能够建立多对多关系。
Joney: 但是会有这样的问题 ”一个Permission 可以包含多个 Menu 比如Permission1 => Menu1 Menu2 Menu3; Permission => Menu1 Menu2 Menu3“ 一个Role 可以包含多个 Permission 那么 如果role1 拥有 Permission1 和 Permission2 那么是不是重复了?
GPT: 如果一个权限可以包含多个菜单,那么每个权限只需要在 Permission 实体中出现一次。例如,有一个名为 Permission1 的权限, 它包含菜单 Menu1、Menu2 和 Menu3。在 Permission 实体中,我们只需要创建一个名为 Permission1 的实体,并将其与菜单 Menu1、Menu2 和 Menu3 相关联。
当一个角色拥有多个权限时,如果其中有两个权限都包含相同的菜单,那么在 Role-Permission 实体中就会出现重复的条目。 例如,如果 role1 拥有 Permission1 和 Permission2,而 Permission1 和 Permission2 都包含菜单 Menu1, 那么在 Role-Permission 实体中将会有两个条目,分别表示 role1 拥有 Permission1 和 role1 拥 有Permission2。这不是重复,而是表明 role1 具有访问菜单 Menu1 的两个不同的途径。
Joney: 为什么你怎么说? 这不是重复,而是表明 role1 具有访问菜单 Menu1 的两个不同的途径。
GPT: 您是正确的,我之前的回答有误。如果 role1 拥有 Permission1 和 Permission2,而 Permission1 和 Permission2 都包含菜单 Menu1,那么在 Role-Permission 实体中将会有两个条目,分别表示 role1 拥有 Permission1 和 Permission2,但它们都授予 role1 访问 Menu1 的权限。因此,这确实是重复的条目, 因为它们表示相同的访问权限,可以通过删除其中一个条目来避免重复。抱歉给您带来的困惑。
Joney: 为什么 您 这么说 “这不是重复,而是表明 role1 具有访问菜单 Menu1 的两个不同的途径。”
我非常抱歉,我的回答有误。如果 role1 拥有 Permission1 和 Permission2,而 Permission1 和 Permission2 都包含菜单 Menu1,那么在 Role-Permission 实体中将会有两个条目,分别表示 role1 拥有 Permission1 和 Permission2,但它们都授予 role1 访问 Menu1 的权限。因此,这确实是重复的条目,因为它们表示相同的访问权限,可以通过删除其中一个条目来避免重复。多谢您的指正。
可是删除其中一个条目 role1 role2 其中的一个必受影响,如果其它user 恰好拥有 role1 role2 其中的一个 那么不就出问题了吗
您是正确的,如果删除 Role-Permission 实体中的其中一个条目,那么拥有这个角色的用户可能会受到影响。如果有一个用户拥有 role1 和 role2 中的其中一个,而这两个角色都授予了访问 Menu1 的权限,那么这个用户将失去其中一个角色所授予的访问权限。
为了避免这种情况,我们可以在设计 Role-Permission 实体时,使用复合主键来唯一标识一个角色和一个权限的组合。这样,即使一个角色拥有多个权限,每个权限也只会在 Role-Permission 实体中出现一次,从而避免了重复的问题。这样的话,删除其中一个条目将不会影响到其他角色或用户。
所以,在 Role-Permission 实体中,我们可以使用复合主键来表示角色和权限之间的关系,如下所示:
Copy
+---------+--------------+
| role_id | permission_id|
+---------+--------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
+---------+--------------+
在这个示例中,Role-Permission 实体使用复合主键 (role_id, permission_id) 来表示一个角色和一个权限的组合。这样,每个角色和权限的组合都是唯一的,并且不会出现重复的条目。
我不太明白你说的复合键 你详细解释一下
当一个实体需要使用多个属性来唯一标识一个记录时,我们可以使用复合键(Composite Key)来定义实体的主键。复合键由多个属性组成,这些属性的组合在整个实体中必须是唯一的。
在 Role-Permission 实体中,一个角色和一个权限的组合必须是唯一的,因此我们可以使用复合键来定义实体的主键,如下所示:
Copy
+---------+--------------+
| role_id | permission_id|
+---------+--------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
+---------+--------------+
在这个示例中,(role_id, permission_id) 构成了 Role-Permission 实体的复合主键。这意味着,每个角色和权限的组合都是唯一的,并且可以使用这个组合来标识 Role-Permission 实体中的每个记录。
使用复合键可以确保实体中的记录是唯一的,并且可以更精确地定义记录之间的关系。在 Role-Permission 实体中,使用复合键可以避免重复记录,同时确保每个角色和权限的组合都可以被唯一地识别。
你能给我举个其他实体使用复合键的例子吗?
当一个实体需要使用多个属性来唯一标识一个记录时,就可以使用复合键。下面是一个使用复合键的例子:
假设我们有一个 Student 实体,每个学生都有一个学生编号和一个学生姓名。此外,每个学生可以被分配到一个或多个课程,并且在每个课程中有一个成绩。在这种情况下,我们可以使用复合键来定义 Student_Course 实体的主键,以便能够唯一地标识每个学生在每个课程中的成绩。
Copy
+--------------+-------------+--------+
| student_id | course_id | grade |
+--------------+-------------+--------+
| 1 | 101 | 85 |
| 1 | 102 | 90 |
| 2 | 101 | 80 |
+--------------+-------------+--------+
在这个示例中,(student_id, course_id) 构成了 Student_Course 实体的复合主键。这意味着,每个学生和课程的组合都是唯一的,并且可以使用这个组合来标识 Student_Course 实体中的每个记录。这样,我们就可以记录每个学生在每个课程中的成绩,并且可以使用复合键来确保每个学生和课程的组合都是唯一的。
在typeorm中如何实现? 比如我们之前提到的 role-permission 问题
@Entity()
export class RolePermission extends Base {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => RoleInfo, (role) => role.rolePermissions, {
onDelete: 'CASCADE',
})
@JoinColumn()
roles: RoleInfo;
@ManyToOne(() => Permission, (permission) => permission.rolePermissions, {
onDelete: 'CASCADE',
})
@JoinColumn()
permission: Permission;
}
在 typeorm 中,可以使用 @PrimaryColumn() 和 @Column() 装饰器来定义实体的主键和其他列。对于使用复合键的实体,我们可以使用 @PrimaryColumn() 装饰器将多个列组合为实体的主键。
对于 RolePermission 实体,我们可以使用 @PrimaryColumn() 装饰器将 role_id 和 permission_id 组合成复合主键,如下所示:
Copy
@Entity()
export class RolePermission extends BaseEntity {
@PrimaryColumn()
role_id: number;
@PrimaryColumn()
permission_id: number;
@ManyToOne(() => Role, role => role.permissions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'role_id' })
role: Role;
@ManyToOne(() => Permission, permission => permission.roles, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'permission_id' })
permission: Permission;
}
在这个示例中,role_id 和 permission_id 组合成了 RolePermission 实体的复合主键。这样,每个角色和权限的组合都是唯一的,并且可以使用这个组合来标识 RolePermission 实体中的每个记录。
注意,在 @JoinColumn() 装饰器中,我们需要指定外键的名称,以便 typeorm 可以正确地在数据库中创建外键约束。在这个示例中,我们指定了 role_id 和 permission_id 作为外键列的名称。
复合主键是如何解决我上述的问题
apache Copy 一个Permission 可以包含多个 Menu 比如Permission1 => Menu1 Menu2 Menu3; Permission2 => Menu1 Menu2 Menu3“ 一个Role 可以包含多个 Permission 那么 如果role1 拥有 Permission1 和 Permission2 那么 这些permission包含的 同样的 menu这些就重复了 使用复合主键可以避免在 Role-Permission 实体中出现重复的记录,并确保每个角色和权限的组合都是唯一的。在您之前提到的情况中,如果一个角色拥有多个权限,而这些权限都授予了访问相同菜单的权限,那么这些权限和菜单的组合将会在 Role-Permission 实体中出现多次,从而导致重复。
使用复合主键,我们可以将 role_id 和 permission_id 两个列组合成 Role-Permission 实体的主键,如下所示:
+---------+--------------+
| role_id | permission_id|
+---------+--------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
+---------+--------------+
在这个示例中,每个角色和权限的组合都是唯一的,并且可以使用这个组合来标识 Role-Permission 实体中的每个记录。这样,即使多个权限都授予了访问相同菜单的权限,每个组合仍将在 Role-Permission 实体中只出现一次。
这种方法可以确保在 Role-Permission 实体中不会出现重复的记录,并确保每个角色和权限的组合都是唯一的。同时,这也是一种标准的数据库设计模式,可以避免许多常见的问题,例如数据冗余和不一致性。
这种解决方案 在数据库存储permission 的时候能解决重复吗? 比如
PermissionId=1 MenuId:1
PermissionId=2 MenuId:1
PermissionId=3 MenuId:1
使用复合主键可以确保在 Role-Permission 实体中不会出现重复的记录,并确保每个角色和权限的组合都是唯一的。但是,如果在 Permission 实体中出现多个记录都授予了访问相同菜单的权限,那么这些记录将会在数据库中出现多次,从而导致重复。
如果您希望在数据库中避免出现重复的权限记录,可以考虑在 Permission 实体中使用唯一约束来确保每个菜单只被授予一次权限,如下所示:
Copy
@Entity()
export class Permission {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Menu, (menu) => menu.permissions)
menu: Menu;
@ManyToMany(() => Role, (role) => role.permissions)
roles: Role[];
@Index('UQ_permission_menu', ['menu'], { unique: true })
@Column()
menu_id: number;
}
在这个示例中,我们在 menu_id 列上创建了一个唯一约束,确保每个菜单只出现一次。这样,当多个权限都授予了访问相同菜单的权限时,只有一个记录将被插入到数据库中,并且其他重复记录将被拒绝。
需要注意的是,使用唯一约束可以确保数据库中不会出现重复的记录,但这并不是一种完美的解决方案。如果您的应用程序需要在多个角色之间共享相同的权限,那么您可能需要在 Role-Permission 实体中为每个角色分别插入一条记录,这可能会导致一些冗余的数据。在这种情况下,您需要在冗余数据和数据一致性之间做出权衡,并根据您的实际需求选择最适合您的解决方案。
但是你这个做法会导致 Permision - Menu 之间的多对多关系 发生异常
您提到的情况是正确的,因为在 Permission 实体中,menu 和 permissions 之间是多对多关系,如果我们在 menu_id 列上创建唯一约束,那么每个菜单只能被授予一次权限,这可能会导致数据不一致。
在这种情况下,我们可以考虑在 Role-Permission 实体中使用复合主键来解决重复问题。这样,即使多个权限都授予了访问相同菜单的权限,每个组合仍将在 Role-Permission 实体中只出现一次。同时,我们可以在 Permission 实体中保留多对多关系,以确保每个菜单可以被多个权限授予。
下面是 Role-Permission 实体的示例代码,其中使用了复合主键来确保每个角色和权限的组合都是唯一的:
Copy
@Entity()
export class RolePermission {
@ManyToOne(() => Role, role => role.permissions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'role_id' })
role: Role;
@ManyToOne(() => Permission, permission => permission.roles, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'permission_id' })
permission: Permission;
@PrimaryColumn()
role_id: number;
@PrimaryColumn()
permission_id: number;
}
在这个示例中,role_id 和 permission_id 组合成了 Role-Permission 实体的复合主键。这样,即使多个权限都授予了访问相同菜单的权限,每个组合仍将在 Role-Permission 实体中只出现一次。同时,我们可以在 Permission 实体中保留多对多关系,以确保每个菜单可以被多个权限授予。
所以你说的 复合主键 实际上 还是会在DB存储相同的条目的数据 ,但是查询的时候只会查询一条(在entity中) 我是否理解正确
是的,您的理解是正确的。使用复合主键可以确保在数据库中不会出现重复的记录,并确保每个角色和权限的组合都是唯一的。但是,如果在 Permission 实体中出现多个记录都授予了访问相同菜单的权限,那么这些记录将会在数据库中出现多次,从而导致重复。
当您查询数据库时,使用复合主键可以确保每个角色和权限的组合只返回一条记录。这是因为在使用复合主键定义实体时,TypeORM 会自动合并具有相同主键值的记录,以便只返回一条记录。这样,您可以在查询结果中获得正确的数据,并避免重复记录的问题。
需要注意的是,使用复合主键只能确保在查询实体时返回正确的数据,而无法处理数据库中的重复记录。如果您需要确保数据库中不会出现重复记录,您可能需要使用其他方法,例如在 Permission 实体中使用唯一约束。
以 User,Role , Menu 演示一下
了解以上 的问题和需求之后我们看看如何实现
Entity
@Entity()
@Tree('materialized-path')
export class Menu extends Base {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({
comment: '菜单类型 1 目录 2 实际菜单 类型目录的时候 childMenus 才有值',
})
type: number;
@Column()
description: string;
@Column()
icon: string;
// 如果进行嵌套树?
@TreeParent()
parentMenu: Menu;
// 一般而言 OneToMany 的这一边为 虚拟键
@TreeChildren()
childMenus: Menu[];
// 虚拟键 关联到外部的一张表 这里可以先不关注他
@OneToMany(() => PermissionMenu, (premissionMenu) => premissionMenu.menu, {
cascade: true,
})
permissionMenus: PermissionMenu[];
}
@Entity()
export class UserInfo extends Base {
@PrimaryGeneratedColumn()
id: number;
@Column({
unique: true,
})
username: string;
@Column()
email: string;
@Column()
password: string;
@Column({
type: 'int',
default: 1,
comment: '1 禁用 2 启用',
})
state: number;
@OneToMany(() => UserRole, (userRole) => userRole.users, { cascade: true })
userRoles: UserRole[];
}
@Entity()
export class RoleInfo extends Base {
@PrimaryGeneratedColumn()
id: number;
@Column({
unique: true,
})
name: string;
@Column()
icon: string;
@Column()
description: string;
// 虚拟的key 表中是没有的 这里可以先不关注他们
@OneToMany(() => UserRole, (userRole) => userRole.roles, { cascade: true })
userRoles: UserRole[];
@OneToMany(() => RolePermission, (rolePermission) => rolePermission.roles, {
cascade: true,
})
rolePermissions: RolePermission[];
}
// 详见 这个 文档https://typeorm.io/many-to-many-relations#bi-directional-relations
// user-role 中间表
@Entity()
export class UserRole extends Base {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => UserInfo, (user) => user.userRoles, {
onDelete: 'CASCADE',
})
@JoinColumn()
users: UserInfo;
@ManyToOne(() => RoleInfo, (role) => role.userRoles, {
onDelete: 'CASCADE',
})
@JoinColumn()
roles: RoleInfo;
}
Menu
Menu 的处理最简单 它比较的独立 不需要考虑关联来关联去的
// contoller
// 注意可以递归的树结构
@Controller({
path: '/menu',
scope: Scope.REQUEST,
})
@ApiTags('menu')
@ApiExtraModels(PagenationWrapResDTO)
@SerializeOptions({
enableImplicitConversion: false,
})
@ApiBearerAuth()
@UseInterceptors(ClassSerializerMysqlInterceptor)
export default class MenuController {
constructor(
private readonly menuService: MenuService,
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger: LoggerService,
) {} // please do CRUD here
throwError(message: string, httpCode: HttpStatus) {
throw new HttpException(
{
status: httpCode,
error: message,
},
httpCode,
);
}
@Post('/add')
@MysqlEntityClass(MenuResDTO)
@ApiResponse({
type: MenuResDTO,
})
addMenu(@Body() createMenu: MenuCreateReqDTO) {
try {
return this.menuService.addMenu(createMenu);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('创建失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get('/all')
@MysqlEntityClass(MenuResDTO)
@ApiResponse({
type: MenuResDTO,
})
// 这个值必选传 但是我们把 -1 当做不存在的查询条件
getAllACB(@Query('parentId', ParseIntPipe) parentId: number) {
try {
return this.menuService.getAllMenus(parentId <= 0 ? null : parentId);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('查询失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Put('/update')
@MysqlEntityClass(MenuResDTO)
@ApiResponse({ type: MenuResDTO })
updateACB(@Body() menuInfo: MenuCreateReqDTO) {
try {
return this.menuService.updateACB(menuInfo);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('更新失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Delete('/delete')
deleteACB(
@Query(
'ids',
new ParseArrayPipe({
items: Number,
separator: ',',
}),
)
ids: number[],
) {
try {
return this.menuService.deleteACB(ids);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('删除失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
// service
@Injectable()
export default class MenuService {
constructor(
@InjectEntityManager('rbac_db')
private entityManager: EntityManager,
) {}
async addMenu(menuInfo: MenuCreateReqDTO) {
const menu = Object.assign(new Menu(), menuInfo);
if (menuInfo.parentMenuID) {
const parentMenu = await this.entityManager.findOne(Menu, {
where: {
id: menuInfo.parentMenuID,
},
});
menu.parentMenu = parentMenu;
}
return this.entityManager.save(Menu, menu);
}
async getAllMenus(praentId?: number) {
if (!praentId) {
return await this.entityManager.getTreeRepository(Menu).findRoots();
}
const findPraentMenu = await this.entityManager.findOne(Menu, {
where: { id: praentId },
relations: ['childMenus'],
});
const findRes = await this.entityManager
.getTreeRepository(Menu)
.findDescendantsTree(findPraentMenu);
return findRes;
}
async updateACB(menuInfo: MenuCreateReqDTO) {
return this.entityManager.update(Menu, { id: menuInfo.id }, menuInfo);
}
async deleteACB(ids: number[]) {
return this.entityManager.delete(Menu, ids);
}
}
user
// controller
@Controller({
path: '/user',
scope: Scope.REQUEST,
})
@ApiExtraModels(PagenationWrapResDTO, UserDetailResDTO)
@ApiTags('user')
@SerializeOptions({
enableImplicitConversion: false,
})
@ApiBearerAuth()
@UseInterceptors(ClassSerializerMysqlInterceptor)
export default class UserController {
constructor(
private readonly userService: UserService,
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger,
) {}
throwError(message: string, httpCode: HttpStatus) {
throw new HttpException(
{
status: httpCode,
error: message,
},
httpCode,
);
}
@Get('/users-page')
@MysqlEntityClass(UserInfoListResDTO)
@ApiPaginatedResponse(UserDetailResDTO)
async getUserList(
@Query() pagenation: PagenationReqDTO,
): Promise<UserInfoListResDTO> {
try {
const res = await this.userService.getUserList(pagenation);
return res;
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('查询失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get('/user-info/:id')
@MysqlEntityClass(UserInfoResDTO)
@ApiResponse({
type: UserInfoResDTO,
})
getUserInfo(@Param('id', ParseIntPipe) id: number) {
try {
return this.userService.getUserInfo(id);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('查询失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Put('/update-user')
@MysqlEntityClass(UserInfoResDTO)
@ApiResponse({ type: UserInfoResDTO })
updateUser(@Body() userInfo: UpdateUserInfoReqDTO) {
try {
userInfo.password = encryptPassword(userInfo.password);
return this.userService.updateUser(userInfo);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('更新失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Delete('/delete-user')
@MysqlEntityClass(null)
deleteUser(
@Query(
'ids',
new ParseArrayPipe({
items: Number,
separator: ',',
}),
)
ids: number[],
) {
try {
return this.userService.deleteUser(ids);
} catch (error) {
this.logger.error(JSON.stringify(error));
this.throwError('删除失败', HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
// service
@Injectable()
export default class UserService {
constructor(
@InjectEntityManager('rbac_db')
private entityManager: EntityManager,
) {}
async getUserList(pageInfo: PagenationReqDTO) {
const res = await DoPagenation<UserInfo>(
pageInfo,
this.entityManager,
UserInfo,
{},
{},
);
return res;
}
async getUserInfo(id: number) {
const sqlRes = await this.entityManager.findOne(UserInfo, {
where: { id: id },
relations: {
userRoles: {
roles: {
rolePermissions: {
permission: {
permissionMenus: {
menu: true,
},
permissionABs: {
actionButton: true,
},
},
},
},
},
},
});
const menusMap = new Map();
const menus = sqlRes.userRoles.reduce((prev, userRole) => {
const rolePermissions = userRole.roles.rolePermissions;
rolePermissions.forEach((rolePermission) => {
const permission = rolePermission.permission;
const permissionMenus = permission.permissionMenus;
permissionMenus.forEach((permissionMenu) => {
const menu = permissionMenu.menu;
if (!menusMap.has(menu.id)) {
menusMap.set(menu.id, true);
prev.push(menu);
}
});
});
return prev;
}, []);
const actionButtonMap = new Map();
const actionButtons = sqlRes.userRoles.reduce((prev, userRole) => {
const rolePermissions = userRole.roles?.rolePermissions;
rolePermissions.forEach((rolePermission) => {
const permission = rolePermission.permission;
const permissionABs = permission.permissionABs;
permissionABs?.forEach((permissionMenu) => {
const actionButton = permissionMenu.actionButton;
if (actionButton && !actionButtonMap.has(actionButton.id)) {
actionButtonMap.set(actionButton.id, true);
prev.push(actionButton);
}
});
});
return prev;
}, []);
const roles = sqlRes.userRoles.map((item) => {
delete item.roles.rolePermissions;
return { ...item.roles };
});
return {
...sqlRes,
roles,
menus,
actionButtons,
};
}
async updateUser(userInfo: UpdateUserInfoReqDTO) {
// 1. 通过entity找到原数据
let user = await this.entityManager.findOne(UserInfo, {
where: { id: userInfo.id },
relations: {
userRoles: true,
},
});
const role = await this.entityManager.findByIds(RoleInfo, userInfo.roles);
// 2. 构建新数据 新中间表
let newUserRoles = role?.map((item) => {
return Object.assign(new UserRole(), {
roles: item,
});
});
// 3. 删除原中间表数据
await this.entityManager.remove(UserRole, user.userRoles);
// 4. 更新的时候 保存中间件表
if (userInfo.roles.length) {
await this.entityManager.save(UserRole, newUserRoles);
}
delete user.userRoles;
user = {
...user,
...userInfo,
};
// 5.关联上主表 然后再保存
const { id } = await this.entityManager.save(UserInfo, user, {});
newUserRoles = newUserRoles.map((item) => ({ ...item, users: user }));
if (userInfo.roles.length) {
await this.entityManager.save(UserRole, newUserRoles);
}
return this.getUserInfo(id);
}
deleteUser(ids: Array<number>) {
return this.entityManager.delete(UserInfo, ids);
}
}
其中 重点和难点是: 关联表数据的查询,当然你可以直接用sql 不必和我写的一样
role
这里我就不说了 比较的简单 虽然也有关联表的逻辑 但是做法和 user 是一样的 其它的模块 照猫画虎的一一去实现就好了
最后得到完整的Service
RBAC实现
我们有两种方式
2 另一种RBAC 基于 Claims-based authorization https://juejin.cn/post/7231860080471523384
这里我们选择 第一种方案
// 自定义装饰器
import { SetMetadata } from '@nestjs/common';
// 我们设定 以 name 为 唯一值
export enum Role {
Admin = 'admin',
Editor = 'editor',
Guset = 'guest',
}
export const ROLES_KEY = 'ROLES';
// 装饰器Roles SetMetadata将装饰器的值缓存
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// 自己实现一个 Guard
@Injectable()
export class RoleGuard implements CanActivate {
constructor(
private reflector: Reflector,
private authUserService: AuthUserService,
private readonly authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1.通过反射获取到装饰器的权限
// getAllAndOverride读取路由上的metadata getAllAndMerge合并路由上的metadata
const requireRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// 2.获取req拿到鉴权后的用户数据
const req = context.switchToHttp().getRequest();
const userTokenInfo = this.authService.decodeToken(
req.headers.authorization.replace(/Bearer /, ''),
);
// 3.通过用户数据从数据查询权限
const userInfo = await this.authUserService.findOne({
username: userTokenInfo.username,
id: userTokenInfo.sub,
});
const roles = userInfo.roles.map((item) => item.name);
// 4.判断用户权限是否为装饰器的权限 的some返回boolean
const flage = roles.some((role) => requireRoles.includes(role as any));
return flage;
}
}
// 使用起来也比较合理
providers: [RoleGuard],
})
export class CoreModule {}
@Controller({
path: '/acb',
scope: Scope.REQUEST,
})
@ApiExtraModels(PagenationWrapResDTO)
@ApiTags('acb')
@SerializeOptions({
enableImplicitConversion: false,
})
@ApiBearerAuth()
// @Roles(Role.Admin)
@Roles(Role.Guset)
@UseGuards(RoleGuard)
@UseInterceptors(ClassSerializerMysqlInterceptor)
export default class ACBController {
// ......
}