这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
本文是项目技术专栏的第一篇,该专栏主要沉淀项目中较为突出/重要/必要的技术经验,并结合于实践,整理出每个技术的详细设计和实现过程。
本篇叙述项目中的权限管理模型 RBAC,每篇文章都会进行后续更新迭代,知道完善完美,感谢关注。
RBAC 中最重要的概念是:用户、角色、权限
- 用户一般具有多个角色
- 角色一般具有多个权限
- 用户->角色->权限就构成了 RBAC 模型
- 用户和角色、角色和权限之间一般是多对多关系
- 易于拓展和维护、便于统一管理
RBAC - Role Based Access Control
数据库设计
公共
- 创建时间
- Date
- createTime
- default(Date.now)
- 更新时间
- updateTime
- Date
- default(Date.now)
用户
- 用户名
- String
- required
- unique
- 密码
- String
- required
- 邮箱
- String
- unique
- 是否是超级管理员
- isAdmin
- Boolean
- default(false)
- 用户状态:启用、停用
- status
- Boolean
- default(true)
- 姓名
- name
- String
- default('')
- 角色
- roles
- 多对多关联角色,便于查出用户多个角色
角色(待更新)
- 角色名称
- 角色 Code
- 角色描述
- 角色关联的权限
- auths
- 多对多关联权限,便于查出角色多个权限
权限(待更新)
- 权限名称
- 权限 Code
- 权限描述
- 上级权限 ID
- 权限类型:菜单、操作
- 权限 Icon
- 权限备注
- 权限状态:启用、停用
后端提供接口
公共
- 所有接口返回 code 成功失败,失败返回错误信息
- code 封为公共 code 所有接口复用
- 所有接口需要获取到当前登录用户,并校验用户是否存在接口的触发权限
- 若当前未登录或用户没有相应接口触发权限,则报错:无权限
用户
- 用户登录
- 传入用户名和密码
- 根据用户名查找用户
- 若用户不存在,则报错:用户不存在
- 若用户存在,则将传入密码加密后和数据库中的加密密码比对
- 若用户密码错误,则报错:用户密码不正确
- 若用户密码正确,则通过 JWT 生成用户的 token,报成功:用户登录成功,返回用户信息(除密码)
- 用户列表
- 传入分页数据
- 查询所有用户,根据分页数据进行分页处理
- 返回用户列表
- 创建用户
- 传入用户信息
- 查找是否存在重复用户,若用户 email/username 重复,则报错:用户已存在
- 若用户不重复,则创建用户
- 更新用户
- 传入部分用户信息和用户 ID
- 根据 ID 查找到用户
- 若用户存在,则将传入的部分用户信息和查出的用户信息进行合并,然后更新用户,成功则返回更新后的用户信息,发生错误则报错:更新用户失败
- 若用户不存在,则报错:用户不存在
- 删除用户
- 传入用户 ID
- 根据 ID 查找到用户
- 查找到用户则删除用户
- 未查找到用户则返回失败:未找到用户
- 查找单个用户(不含角色和权限)
- 传入用户 ID
- 根据 ID 查找到用户信息
- 若存在用户,则返回用户信息(除密码)
- 若用户不存在,则报错:用户不存在
- 更新用户角色
- 传入用户 ID 和角色 ID 数组
- 根据 ID 查找到用户
- 若用户存在 ,则将用户已有角色数组和传入的角色数组进行合并,然后更新用户
- 若用户不存在,则报错:用户不存在
- 查找用户权限
- 传入用户 ID
- 根据 ID 查找到用户
- 若用户存在 ,则将用户关联的角色所关联的所有权限进行查询并去重,返回权限数组
- 若用户不存在,则报错:用户不存在
角色
- 获取角色列表
- 传入分页数据
- 查询所有角色,根据分页数据进行分页处理
- 返回角色列表
- 创建角色
- 传入角色信息
- 查找是否存在重复角色,若角色 Code 重复,则报错:角色已存在
- 若角色不重复,则创建角色
- 更新角色
- 传入部分角色信息和角色 ID/Code
- 根据 ID/Code 查找到角色
- 若角色不存在,则报错:角色不存在
- 若角色存在,则将传入的部分角色信息和查出的角色信息进行合并,然后更新角色,
- 角色 Code 不可修改,传入了关联的角色权限 ID 数组,则还需确认传入的权限 ID 存在才可以更新到角色权限
- 成功则返回更新后的用户信息,发生错误则报错:更新用户失败
- 删除角色
- 传入角色 ID/Code
- 根据 ID/Code 查找到角色
- 若角色不存在,则报错:角色不存在
- 查找到角色,且角色没有关联用户绑定,则删除角色
- 若角色存在用户关联关系,则报错:存在用户关联,不可删除
- 查找单个角色
- 传入角色 ID/Code
- 根据 ID/Code 查找到角色信息
- 若角色不存在,则报错:角色不存在
- 若存在角色,则返回角色信息,还需返回关联的权限数组
- 修改角色拥有的权限
- 传入角色 ID 和权限列表
- 根据 ID 查找到角色信息
- 若角色不存在,则报错:角色不存在
- 若存在角色,则将传入的权限列表覆盖掉该角色的权限列表
- 查找单个角色关联的用户列表
- 传入角色 ID/Code 和分页数据
- 根据 ID/Code 查找到角色信息
- 若存在角色,则根据分页数据返回关联的用户列表
- 若角色不存在,则报错:角色不存在
权限(待更新)
- 获取权限列表
- 创建权限
- 更新权限
- 删除权限
- 获取单个权限信息
- 获取权限树列表
前端验权(React)(待更新)
home.js
<Auth requires={['view', 'submit']}>
<div className={styles.form}>
<h3>这是一个表单</h3>
<div className={styles.formContent}>
<Input />
<Auth requires={['submit']}>
<Button className={[styles.rightButton, styles.submitButton]}>提交</Button>
</Auth>
</div>
</div>
</Auth>
Auth.js
import fp from 'lodash/fp';
import { useEffect, useState } from 'react'
import axios from './axios';
/**
* @description 权限控制组件
* @param {Object} param
* @param {Element} param.children slot
* @param {Array} param.requires 需要校验的权限数组
* @returns {Element}
*/
const Auth = ({ children, requires = [] }) => {
// 元素是否有权限可以显示, 起始默认无权限
const [hasAuth, setHasAuth] = useState(false)
// 校验权限的方法
const fetchAuth = () => {
// 这里先 axios 这么写,后续会更新为页面加载时获取当前用户权限列表进行全局挂载,不用每个权限都要发送请求
axios.post('/fetchAuth', { requires }).then(res => {
const { code, data } = res.data
// 响应校验,code 不对 / data 不是布尔值就抛出
if (code !== 0) throw 'code no = 0'
if (!fp.isBoolean(data)) throw 'no boolean'
// data 是 true 才展示元素
if (data) setHasAuth(true)
})
}
// 元素首次渲染触发校验权限,权限不通过就不展示 slot 传入的元素
useEffect(fetchAuth, [])
return hasAuth ? children : null
}
export default Auth
前端管理页面(待更新)
用户列表
角色列表
权限列表
先埋坑,后续填