文章问题导向

RBAC权限管理是什么?如何设计数据库?如何实现?

如果你都有了答案,可以忽略本文章,或去nest学习导图寻找更多答案


阅前必知

阅读此文,需要有一定的数据库知识
此文并非最佳实践,只能参考,如果大家有好的设计与代码,欢迎留言


RBAC

RBAC是基于角色的权限访问控制(Role-Based Access Control)
一种数据库设计思想,根据设计数据库设计方案,完成项目的权限管理
在RBAC中,有3个基础组成部分,分别是:用户、角色和权限,权限与角色相关联,用户通过成为适当角色而得到这些角色的权限

  • 权限:具备操作某个事务的能力
  • 角色:一系列权限的集合

如:一般的管理系统中:
销售人员:仅仅可以查看商品信息
运营人员:可以查看,修改商品信息
管理人员:可以查看,修改,删除,以及修改员工权限等等
管理人员只要为每个员工账号分配对应的角色,登陆操作时就只能执行对应的权限或看到对应的页面

权限类型

展示(菜单),如:显示用户列表,显示删除按钮等等…
操作(功能),如:增删改查,上传下载,发布公告,发起活动等等…


数据库设计

数据库设计:可简单,可复杂,几个人使用的系统和几千人使用的系统是不一样的
小型项目:用户表,权限表
中型项目:用户表,角色表,权限表
大型项目:用户表,用户分组表,角色表,权限表,菜单表…


没有角色的设计

只有用户表,菜单表,两者是多对多关系,有一个关联表

缺点:

新建一个用户时,在用户表中添加一条数据
新建一个用户时,在关联表中添加N条数据
每次新建一个用户需要添加1+N(关联几个)条数据
如果有100个用户,每个用户100个权限,那需要添加10000条数据


基于RBAC的设计

用户表和角色表的关系设计:

如果你希望一个用户可以有多个角色,如:一个人即是销售总监,也是人事管理,就设计多对多关系
如果你希望一个用户只能有一个角色,就设计一对多,多对一关系

角色表和权限表的关系设计:

一个角色可以拥有多个权限,一个权限被多个角色使用,设计多对多关系

多对多关系设计

用户表与角色表是多对多关系,角色表与菜单表是多对多关系


更加复杂的设计


实现流程

  1. 数据表设计
  2. 实现角色的增删改查
  3. 实现用户的增删改查,增加和修改用户的时候需要选择角色
  4. 实现权限的增删改查
  5. 实现角色与授权的关联
  6. 判断当前登录的用户是否有访问菜单的权限
  7. 根据当前登录账户的角色信息动态显示左侧菜单(前端)

代码实现

这里将实现一个用户,部门,角色,权限的例子:
用户通过成为部门的一员,则拥有部门普通角色的权限,还可以单独给用户设置角色,通过角色,获取权限。
权限模块包括,模块,菜单,操作,通过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权限管理相关推荐

  1. mysql 查看个人版,MySQL系列-权限管理

    MySQL系列-权限管理 运维少年 运维少年 系列文章说明 MySQL系列文章包含了软件安装.具体使用.备份恢复等内容,主要用于记录个人的学习笔记,主要使用的MySQL版本为5.7.28,服务器系统版 ...

  2. mysql的权限管理

    mysql的权限管理 1.授权的基本原则   只授予满足要求的最小权限,但要注意使用户能够授权给别的用户(with grant option)   对用户设置登录的主机限制   删除没有密码的用户   ...

  3. 【MySQL】MySQL之权限管理

    MySQL有哪些权限? https://blog.csdn.net/zhouhao88410234/article/details/79245544 Privileges Supported by M ...

  4. RBAC权限管理设计思想

    RBAC权限管理设计 一.概述 二.权限模型 三.RBAC模型 什么是RBAC模型 基本模型RBAC0 角色分层模型RBAC1 角色限制模型RBAC2 统一模型RBAC3 基于RBAC的延展--用户组 ...

  5. MySQL之权限管理

    MySQL之权限管理 一.MySQL权限简介 关于mysql的权限简单的理解就是mysql允许你做你全力以内的事情,不可以越界.比如只允许你执行select操作,那么你就不能执行update操作.只允 ...

  6. RBAC权限管理设计

    RBAC权限管理设计 一.RBAC组成 1. RBAC 2. RBAC组成 3. RBAC支持的安全原则 4. RBAC的优缺点 二.RBAC权限分配 1. RBAC的功能模块 2. RBAC权限分配 ...

  7. php rbac实现,php实现rbac权限管理

    php实现rbac权限管理 介绍: RBAC(Role-Based Access Control)基于角色的权限管理方式. RBAC的最大特征就是将权限跟角色挂钩,用户又跟角色挂钩. 优点: ①管理维 ...

  8. rbac权限管理表mysql_RBAC权限管理数据库表小解

    TP2.0版本就已经支持扩展RBAC权限管理,也有对应的demo,Rbac权限管理在Examples目录下面. RBAC扩展库核心文件则可以在ThinkPHP/Lib/ORG/Util下面找到,查看源 ...

  9. Java Web RBAC权限管理

    前言 权限管理是在项目中经常要使用到的模块,有着极其重要的功能.比较出名的权限框架,分别为 Shiro 和 Spring Security,两者各有优缺,这次我们不用任何权限框架来实现 RBAC 权限 ...

最新文章

  1. Appro DM8127 IPNC 挂载NFS遇到的问题及解决
  2. dataGridView1去掉第一列
  3. Kotlin教程学习-数据类型
  4. 服务中添加mysql服务_Windows平台下在服务中添加MySQL
  5. 每日程序C语言15-猴子吃桃问题
  6. 如何在没有 System.Drawing.Common 的情况下使用 C# 获取图片格式
  7. 抽象类(Abstract)和接口的不同点、共同点(Interface)。
  8. 计算机导论布尔运算,计算机导论第2讲-符号化-计算化-自动化.pdf
  9. sql动态sql给变量复值_在动态SQL中使用变量
  10. [ora-02289] sequence does not exist
  11. 自动控制原理第七版胡寿松课后答案
  12. 推荐5款优质的黑科技软件,好不好用你来判断
  13. 【小技巧】苹果手机获取UDID的方法【两种UID的获取方法,非常实用】
  14. 努力不是为了追赶别人,只是为了超越自己
  15. \t\tP2P终结者原理
  16. 感受野的含义及计算方法
  17. nodejs部署的服务用localhost+端口可以访问,换成ip+端口就无法访问
  18. 【苹果相册推送位置推送iMessage】软件安装TestFlight计划的信息
  19. UOS系统中安装x11vnc远程桌面
  20. 在网上下的PPT模板打不开

热门文章

  1. Python 全局变量、局部变量、静态变量 详解
  2. 相机溯源之传统方法(PRNU提取)
  3. Sql Server 03
  4. 词性标注集句和句法分析标注集
  5. 2017年总结(补全)
  6. java 声明和动态创建数组
  7. 2020最新整理JAVA面试题附答案
  8. 前端必备:五大css自动化生成网站(稀有级别!)
  9. 【无标题】c++ 实现 interpolate.interp1d插值
  10. SimpleDateFormat的pattern