@TOC
前言
rbac - 基于角色的权限管理,介绍了acl(访问权限列表)基于用户的权限管理,rbac基于角色的权限管理。
一、rbac 基于角色的权限管理
1.acl 基于用户的权限管理
ACL(Access Control List,访问控制列表) 基于用户的权限管理
用户表:id、name、mobile、password 1 张三 18612340000 123 2 李四 18631240001 1329
资源表:id、name、pid 1 内容菜单 null 2 用户菜单 null 3 课程管理 1 4 课程分类 1 5 用户管理 2
用户资源表:userid、resid 1 3 1 4 2 5
2.rbac 基于角色的权限管理
rbac基于角色的权限管理
用户表:id、name、mobile、password、roleid 1 张三 18612340000 123 2 李四 18631240001 1329
角色表:id、name 1 编辑人员 2 推广员 3 财务专员
资源表:id、name、pid、url 1 内容菜单 null 2 用户菜单 null 3 课程管理 1 /course/ 4 课程分类 1 /cates/ 5 用户管理 2 /user/
角色资源表:userid、resid 1 3 1 4 2 5
流程:
员工入职-->管理员--->添加角色、添加资源、角色配置资源、添加用户(选择角色)--->用户登录--->手机号验证码--->验证通过,通过后查询此用户对应的资源(页面资源、接口资源)--->把页面资源返回给前端--->把接口资源存入redis--->在中间件验证是否有接口权限
二、应用示例
1.配置角色资源
a.分析表
user/models.py:资源表、角色表、用户表、角色资源表、接口权限表
```python
from django.db import models
Create your models here.
资源表
class ResourceModel(models.Model):
name = models.CharField(max_length=10, unique=True, default="默认模块")
pid = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='son')
url = models.CharField(max_length=20, unique=True)
def str(self):
return self.name
class Meta:
verbose_name = "资源表"
verbose_name_plural = "资源表"
db_table = 'resource'
角色表
class RoleModel(models.Model):
name = models.CharField(max_length=10,unique=True ,default="角色")
resource = models.ManyToManyField(ResourceModel)
def str(self):
return self.name
class Meta:
verbose_name = "角色表"
verbose_name_plural = "角色表"
db_table = 'role'
用户表
class UserModel(models.Model):
username = models.CharField(max_length=10, default='默认用户名')
phone = models.CharField(max_length=11, unique=True)
password = models.CharField(max_length=10, default='123')
role = models.ForeignKey(RoleModel, on_delete=models.CASCADE,default=2)
def str(self):
return self.username
class Meta:
verbose_name = "用户表"
verbose_name_plural = "用户表"
db_table = 'user'
接口权限表:
class InterfacePerModel(models.Model):
resource = models.ManyToManyField(ResourceModel,related_name="interfaces")
url = models.CharField(max_length=20, unique=True)
def str(self):
return self.url
class Meta:
verbose_name = '接口权限表'
verbose_name_plural = '接口权限表'
db_table = 'interface_per'
### b.核心逻辑
```python
# 获取角色对应信息
class RoleView(APIView):
def get(self, request):
data = RoleModel.objects.all()
roleSer = RoleSerializer(data, many=True)
return Response({"message":"获取角色资源对应信息","code":200,"data":roleSer.data})
class ResourceView(APIView):
def get(self, request):
# 拿 roleid
# 拿 roleid 对应的资源,
roleid = request.GET.get('roleid')
res = ResourceModel.objects.filter(pid_id__gt=0).all()
# transf -- key -- label
allres = [{"key":i.id,"label":i.name} for i in res] #pid>0,所有资源
role = RoleModel.objects.filter(id=roleid).first()
checkres = role.resource.all() #拿这个角色对应的所有资源(选中)
checkedids = [i.id for i in checkres] #拿对应的所有资源id(选中)
return Response({"code":200,"allres":allres,"checkedids":checkedids})
def post(self, request):
resids = request.data.get("resids")
roleid = request.data.get("roleid")
role = RoleModel.objects.filter(id=roleid).first()
print(role.resource.all())
if role.resource.all():
role.resource.clear()#清除之前存在的所有资源
role.resource.add(*resids)#添加传进来的 资源 ids
return Response({"code":200})
c.使用transfer在前端实现资源配置
使用 el-transfer 构建角色资源配置页面
<template>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="角色名" width="120" />
<el-table-column fixed="right" label="Operations" width="120">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleClick">Detail</el-button>
<el-button link type="primary" size="small">Edit</el-button>
<el-button link type="primary" size="small" @click="setres(scope.row.id)">资源配制</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" title="Tips" width="80%">
<el-transfer v-model="value" :data="data" />
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="addresource">Confirm</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import http from "../../http";
import {
onMounted,ref} from 'vue'
const dialogVisible=ref(false)
const value=ref([])
const data = ref([])
const roleid = ref('')
const handleClick = () => {
console.log('click')
}
const tableData = ref([])
onMounted(()=>{
http.get('role/').then(res=>{
tableData.value = res.data.data
console.log(res.data.data);
})
})
const setres=(rid)=>{
roleid.value = rid
http.get('resource/?roleid='+rid).then(res=>{
data.value = res.data.allres
value.value = res.data.checkedids
dialogVisible.value=true
console.log(res.data.allres);
console.log(res.data.checkedids);
})
}
const addresource=()=>{
http.post('resource/',{
'roleid':roleid.value,'resids':value.value}).then(res=>{
roleid.value=''
dialogVisible.value=false
})
}
</script>
d.页面效果
2.登录时获取对应权限
a.员工登录
课程模块 分类管理 课程管理
接口权限表:资源id(manytomany)、接口地址 2 /cates/ 3 /courses/ 3 /cates/
手机号--->验证通过后获取roleid,通过roleid获取资源列表,把资源列表返回给前端
eg:[{"id":1,"name":"课程模块","sons":[{"id":2,"name":"分类管理",'url':""}]}]
查询资源对应的接口权限列表,存入redis
# 账号密码登录
class LoginView(APIView):
def post(self, request):
# 获取参数
# username = request.data.get('username', None)
# password = request.data.get('password', None)
phone = request.data.get('phone', None)
# 验证码
# ...
# 查询user表
print(phone)
user = UserModel.objects.filter(phone__exact=phone).first()
reso = user.role.resource.all()
# 构建父类资源/资源信息重组、
rlist = []
idlist = []
interlist=[] #接口权限列表
for i in reso:
# 获取此资源的所有接口
interfaces = i.interfaces.all()
for interface in interfaces:
interlist.append(interface.url)
pid = i.pid.id
if pid not in idlist:
rlist.append({
"id":pid,"name":i.pid.name,'son':[]})
idlist.append(pid)
# 遍历rlist
for index,i in enumerate(rlist):
#遍历res
for j in reso:
# 找到父类的son
if j.pid.id == i['id']:
rlist[index]['son'].append({
"id":j.id,'name':j.name,'url':j.url})
print("rlist--->")
print(rlist)
# 查询资源对应的接口权限列表,存入redis
r.set_str('user'+str(user.id)+'interface',json.dumps(interlist))
# if not user:
# return Response({"code":"2001",'message': 'User not found'})
# if password != user.password:
# return Response({"code":"2002",'message': 'Wrong password'})
token = mjwt.jwt_encode({
"userid":user.id,'exp':int(time.time())+3600})
return Response({
"code":"200",'message': 'Successfully logged in',"token":token,"rlist":rlist})
b.中间件
中间件:
token 是否存在、是否过期、是否退出
从token中解析出userid,根据userid查询接口权限列表,查询redis
request.path not in reslist:
class PermitionMiddleware(MiddlewareMixin):
def process_request(self, request):
# 1.定义白名单,在登录前需要操作的接口放到白名单中
wlist = ['/register/', '/login/', '/sendsms/']
# 获取当前的url
path = request.path
print("middleware---------------------------1")
print(path)
# 2.如果不在白名单,获取token,验证
if path not in wlist:
print("middleware----------------------------2")
try:
token = request.headers.get('Authorization')
data = mjwt.jwt_decode(token)
except:
return JsonResponse({
"code": 401, "mes": "token不存在或者被修改"})
# data没问题 ↓
exp = int(data['exp'])
now = int(time.time())
userid = data['user_id']
request.userid = userid
# 判断是否过期
if now > exp:
return JsonResponse({
"code": 401, "mes": "token已经过期不能操作"})
#是否退出,退出时存token
value = r.get_str(token)
if value:
return JsonResponse({
"code": 401, "mes": "用户已经退出,不能操作"})
# 3.↑↑↑验证是否被修改,是否过期,是否已经退出 (点击退出,把token存入redis, 加一个过期时间),任何一个问题,return 401没有权限操作,通过继续下一步操作
#验证是否有权限操作接口
list = r.get_str('user'+str(userid)+'interface')
if list:
list = json.loads(list)
if path not in list:
return JsonResponse({
"code":401,"mes":"没有操作此接口的权限"})
c.前端请求
登录请求
fnLogin() { // alert("登录") http.post("http://localhost:8000/login/",{ phone:this.user.phone,password:this.user.password}).then((result) => { console.log(result.data.rlist); if (result.data.code == '200') { alert(result.data.message) // 资源列表 localStorage.setItem('rlist',JSON.stringify(result.data.rlist)) // 用户 token localStorage.setItem('token',result.data.token) // localStorage.setItem("phone",this.user.phone) // localStorage.setItem('menulist',JSON.stringify(result.data.menulist)) // this.$router.push('/index') this.$router.push('/home') } else { alert(result.data.message) } }).catch((err) => { alert(err) }); }
d.效果图
登录成功后,跳转到home页面,展示当前用户所具有的功能模块
List2为当前用户所配置的资源模块
展示用户所具有的资源模块
2.前端-路由守卫-页面权限
1.login接口返回menulist
2.vue页面调用登录接口
3.路由守卫
// 导航守卫
router.beforeEach((to, from, next) => {
var reslist = ['/login', '/register', '/home', '/', '/chat']
if (reslist.indexOf(to.path) == -1) {
var token = localStorage.getItem('token')
if (token) {
//验证是否在权限列表中
// var menulist = localStorage.getItem('menulist');
var menulist = localStorage.getItem('rlist');
// var mlist = JSON.parse(menulist)
var mlist: any[] = menulist ? JSON.parse(menulist) : [];
console.log("milist-------");
console.log(mlist);
if (mlist.indexOf(to.path) >= 0) {
next()
} else {
alert("无权访问此页面")
next({
"name": '/' })
}
} else {
next({
"name": 'Login' })
}
}
//对于登录、注册、首页等不需要权限的页面,直接放行
next()
})
三、拓展
- rbac0 基础
- 四张表
- rbac1 角色继承
- 基础角色-->配置资源
- 角色->继承基础角色
- 角色表:id、name、pid
- rbac2 资源互斥
~~~bash
考试系统、比赛系统
1 考试
2 打分互斥表
res1 res2
1 2
~~~
- rbac3 继承+互斥
- 位运算优化权限系统
- 添加权限 | 对比权限& 删除权限^