- 创建项目
- Nest 控制器
- nest 配置路由请求数据
- Nest 服务
- Nest 模块
- 配置静态资源
- 配置模板引擎
- Cookie 的使用
- Session 的使用
- 跨域,前缀路径、网站安全、请求限速
- 管道、守卫、拦截器、过滤器、中间件
- 一例看懂中间件、守卫、管道、异常过滤器、拦截器
- 数据验证
- 配置抽离
- 环境配置
- 文件上传与下载
- 实现图片随机验证码
- 邮件服务
- nest 基于 possport + jwt 做登陆验证
- 对数据库的密码加密:md5 和 bcryptjs
- 角色权限
- 定时任务
- 接入 Swagger 接口文档
- nest 连接 Mongodb
- typeORM 操作 Mysql 数据库
- nest 统一处理数据库操作的查询结果
- 数据库实体设计与操作
- typeorm 增删改查操作
- typeorm 使用事务的 3 种方式
- typeorm 一对一关系设计与增删改查
- typeorm 一对多和多对一关系设计与增删改查
- typeorm 多对多关系设计与增删改查
- nest 连接 Redis
- 集成 redis 实现单点登录
- Q:nestJS 注入其他依赖时为什么还需要导入其 module
角色权限
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, rolename from user, department, role, department_role as dr where user.department_id = department.id and department.id = dr.departmentId and role.id = dr.roleId and 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 来处理
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论