nest-mysql:RBAC权限管理
文章问题导向
RBAC权限管理是什么?如何设计数据库?如何实现?
如果你都有了答案,可以忽略本文章,或去nest学习导图寻找更多答案
阅前必知
阅读此文,需要有一定的数据库知识
此文并非最佳实践,只能参考,如果大家有好的设计与代码,欢迎留言
RBAC
RBAC是基于角色的权限访问控制(Role-Based Access Control)
一种数据库设计思想,根据设计数据库设计方案,完成项目的权限管理
在RBAC中,有3个基础组成部分,分别是:用户、角色和权限,权限与角色相关联,用户通过成为适当角色而得到这些角色的权限
- 权限:具备操作某个事务的能力
- 角色:一系列权限的集合
如:一般的管理系统中:
销售人员:仅仅可以查看商品信息
运营人员:可以查看,修改商品信息
管理人员:可以查看,修改,删除,以及修改员工权限等等
管理人员只要为每个员工账号分配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面
权限类型
展示(菜单),如:显示用户列表,显示删除按钮等等…
操作(功能),如:增删改查,上传下载,发布公告,发起活动等等…
数据库设计
数据库设计:可简单,可复杂,几个人使用的系统和几千人使用的系统是不一样的
小型项目:用户表,权限表
中型项目:用户表,角色表,权限表
大型项目:用户表,用户分组表,角色表,权限表,菜单表…
没有角色的设计
只有用户表,菜单表,两者是多对多关系,有一个关联表
缺点:
新建一个用户时,在用户表中添加一条数据
新建一个用户时,在关联表中添加N条数据
每次新建一个用户需要添加1+N(关联几个)条数据
如果有100个用户,每个用户100个权限,那需要添加10000条数据
基于RBAC的设计
用户表和角色表的关系设计:
如果你希望一个用户可以有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系
如果你希望一个用户只能有一个角色,就设计一对多,多对一关系
角色表和权限表的关系设计:
一个角色可以拥有多个权限,一个权限被多个角色使用,设计多对多关系
多对多关系设计
用户表与角色表是多对多关系,角色表与菜单表是多对多关系
更加复杂的设计
实现流程
- 数据表设计
- 实现角色的增删改查
- 实现用户的增删改查,增加和修改用户的时候需要选择角色
- 实现权限的增删改查
- 实现角色与授权的关联
- 判断当前登录的用户是否有访问菜单的权限
- 根据当前登录账户的角色信息动态显示左侧菜单(前端)
代码实现
这里将实现一个用户,部门,角色,权限的例子:
用户通过成为部门的一员,则拥有部门普通角色的权限,还可以单独给用户设置角色,通过角色,获取权限。
权限模块包括,模块,菜单,操作,通过type区分类型,这里就不再拆分。
关系总览:
用户 - 部门:一对多关系,这里设计用户只能加入一个部门,如果设计可以加入多个部门,设计为多对多关系
用户 - 角色:多对多关系,可以给用户设置多个角色
角色 - 部门:多对多关系,一个部门多个角色
角色 - 权限:多对多关系,一个角色拥有多个权限,一个权限被多个角色使用
数据库实体设计
用户
import {Column,Entity,ManyToMany,ManyToOne,JoinColumn,JoinTable,PrimaryGeneratedColumn,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';@Entity({ name: 'user' })
export class UsersEntity {@PrimaryGeneratedColumn()id: number;@Column({type: 'varchar',length: 30,nullable: false,unique: true,})username: string;@Column({type: 'varchar',name: 'password',length: 100,nullable: false,select: false,comment: '密码',})password: string;@ManyToMany(() => RoleEntity, (role) => role.users)@JoinTable({ name: 'user_role' })roles: RoleEntity[];@ManyToOne(() => DepartmentEntity, (department) => department.users)@JoinColumn({ name: 'department_id' })department: DepartmentEntity;
}
角色
import {Entity,PrimaryGeneratedColumn,Column,ManyToMany,JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { DepartmentEntity } from '../../department/entities/department.entity';
import { AccessEntity } from '../../access/entities/access.entity';@Entity({ name: 'role' })
export class RoleEntity {@PrimaryGeneratedColumn()id: number;@Column({ type: 'varchar', length: 30 })rolename: string;@ManyToMany(() => UsersEntity, (user) => user.roles)users: UsersEntity[];@ManyToMany(() => DepartmentEntity, (department) => department.roles)department: DepartmentEntity[];@ManyToMany(() => AccessEntity, (access) => access.roles)@JoinTable({ name: 'role_access' })access: AccessEntity[];
}
部门
import {Entity,PrimaryGeneratedColumn,Column,ManyToMany,OneToMany,JoinTable,
} from 'typeorm';
import { UsersEntity } from '../../user/entities/user.entity';
import { RoleEntity } from '../../role/entities/role.entity';@Entity({ name: 'department' })
export class DepartmentEntity {@PrimaryGeneratedColumn()id: number;@Column({ type: 'varchar', length: 30 })departmentname: string;@OneToMany(() => UsersEntity, (user) => user.department)users: UsersEntity[];@ManyToMany(() => RoleEntity, (role) => role.department)@JoinTable({ name: 'department_role' })roles: RoleEntity[];
}
权限
import {Entity,PrimaryGeneratedColumn,Column,Tree,TreeChildren,TreeParent,ManyToMany,
} from 'typeorm';
import { RoleEntity } from '../../role/entities/role.entity';@Entity({ name: 'access' })
@Tree('closure-table')
export class AccessEntity {@PrimaryGeneratedColumn()id: number;@Column({ type: 'varchar', length: 30, comment: '模块' })module_name: string;@Column({ type: 'varchar', length: 30, nullable: true, comment: '操作' })action_name: string;@Column({ type: 'tinyint', comment: '类型:1:模块,2:菜单,3:操作' })type: number;@Column({ type: 'text', nullable: true, comment: '操作地址' })url: string;@TreeParent()parentCategory: AccessEntity;@TreeChildren()childCategorys: AccessEntity[];@ManyToMany(() => RoleEntity, (role) => role.access)roles: RoleEntity[];
}
接口实现
由于要实现很多接口,这里只说明一部分,其实都是数据库的操作,所有接口如下:
根据用户的id获取信息:id,用户名,部门名,角色,这些信息在做用户登陆时传递到token中。
这里设计的是:
创建用户时,添加部门,就会成为部门的普通角色,也可单独设置角色,但不是每个用户都有单独的角色。
async getUserinfoByUid(uid: number) {获取用户const user = await this.usersRepository.findOne({ id: uid },{ relations: ['roles'] },);if (!user) ToolsService.fail('用户ID不存在');const sql = `select user.id as user_id, user.username, user.department_id, department.departmentname, role.id as role_id, rolenamefromuser, department, role, department_role as drwhere user.department_id = department.idand department.id = dr.departmentIdand role.id = dr.roleIdand user.id = ${uid}`;const result = await this.usersRepository.query(sql);const userinfo = result[0];const userObj = {user_id: userinfo.user_id,username: userinfo.username,department_id: userinfo.department_id,departmentname: userinfo.departmentname,roles: [{ id: userinfo.role_id, rolename: userinfo.rolename }],};如果用户的角色roles有值,证明单独设置了角色,所以需要拼接起来if (user.roles.length > 0) {const _user = JSON.parse(JSON.stringify(user));userObj.roles = [...userObj.roles, ..._user.roles];}return userObj;
}接口请求结果:
{"status": 200,"message": "请求成功","data": {"user_id": 1,"username": "admin","department_id": 1,"departmentname": "销售部","roles": [{"id": 1,"rolename": "销售部员工"},{"id": 5,"rolename": "admin"}]}
}
结合possport + jwt 做用户登陆授权验证,如果不会请先学习相关内容
在验证账户密码通过后,possport 返回用户,然后根据用户id获取用户信息,存储token,用于路由守卫,还可以使用redis存储,以作他用。
async login(user: any): Promise<any> {const { id } = user;const userResult = await this.userService.getUserinfoByUid(id);const access_token = this.jwtService.sign(userResult);await this.redisService.set(`user-token-${id}`, access_token, 60 * 60 * 24);return { access_token };
}{"status": 200,"message": "请求成功","data": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZGVwYXJ0bWVudF9pZCI6MSwiZGVwYXJ0bWVudG5hbWUiOiLplIDllK7pg6giLCJyb2xlcyI6W3siaWQiOjEsInJvbGVuYW1lIjoi6ZSA5ZSu6YOo5ZGY5belIn0seyJpZCI6NSwicm9sZW5hbWUiOiJhZG1pbiJ9XSwiaWF0IjoxNjIxNjA1Nzg5LCJleHAiOjE2MjE2OTIxODl9.VIp0MdzSPM13eq1Bn8bB9Iu_SLKy4yoMU2N4uwgWDls"}
}
后端的权限访问
使用守卫,装饰器,结合token,验证访问权限
逻辑:
第一步:在controller使用自定义守卫装饰接口路径,在请求该接口路径时,全部进入守卫逻辑
第二步:使用自定义装饰器装饰特定接口,传递角色,自定义守卫会使用反射器获取该值,以判断该用户是否有权限
如下:findOne接口使用了自定义装饰器装饰接口,意思是只能admin来访问
import {Controller,Get,Body,Patch,Post,Param,Delete,UseGuards,ParseIntPipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { AuthGuard } from '../../common/guard/auth.guard';
import { Roles } from '../../common/decorator/role.decorator';@UseGuards(AuthGuard) 自定义守卫
@Controller('user')
export class UserController {constructor(private readonly userService: UserService) { }@Get()async findAll() {const [data, count] = await this.userService.findAll();return { count, data };}@Get(':id')@Roles('admin') 自定义装饰器async findOne(@Param('id', new ParseIntPipe()) id: number) {return await this.userService.findOne(id);}
}
装饰器
import { SetMetadata } from '@nestjs/common';SetMetadata作用:将获取到的值,设置到元数据中,然后守卫通过反射器才能获取到值
export const Roles = (...args: string[]) => SetMetadata('roles', args);
自定义守卫
返回true则有访问权限,返回false则直接报403
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Reflector } from '@nestjs/core'; 反射器,作用与自定义装饰器桥接
import { ToolsService } from '../../utils/tools.service';@Injectable()
export class AuthGuard implements CanActivate {constructor(private readonly reflector: Reflector,private readonly jwtService: JwtService,) { }白名单数组private whiteUrlList: string[] = [];验证该次请求是否为白名单内的路由private isWhiteUrl(urlList: string[], url: string): boolean {if (urlList.includes(url)) {return true;}return false;}canActivate(context: ExecutionContext): boolean {获取请求对象const request = context.switchToHttp().getRequest();验证是否是白名单内的路由if (this.isWhiteUrl(this.whiteUrlList, request.url)) return true;获取请求头中的token字段,解析获取存储在token的用户信息const token = context.switchToRpc().getData().headers.token;const user: any = this.jwtService.decode(token);if (!user) ToolsService.fail('token获取失败,请传递token或书写正确');使用反射器,配合装饰器使用,获取装饰器传递过来的数据const authRoles = this.reflector.get<string[]>('roles',context.getHandler(),);如果没有使用roles装饰,就获取不到值,就不鉴权,等于白名单if (!authRoles) return true;如果用户的所属角色与装饰器传递过来的值匹配则通过,否则不通过const userRoles = user.roles;for (let i = 0; i < userRoles.length; i++) {if (authRoles.includes(userRoles[i].rolename)) {return true;}}return false;}
}
简单测试
两个用户,分别对应不同的角色,分别请求user的findOne接口
用户1:销售部员工和admin
用户2:人事部员工
用户1:销售部员工和admin
{"status": 200,"message": "请求成功","data": {"user_id": 1,"username": "admin","department_id": 1,"departmentname": "销售部","roles": [{"id": 1,"rolename": "销售部员工"},{"id": 5,"rolename": "admin"}]}
}用户2:人事部员工
{"status": 200,"message": "请求成功","data": {"user_id": 2,"username": "admin2","department_id": 2,"departmentname": "人事部","roles": [{"id": 3,"rolename": "人事部员工"}]}
}不出意外的话:2号用户的请求结果
{"status": 403,"message": "Forbidden resource","error": "Forbidden","path": "/user/1","timestamp": "2021-05-21T14:44:04.954Z"
}
前端的权限访问则是通过权限表url和type来处理,这里就不再过多说明了
学习更多
nest学习导图
nest-mysql:RBAC权限管理相关推荐
- mysql 查看个人版,MySQL系列-权限管理
MySQL系列-权限管理 运维少年 运维少年 系列文章说明 MySQL系列文章包含了软件安装.具体使用.备份恢复等内容,主要用于记录个人的学习笔记,主要使用的MySQL版本为5.7.28,服务器系统版 ...
- mysql的权限管理
mysql的权限管理 1.授权的基本原则 只授予满足要求的最小权限,但要注意使用户能够授权给别的用户(with grant option) 对用户设置登录的主机限制 删除没有密码的用户 ...
- 【MySQL】MySQL之权限管理
MySQL有哪些权限? https://blog.csdn.net/zhouhao88410234/article/details/79245544 Privileges Supported by M ...
- RBAC权限管理设计思想
RBAC权限管理设计 一.概述 二.权限模型 三.RBAC模型 什么是RBAC模型 基本模型RBAC0 角色分层模型RBAC1 角色限制模型RBAC2 统一模型RBAC3 基于RBAC的延展--用户组 ...
- MySQL之权限管理
MySQL之权限管理 一.MySQL权限简介 关于mysql的权限简单的理解就是mysql允许你做你全力以内的事情,不可以越界.比如只允许你执行select操作,那么你就不能执行update操作.只允 ...
- RBAC权限管理设计
RBAC权限管理设计 一.RBAC组成 1. RBAC 2. RBAC组成 3. RBAC支持的安全原则 4. RBAC的优缺点 二.RBAC权限分配 1. RBAC的功能模块 2. RBAC权限分配 ...
- php rbac实现,php实现rbac权限管理
php实现rbac权限管理 介绍: RBAC(Role-Based Access Control)基于角色的权限管理方式. RBAC的最大特征就是将权限跟角色挂钩,用户又跟角色挂钩. 优点: ①管理维 ...
- rbac权限管理表mysql_RBAC权限管理数据库表小解
TP2.0版本就已经支持扩展RBAC权限管理,也有对应的demo,Rbac权限管理在Examples目录下面. RBAC扩展库核心文件则可以在ThinkPHP/Lib/ORG/Util下面找到,查看源 ...
- Java Web RBAC权限管理
前言 权限管理是在项目中经常要使用到的模块,有着极其重要的功能.比较出名的权限框架,分别为 Shiro 和 Spring Security,两者各有优缺,这次我们不用任何权限框架来实现 RBAC 权限 ...
最新文章
- Appro DM8127 IPNC 挂载NFS遇到的问题及解决
- dataGridView1去掉第一列
- Kotlin教程学习-数据类型
- 服务中添加mysql服务_Windows平台下在服务中添加MySQL
- 每日程序C语言15-猴子吃桃问题
- 如何在没有 System.Drawing.Common 的情况下使用 C# 获取图片格式
- 抽象类(Abstract)和接口的不同点、共同点(Interface)。
- 计算机导论布尔运算,计算机导论第2讲-符号化-计算化-自动化.pdf
- sql动态sql给变量复值_在动态SQL中使用变量
- [ora-02289] sequence does not exist
- 自动控制原理第七版胡寿松课后答案
- 推荐5款优质的黑科技软件,好不好用你来判断
- 【小技巧】苹果手机获取UDID的方法【两种UID的获取方法,非常实用】
- 努力不是为了追赶别人,只是为了超越自己
- \t\tP2P终结者原理
- 感受野的含义及计算方法
- nodejs部署的服务用localhost+端口可以访问,换成ip+端口就无法访问
- 【苹果相册推送位置推送iMessage】软件安装TestFlight计划的信息
- UOS系统中安装x11vnc远程桌面
- 在网上下的PPT模板打不开