RBAC和ACL
ACL
ACL权限控制是一种控制用户权限访问的手段,一个用户就对应了一个权限,讲起来比较抽象。
也就是说:我们现在有ABC三个接口,但是用户二只能访问A,不能让他看到B,但是用户三只有AB两个权限。(最常见的例子比如:学生管理系统,包括学生老师管理员三种角色)
我们用图来解释一下咱们的acl权限控制:
通过图我们能很直观的看出来ACL权限控制是做了件什么事。
通常我们使用ACL权限控制我们只需要建立三张表即可:
用户表,权限表以及权限和用户的中间表。
RBAC
现在我们有这样一个场景:现在有abc三个管理员,他们有aaa这个权限。现在我们新出了一个bbb权限,那么我们就要分别给abc三个管理员都加上bbb这个权限。这里只有三个管理员和一个新权限可能感觉不出来,那如果是一百个管理员和一百个权限,是不是这种方法就显得累赘。这时候我们就出现了RBAC这种新的权限控制方法。
RBAC也就是说在ACL中间加一层,先给每个用户分配角色,然后给每个角色分配权限,就像上面说的三个管理员和一权限,用ACL我需要添加三次,而RBAC这种方式只需要添加一次就够了。也就是只需要在权限表中吧bbb这个权限添加给管理员,这样abc三个管理员都有这个权限了。听起来还是有点抽象,我们还是看图:
这张图就更加直观的反应了我们RBAC权限控制的流程。
我们想要实现这个RBAC的控制流程我们就需要建立五张表:
用户,角色,权限以及用户角色关联表和角色权限关联表。
nest+mysql+prisma实现RBAC
这里就不介绍nest,mysql和prisma分别是啥了。简单来说:nest是一个nodejs的企业级开发框架,mysql就是数据库,prisma是操作数据库的orm可以让我们不用写sql用他们封装好的方法就能操作数据库的增删改查。
新建nest项目
pnpm i -g @nestjs/cli //全局安装nest脚手架
nest new rbac-test // 新建nest项目
然后安装需要的依赖,我们这里只需要安装prisma所需要的依赖就行了,为了方便调试,在安装一个swagger依赖。
pnpm install --save @nestjs/swagger
pnpm install prisma --save-dev
这里就不说怎么配置了详情可以看文档‘
nest集成prisma
nest集成swagger
集成完以后项目目录应该是这样的
然后我们可以在prisma文件夹下面的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-init
generator client {
provider = "prisma-client-js"
}
//provider 为mysql数据库
//url 为读取的数据源来源也就是你需要在目录中.env配置你的数据库连接信息
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// 用户表
model User {
id String @id @default(uuid())
username String
password String
nickName String?
email String
phone String?
avatar String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roles Role[]
UserRole UserRole[]
}
//角色表
model Role {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(references: [id], fields: [userId])
userId String?
UserRole UserRole[]
RoleAuthority RoleAuthority[]
authorities Authority[]
}
//用户角色关联表
model UserRole {
userId String
roleId String
user User @relation(fields: [userId], references: [id])
role Role @relation(fields: [roleId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@id([userId, roleId])
}
//权限表
model Authority {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Role Role? @relation(fields: [roleId], references: [id])
roleId String?
RoleAuthority RoleAuthority[]
}
//权限角色关联表
model RoleAuthority {
id String @id @default(uuid())
roleId String
authorityId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Role Role @relation(fields: [roleId], references: [id])
Authority Authority @relation(fields: [authorityId], references: [id])
}
这里就不具体说怎么完成登录注册,就是使用jwt区分token。
我们可以在表中新建三个用户,两个角色和8个权限 具体的我们参考下面这个图
可以看看每个表:
这里的表就是按照上面的图所写的数据。
我们在nest里面生成aaa bbb 的一些crud,在nest中生成crud相当简单
nest g res aaa
nest g res bbb
这样就能生成aaa和bbb所有的crud接口。
我们现在就是要用权限去控制这些接口的访问。
管理员的角色有 aaa、bbb 的增删改查权限,而普通用户只有 bbb 的增删改查权限。
这里省略验证用户是否登录的过程。
新建一个自定义守卫
nest g guard permission --no-spec --flat
在app.module下面注册一下我们刚刚新建的自定义守卫。
然后我们导出一下user.module
然后我们在装饰器里面注入UserService:
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { UserService } from './user.service';
@Injectable()
export class PermissionGuard implements CanActivate {
@Inject(UserService)
private userService: UserService;
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log(this.userService);
return true;
}
}
然后在 userService 里实现查询 role 的信息的 service:
async findRolesByIds(roleIds: string[]) {
return this.prisma.role.findMany({
where: {
id: {
in: roleIds
}
},
include: {
RoleAuthority: {
include: {
Authority: true
}
}
}
})
}
然后我们改造一下PermissionGuard
@Injectable()
export class PermissionGuard implements CanActivate {
@Inject(UserService)
private userService: UserService;
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
if (!request.user) {
return true;
}
const arr = []
arr.push(request.user.roles)
const roles = await this.userService.findRolesByIds(arr.map(item => item.id))
const permissions: Authority[] = roles.reduce((total, current) => {
total.push(...current.RoleAuthority.map(item => item.Authority));
return total;
}, []);
console.log(permissions);
return true;
}
}
我们在请求头里面加上登录的token信息
然后访问接口就能看见该用户的权限列表
然后在自定义一个decorator:
export const RequirePermission = (...permissions: string[]) => SetMetadata('require-permission', permissions);
然后我们在 BbbController 上声明需要的权限。
在 PermissionGuard 里取出来判断:
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('require-permission', [
context.getClass(),
context.getHandler()
])
可以看到打印了用户有的 permission 还有这个接口需要的 permission。
然后把权限列表和所需权限一对比,就达到了控制的效果
for (let i = 0; i < requiredPermissions.length; i++) {
const curPermission = requiredPermissions[i];
const found = permissions.find(item => item.name === curPermission);
if (!found) {
throw new UnauthorizedException('您没有访问该接口的权限');
}
}
通过最开始的图李四是没有权限访问aaa接口的 我们测试一下试试
在aaa的get请求上加上
然后再apipost运行一下看看有没有这个接口的权限
发现是没有权限的。说明我们rbac权限控制成功了。
仓库:简单的rbac实现