Nest 使用 RBAC 权限控制
RBAC
RBAC(Role-Based Access Control,基于角色的访问控制)是一种访问控制方法,它将权限与角色关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。
实现思路
- 定义权限表、角色表、用户表、角色权限关系表、用户角色关系表
- 在控制器上使用装饰器定义权限,在守卫中获取权限,与用户角色关系表中的角色进行对比,如果存在则放行,否则抛出异常
数据库
- permission 权限表
sql
CREATE TABLE `permission` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
- role 角色表
sql
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
- user
sql
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`createTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
- role_permission_relation
sql
CREATE TABLE `role_permission_relation` (
`roleId` int NOT NULL,
`permissionId` int NOT NULL,
PRIMARY KEY (`roleId`,`permissionId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
- user_role_relation
sql
CREATE TABLE `user_role_relation` (
`userId` int NOT NULL,
`roleId` int NOT NULL,
PRIMARY KEY (`userId`,`roleId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
代码
控制器
- 这里我加在了控制器上,也可以加在方法上
ts
import { Controller, Get, UseGuards, SetMetadata } from "@nestjs/common";
import { AaaService } from "./aaa.service";
import { AaaGuard } from "./aaa.guard";
@Controller("aaa")
@SetMetadata("permissions", ["查询aaa"])
@UseGuards(AaaGuard)
export class AaaController {
constructor(private readonly aaaService: AaaService) {}
@Get()
findAll() {
return {
data: this.aaaService.findAll(),
};
}
}
全局守卫
他的作用就是验证 jwt,并把解析出来的信息赋值给 request,这样在下一步守卫中就可以拿到 userId 了
ts
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { GlobalCheckException } from "./globalcheck.exception";
import { ConfigService } from "@nestjs/config";
import { JwtServiceAll } from "../modules/jwt/jwt.service";
@Injectable()
export class GlobalGuard implements CanActivate {
@Inject()
private configService: ConfigService;
@Inject()
private jwtService: JwtServiceAll;
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
//权限验证
const request = context.switchToHttp().getRequest();
// 白名单
const whiteList = this.configService.get("perm").router.whiteList;
// 验证
if (whiteList.includes(request.url)) {
return true;
} else {
// 判断
const authorization = request.header("Authorization") || "";
const bearer = authorization.split(" ");
if (!bearer || bearer.length < 2) {
const result = JSON.stringify([{ message: "请输入token" }]);
throw new GlobalCheckException(result);
}
const token = bearer[1];
try {
const info = this.jwtService.verifyToken(token);
// 我这里是解析出来后把userId赋值给request 这样控制器就拿到了
(request as any).userId = info.id; // 换成你自己的payload对应字段
return true;
} catch (e) {
const result = JSON.stringify([{ message: "token 错误" }]);
throw new GlobalCheckException(result);
}
}
}
}
权限守卫
- aaa.guard.ts
ts
import {
Injectable,
CanActivate,
ExecutionContext,
Inject,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { Reflector } from "@nestjs/core";
import { PrismadbService } from "../prisma/prisma.service";
import { RedisService } from "../redis/redis.service";
@Injectable()
export class AaaGuard implements CanActivate {
@Inject()
private readonly redisService: RedisService;
@Inject()
private readonly prismadbService: PrismadbService;
@Inject(Reflector)
private readonly reflector: Reflector;
async canActivate(context: ExecutionContext): Promise<boolean> {
// 获取到设定的权限
const permissions = this.reflector.getAllAndOverride<string[]>(
"permissions",
[context.getHandler(), context.getClass()]
);
// 通过userId获取到用户角色
// 权限验证 全局守卫会给你useId
const request = context.switchToHttp().getRequest();
// 1. 先去redis中查找 2. 找不到就走数据库 3. 数据库取出来存到redis中
let userPermissionsName: any = await this.redisService.getList(
`userId_${request.userId}_roles`
);
if (!userPermissionsName || userPermissionsName.length === 0) {
console.log("进来了");
// 1. 查出这个用户拥有的角色
const RoleId = await this.prismadbService.user_role_relation.findMany({
where: {
userId: request.userId,
},
select: {
roleId: true,
},
});
// 2. 查询出这个角色拥有的权限
const userPermissionsId =
await this.prismadbService.role_permission_relation.findMany({
where: {
roleId: {
in: RoleId.map((item) => item.roleId),
},
},
select: {
permissionId: true,
},
});
console.log(userPermissionsId);
// 3. 查出这个权限Id对应的权限
let userPermissions = await this.prismadbService.permission.findMany({
where: {
id: {
in: userPermissionsId.map((item) => item.permissionId),
},
},
select: {
name: true,
},
});
userPermissionsName = userPermissions.map((item) => item.name);
console.log("最后结果");
console.log(userPermissionsName);
// 3. 存到redis中
this.redisService.setList(
`userId_${request.userId}_roles`,
userPermissionsName,
60 * 30
); // 过期时间30分钟
}
// 判断返回
const result = permissions.every((item) => {
return userPermissionsName.includes(item);
});
return result;
}
}