一、前言
在实现一个管理系统(CRM、CMS、OA)的时候,权限是一个必然要考虑的问题。通俗讲就是每个用户登陆系统后允许的操作和看到的页面或数据是不一样的。
为了实现这个效果,市面上有很多方案,但每个方案都各有优缺点,今天我们来学习一个成熟的解决方案:基于角色的权限访问控制。
作为开发,你一定看过下面类似的权限配置页面,也就是我们要学习的RBAC(Role-Based Access Control)权限模型经常遇到的操作。
下面,我们将学习:
- 1、什么是RBAC权限
- 2、RBAC权限的实现原理和优缺点
- 3、RBAC权限模型数据库设计
- 4、RBAC权限API服务实现
- 5、RBAC权限在前端页面如何使用
二、什么是RBAC权限模型
RBAC(Role-Based Access Control)权限模型由最初的ACL(Access Control List )访问控制列表发展而来。
比如下面简单的权限控制,一个用户关联几个对应的权限,当前用户登录后,就可以根据当前用户的标识获取拥有的权限,这样我们就能控制某个用户的访问权限。
然而ACL权限控制模型,存在很大的弊端:
- 1、我们的系统每添加一个用户,就需要给当前用户绑定权限
- 2、当某个用户的权限变更时,就需要找到用户,修改绑定的权限
- 3、每增加一个权限时,可能需要给所有的用户添加
想象一下,系统中操作权限和用户都是不断增加的,带来的结果就是极其繁琐的操作。
二、实现原理和权限分类
1、权限三要素
- 用户:系统注册的用户,每个用户可以有一个或多个角色,比如张三可以是销售,也可以是运营
- 角色:系统的角色,可以有:超级管理员、运营、销售、HR...,每个角色可以关联若干个权限
- 权限:删除操作、查看、查看菜单、编辑、添加、页面、API等任意自定义的权限
2、实现原理
基于上图我们可以发现,RBAC权限模型就是在ACL基础上引入了角色的概念,从而大大简化了配置的复杂度。
我们通过角色将用户和权限串联起来,他们是多对多的关系,所以可以实现各种权限控制方式。
3、权限分类
- 操作权限:主要是指某个功能,比如增删改查操作。
- 数据权限:指数据范围,比如上海的销售只能看到上海的销售数据,北京的销售只能看到上海的销售数据。
4、实现方式
- 当系统完成后,我们首先定义权限(个数不限),接着再创建好角色并选择该角色可以访问的权限。
- 当用户注册后,此时只能看到或访问没有设置权限的内容,接着我们给当前用户添加角色。
- 当前用户再次访问时,通过用户关联的角色,就可以获取当前用户对应的权限信息。
以上操作的好处就是,当系统稳定后,常见的操作就是给新用户分配角色,将新增加的权限绑定在角色上。
用户配置RBAC权限后,具体的实现流程如下:
暂时无法在飞书文档外展示此内容
三、技术选型
知道了RBAC的原理,下面我们就来实现下,技术选型如下
- GO 1.19
- Mysql:启动好的mysql数据库
- React单页面应用
先附上代码实现:
- API服务实现:github.com/richLpf/prj...
- 前端代码实现:github.com/richLpf/ant...
四、表结构设计
知道了RBAC的权限模型,我们就可以着手设计数据库表的ER图了,这里我们使用5张表来实现对应的关系,分别是
- 用户表
- 角色表
- 权限表
- 用户角色关系表
- 角色权限关系表
对应的ER图如下:
五、登陆注册API实现
权限系统依赖于用户登陆,识别了用户身份才可以进行权限管理。
所以我们给RBAC服务实现一下登陆、注册能力,提供一个登陆接口、一个注册接口。
1、注册
用户注册要注意的一点是对于密码的保护,我们一般使用md5加密,也要注意不要出现将密码返回给前端。
go
import (
"crypto/md5"
"encoding/hex"
)
func (req *CreateUserApi) CreateUser(){
// 处理密码逻辑
hasher := md5.New()
hasher.Write([]byte(req.Data.Password))
Password := hex.EncodeToString(hasher.Sum(nil))
}
2、登陆
用户登陆,我们通过生成Token来管理用户信息和登陆的有效期
go
import (
"github.com/dgrijalva/jwt-go"
"time"
)
func GenerateToken(Id int, Name string) (string, error) {
// 创建一个 JWT token
token := jwt.New(jwt.SigningMethodHS256)
// 设置 token 的声明
claims := token.Claims.(jwt.MapClaims)
claims["name"] = Name
claims["exp"] = time.Now().Add(time.Hour * 24).Unix() // 设置 token 过期时间
// 生成 token 字符串
tokenString, err := token.SignedString([]byte(common.SecretKey))
if err != nil {
return "", err
}
return tokenString, nil
}
六、ACL权限API实现
1、权限管理
我们通过Key来定义权限名称,并且给权限声明类型,方便识别,对权限进行增删改查操作,每次有新的权限,就新增到权限列表
2、角色管理
我们通过角色来关联权限,增加角色的API,同时支持绑定权限。
3、用户管理
用户的增删改查,都需要可以选择一个或多个角色,这里的用户可以通过管理员添加,也可以自行注册分配角色
七、前端如何对接RBAC权限
1、缓存当前用户的权限信息
用前端维护的权限和后端保存的权限资源进行对比,如果包含的关系,就放行,否则拦截
javascript
export const AllPermission = {
Action: {
CreateUser: 'GetUser',
},
Page: {
Record: 'Record'
}
}
export function keyHasPermission(key, permissions = []) {
return permissions.findIndex(item => item.key === key) > -1;
}
2、操作权限实现
通过简单的判断,来显示隐藏操作按钮
xml
<>{keyHasPermission(AllPermission.Action.CreateUser, permission) ? <Button>操作权限-新增</Button> : null}</>
3、页面权限实现
封装组件,通过简单的判断,显示页面或者告知用户无相关权限
javascript
function Permission(props) {
const permissions = useSelector((state) => {
return state.systemStore.permission;
});
const { aclKey, ...restProps } = props;
return <div>
{
keyHasPermission(aclKey, permissions) ?
<div {...restProps} /> : <Card>
<Result
status="error"
title="403"
subTitle="无相关权限,请联系管理员开通权限!"
></Result>
</Card>
}
</div>
}
八、注意
当前权限模型,我们还可以变种,将权限包在一个一个的组里面,这样可以方便的赋予权限。
- 用户添加用户组,每个用户组的权限一致
- 给权限添加组,比如增删改查API组成了对于某个对象的操作,这样就可以将该对象的所有操作权限统一分配给某个角色