项目中的权限管理:RBAC 数据库设计和前后端交互设计

2,893 阅读6分钟

这是我参与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
    • email
    • 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

前端管理页面(待更新)

用户列表

角色列表

权限列表

先埋坑,后续填