上一篇介绍了如何使用 DTO 和管道对入参进行验证,接下来介绍一下如何用拦截器,实现后台管理系统中最复杂、也最使人头疼的 RBAC。html
GitHub 项目地址,欢迎各位大佬 Star。node
RBAC:基于角色的权限访问控制(Role-Based Access Control),是商业系统中最多见的权限管理技术之一。在 RBAC 中,权限与角色相关联,用户经过成为适当角色的成员而获得这些角色的权限。这就极大地简化了权限的管理。git
RBAC 模型能够分为:RBAC 0、RBAC 一、RBAC 二、RBAC 3 四种。github
其中 RBAC 0 是基础,也是最简单的,至关于底层逻辑。RBAC 一、RBAC 二、RBAC 3 都是以 RBAC 0 为基础的升级。typescript
最简单的用户、角色、权限模型。这里面又包含了2种:数据库
通常状况下,使用 RBAC 0 模型就能够知足常规的权限管理系统设计了。缓存
相对于RBAC0模型,增长了子角色,引入了继承概念,即子角色能够继承父角色的全部权限。bash
基于RBAC0模型,增长了对角色的一些限制:角色互斥、基数约束、先决条件角色等。async
称为统一模型,它包含了 RBAC 1 和 RBAC 2,利用传递性,也把 RBAC 0 包括在内,综合了 RBAC 0、RBAC 1 和 RBAC 2 的全部特色,这里就不在多描述了。测试
因为是入门教程,这里只演示 RBAC 0 模型的实现,即一个用户只能有一种角色,不存在交叉关系。
正所谓:道生一,一辈子二,二生三,三生万物。学会 RBAC 0 以后,相信读者们必定能结合概念,继续扩展权限系统的。
其实 RBAC 0 实现起来很是简单,简单到核心代码都不超过 15 行。
还记得第三篇签发 Token 的时候,有个 role 字段么?那个就是用户角色,下面咱们针对 Token 的 role 字段进行展开。先新建文件:
$ nest g interceptor rbac interceptor
复制代码
// src/interceptor/rbac.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, ForbiddenException } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RbacInterceptor implements NestInterceptor {
// role[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)
constructor(private readonly role: number) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.getArgByIndex(1).req;
if (req.user.role > this.role) {
throw new ForbiddenException('对不起,您无权操做');
}
return next.handle();
}
}
复制代码
上面就是验证的核心代码,抛开注释,总共才15行,
构造器里的 role: number
是经过路由传入的可配置参数,表示必须小于等于这个数字的角色才能访问。经过获取用户角色的数字,和传入的角色数字进行比较便可。
和第二篇同样,直接复制下列 SQL语句 到 navicat 查询模块,运行,建立新表:
CREATE TABLE `commodity` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`ccolumn_id` smallint(6) NOT NULL COMMENT '商品_栏目ID',
`commodity_name` varchar(10) NOT NULL COMMENT '商品_名称',
`commodity_desc` varchar(20) NOT NULL COMMENT '商品_介绍',
`market_price` decimal(7,2) NOT NULL DEFAULT '0.00' COMMENT '市场价',
`sale_money` decimal(7,2) NOT NULL DEFAULT '0.00' COMMENT '销售价',
`c_by` varchar(24) NOT NULL COMMENT '建立人',
`c_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间',
`u_by` varchar(24) NOT NULL DEFAULT '0' COMMENT '修改人',
`u_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `idx_ccid` (`ccolumn_id`),
KEY `idx_cn` (`commodity_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
复制代码
建立 commodity 模块,以前的教程已经教过,这里再也不赘述,直接切入正题,先编写 Service:
// src/logical/commodity/commodity.service.js
import { Injectable } from '@nestjs/common';
import * as Sequelize from 'sequelize'; // 引入 Sequelize 库
import sequelize from '../../database/sequelize'; // 引入 Sequelize 实例
@Injectable()
export class CommodityService {
/** * 查询商品列表 * @param {*} body * @param {string} username * @returns {Promise<any>} * @memberof CommodityService */
async queryCommodityList(body: any): Promise<any> {
const { pageIndex = 1, pageSize = 10, keywords = '' } = body;
// 分页查询条件
const currentIndex = (pageIndex - 1) * pageSize < 0 ? 0 : (pageIndex - 1) * pageSize;
const queryCommodityListSQL = ` SELECT id, ccolumn_id columnId, commodity_name name, commodity_desc description, sale_money saleMoney, market_price marketPrice, c_by createBy, DATE_FORMAT(c_time, '%Y-%m-%d %H:%i:%s') createTime, u_by updateBy, DATE_FORMAT(u_time, '%Y-%m-%d %H:%i:%s') updateTime FROM commodity WHERE commodity_name LIKE '%${keywords}%' ORDER BY id DESC LIMIT ${currentIndex}, ${pageSize} `;
const commodityList: any[] = await sequelize.query(queryCommodityListSQL, {
type: Sequelize.QueryTypes.SELECT,
raw: true,
logging: false,
});
// 统计数据条数
const countCommodityListSQL = ` SELECT COUNT(*) AS total FROM commodity WHERE commodity_name LIKE '%${keywords}%' `;
const count: any = (
await sequelize.query(countCommodityListSQL, {
type: Sequelize.QueryTypes.SELECT,
raw: true,
logging: false,
})
)[0];
return {
code: 200,
data: {
commodityList,
total: count.total,
},
};
}
/** * 建立商品 * * @param {*} body * @param {string} username * @returns {Promise<any>} * @memberof CommodityService */
async createCommodity(body: any, username: string): Promise<any> {
const { columnId = 0, name, description = '', marketPrice = 0, saleMoney = 0 } = body;
const createCommoditySQL = ` INSERT INTO commodity (ccolumn_id, commodity_name, commodity_desc, market_price, sale_money, c_by) VALUES ('${columnId}', '${name}', '${description}', ${marketPrice}, ${saleMoney}, '${username}'); `;
await sequelize.query(createCommoditySQL, { logging: false });
return {
code: 200,
msg: 'Success',
};
}
/** * 修改商品 * * @param {*} body * @param {string} username * @returns * @memberof CommodityService */
async updateCommodity(body: any, username: string) {
const { id, columnId, name, description, saleMoney, marketPrice } = body;
const updateCommoditySQL = ` UPDATE commodity SET ccolumn_id = ${columnId}, commodity_name = '${name}', commodity_desc = '${description}', market_price = ${marketPrice}, sale_money = ${saleMoney}, u_by = '${username}' WHERE id = ${id} `;
const transaction = await sequelize.transaction();
await sequelize.query(updateCommoditySQL, { transaction, logging: false });
return {
code: 200,
msg: 'Success',
};
}
/** * 删除商品 * * @param {*} body * @returns * @memberof CommodityService */
async deleteCommodity(body: any) {
const { id } = body;
const deleteCommoditySQL = ` DELETE FROM commodity WHERE id = ${id} `;
await sequelize.query(deleteCommoditySQL, { logging: false });
return {
code: 200,
msg: 'Success',
};
}
}
复制代码
上面的代码就包含了增、删、改、查,基本就涵盖了平时 80% 的搬砖内容。为了快速验证效果,这里就没有使用 DTO 进行参数验证,平时你们仍是要加上比较好。
接下来编写 Controller,并引入 RBAC 拦截器:
// src/logical/commodity/commodity.controller.js
import { Controller, Request, Post, Body, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CommodityService } from './commodity.service';
import { RbacInterceptor } from '../../interceptor/rbac.interceptor';
@Controller('commodity')
export class CommodityController {
constructor(private readonly commodityService: CommodityService) {}
// 查询商品列表
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(3)) // 调用 RBAC 拦截器
@Post('list')
async queryColumnList(@Body() body: any) {
return await this.commodityService.queryCommodityList(body);
}
// 新建商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(2))
@Post('create')
async createCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.createCommodity(body, req.user.username);
}
// 修改商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(2))
@Post('update')
async updateCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.updateCommodity(body, req.user.username);
}
// 删除商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(1))
@Post('delete')
async deleteCommodity(@Body() body: any) {
return await this.commodityService.deleteCommodity(body);
}
}
复制代码
和平时的路由没什么区别,就是使用了 @UseInterceptors(new RbacInterceptor())
,并把数字传入,这样就能够判断权限了。
这是以前注册的用户表,在没有修改权限的状况下,角色 role
都是 3
:
先往商品表插入一些数据:
我将使用 nodejs
用户登陆,并请求查询接口:
上图的查询结果,也符合预期,共有 2 条商品名称含有关键字 德玛
。
接下来,咱们新建商品(英雄):
上图能够看到,由于权限不足,因此被拦截了。
咱们直接去数据库修改角色 role 字段,将 3(普通用户)
改成 2(开发&测试&运营)
:
而后,从新登陆,从新登陆,从新登陆,重要的事情说 3 遍,再请求:
返回成功信息,再看看数据库:
如图,建立商品功能测试成功。
可是,“麦林炮手”的价格应该是 1350,咱们修改一下价格:
再看看数据库,经过 u_by
字段能够知道是经过接口修改的:
如今问题来了,由于麦林炮手的介绍不太“和谐”,因此须要删除,因而咱们请求一下删除接口:
返回“无权操做”,只好提高角色,或者联系管理员帮忙删除啦,剩下的事情和以前的同样,再也不赘述。
你们可能发现,由于传入的是数字,因此在 Controller 里写的也都是数字,若是是一我的维护的还好,可是多人协同时,就显得不够友好了。
因而,咱们应该建立常量,将角色和数字对应上,这样再看 Controller 的时候,哪些接口有哪些角色能够访问就一目了然了。
咱们修改 auth 目录下的 constants.ts
// src/logical/auth/constants.ts
export const jwtConstants = {
secret: 'shinobi7414',
};
export const roleConstans = {
SUPER_ADMIN: 0, // 超级管理员
ADMIN: 1, // 管理员
DEVELOPER: 2, // 开发者(测试、运营具备同一权限,若提高为 RBAC 1 以上,则可酌情分开)
HUMAN: 3 // 普通用户
};
复制代码
而后修改 Controller,用常量替换数字:
// src/logical/commodity/commodity.controller.js
import { Controller, Request, Post, Body, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CommodityService } from './commodity.service';
import { RbacInterceptor } from '../../interceptor/rbac.interceptor';
import { roleConstans as role } from '../auth/constants'; // 引入角色常量
@Controller('commodity')
export class CommodityController {
constructor(private readonly commodityService: CommodityService) {}
// 查询商品列表
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(role.HUMAN))
@Post('list')
async queryColumnList(@Body() body: any) {
return await this.commodityService.queryCommodityList(body);
}
// 新建商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(role.DEVELOPER))
@Post('create')
async createCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.createCommodity(body, req.user.username);
}
// 修改商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(role.DEVELOPER))
@Post('update')
async updateCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.updateCommodity(body, req.user.username);
}
// 删除商品
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(new RbacInterceptor(role.ADMIN))
@Post('delete')
async deleteCommodity(@Body() body: any) {
return await this.commodityService.deleteCommodity(body);
}
}
复制代码
如此一来,什么角色才有权限操做就一目了然。
评论区有大神指出,应该使用 Guard 来管理角色相关,所以,在这里补充一下 Guard 的实现。
新建 Guard 文件:
$ nest g guard rbac guards
复制代码
编写守卫逻辑:
// src/guards/rbac.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RbacGuard implements CanActivate {
// role[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)
constructor(private readonly role: number) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (user.role > this.role) {
throw new ForbiddenException('对不起,您无权操做');
}
return true;
}
}
复制代码
去掉注释和 TSLint 的换行,一样不超过 15 行,接下来,在 Controller 里引入:
// src/logical/commodity/commodity.controller.ts
import { Controller, Request, Post, Body, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { CommodityService } from './commodity.service';
import { RbacInterceptor } from '../../interceptor/rbac.interceptor';
import { RbacGuard } from '../../guards/rbac.guard';
import { roleConstans as role } from '../auth/constants';
@Controller('commodity')
export class CommodityController {
constructor(private readonly commodityService: CommodityService) {}
// 查询商品列表
@UseGuards(new RbacGuard(role.HUMAN))
@UseGuards(AuthGuard('jwt'))
// @UseInterceptors(new RbacInterceptor(role.HUMAN))
@Post('list')
async queryColumnList(@Body() body: any) {
return await this.commodityService.queryCommodityList(body);
}
// 新建商品
@UseGuards(new RbacGuard(role.DEVELOPER))
@UseGuards(AuthGuard('jwt'))
// @UseInterceptors(new RbacInterceptor(role.DEVELOPER))
@Post('create')
async createCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.createCommodity(body, req.user.username);
}
// 修改商品
@UseGuards(new RbacGuard(role.DEVELOPER))
@UseGuards(AuthGuard('jwt'))
// @UseInterceptors(new RbacInterceptor(role.DEVELOPER))
@Post('update')
async updateCommodity(@Body() body: any, @Request() req: any) {
return await this.commodityService.updateCommodity(body, req.user.username);
}
// 删除商品
@UseGuards(new RbacGuard(role.ADMIN))
@UseGuards(AuthGuard('jwt'))
// @UseInterceptors(new RbacInterceptor(role.ADMIN))
@Post('delete')
async deleteCommodity(@Body() body: any) {
return await this.commodityService.deleteCommodity(body);
}
}
复制代码
注意:RbacGuard 要在 AuthGuard 的上面,否则获取不到用户信息。
请求一下只有管理员才有权限的删除操做:
涛声依旧。
本篇介绍了 RBAC 的概念,以及如何使用拦截器和守卫实现 RBAC 0,原理简单到 15 行代码就搞定了。
然而这种设计,要求路由必须是一一对应的,遇到复杂的用户关系,还须要再建 3 张表,一张是 权限
表,一张是 用户-权限
对应表,还有一张是 路由-权限
对应表,这样基本能覆盖 RBAC 2 以上的需求了。
但万变不离其宗,基本就是在拦截器或守卫里作文章,用户登陆后,将权限列表缓存起来(能够是 Redis),这样就不用每次都查表去判断有没有权限访问路由了。
下一篇,暂时还不知道要介绍什么,清明节前事有点多,多是使用 Swagger 自动生成接口文档吧。
`