随着前后端分离架构的流行,在 web 应用中,RESTful API 几乎已经成为了开发者主要选择,它使得客户端和服务端不需要保存对方的详细信息,也就是无状态性,但是这样在项目中需要动态菜单和动态权限就困难起来,本场Chat就是为大家提供一种思路来解决实际项目中如何实现动态菜单和权限。
因为 RESTful API 通常是无状态性,服务器怎么样才能知道用户已经登录呢?这个时候常用的做法就是每个请求都会携带一个 access token 来在服务端认证用户。最常用的就是 JWT 了,有感兴趣的小伙伴可以再做深入学习。通俗来讲,使用了 JWT 用户在每次请求时都会在请求头中携带一个 Token ,服务端会在执行操作之前先解析这个 Token 进行认证,认证完成之后服务端就会知道过来请求的用户详情,从而做出需要的返回。
通常在一个 web 应用设计中,首先都是从用户、用户组开始的。用户就是 web 应用的核心,JWT 认证也是因为用户才存在的。用户在使用 MySQL 的 web 应用中就是一张用户表,每一个用户就是用户表中的一条数据。用户组就相当于是用户的权限了,例如 一般的系统中都会有超级管理员、管理员、普通用户等用户,用户就是这些用户组下的集合,那么用户组和用户就是一对多的关系,或者说每个用户都有一个外键指向某个用户组 ID。当某个用户是管理员时就表示他拥有管理员用户组下的所有权限。
用户-用户组表设计图如下
在这样用户组-用户的架构设计下,如何设计权限和菜单呢?首先,菜单就类似是用户操作的一些功能的集合,集合内的每个元素就相当于是权限了。例如有个菜单名为 员工管理 ,在它下面就存在四个基本权限:查看员工、新增员工、编辑员工、删除员工。也就是说把这四个权限想象成四个方法或功能,这些功能是关联某个用户组还是某个用户呢?显然是关联某个用户组是比较好的选择。因为这样用户组可以携带着很多菜单以及菜单的权限,当多个用户属于这个用户组时,这些用户就拥有了该用户组下的所有权限。否则关联某个用户的话,每个用户在新增的时候都需要设置菜单和权限的话,不仅浪费时间,还浪费资源 占有数据库空间。
用 RESTful API 的做法就是:用户组的菜单和权限会作为记录存放在权限表中,当需要的时候服务端会将这些数据转成 Json 返回给客户端使用,当然在服务端需要使用权限的接口也会使用权限表中的数据来判断,请求接口的用户是否有权限操作,根据权限表数据做不同处理。
权限-用户组-用户设计图如下
那么动态菜单和权限在服务端和客户端究竟怎样完成这些交互的呢?
流程如下图
以基于 element 的管理后台为例,在 Vue 里面利用 VueX 状态管理器,当用户登录成功后,去获取用户详细信息,获取到详细信息后根据权限 Json 数据,在 Store 中将路由重新设置,不该展示路由节点的需要隐藏或删除掉。从而做到不同的用户展示不同的菜单。
Vue 端实现代码示例
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import router from '../../router'
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
roles: [],
// 自动权限相关
group_id: 0,
user_id: 0,
menu_json: [], // 后端返回的权限Json储存在这里
router: router // 引入路由菜单
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_MENUS: (state, menus) => {
state.menu_json = menus
},
SET_GROUP: (state, group_id) => {
state.group_id = group_id
},
SET_USER: (state, user_id) => {
state.user_id = user_id
},
SET_ROUTE: (state, router) => {
// 动态路由、动态菜单
console.log('router:', router.options.routes)
var group_id = localStorage.getItem('ShopGroupId')
var menus = JSON.parse(localStorage.getItem('ShopMenus'))
console.log('group_id:', localStorage.getItem('ShopGroupId'))
console.log('menus:', JSON.parse(localStorage.getItem('ShopMenus')))
console.log('group_id为1,不执行设置router操作')
if (group_id !== '1') {
for (var i in router.options.routes) {
if (router.options.routes[i].children !== undefined) {
for (var j in router.options.routes[i].children) {
if (router.options.routes[i].children[j].path !== 'dashboard') {
router.options.routes[i].children[j].hidden = true
for (var k in menus) {
if (menus[k].object_name == router.options.routes[i].children[j].name) {
if (menus[k].menu_list) {
router.options.routes[i].children[j].hidden = false
}
}
}
}
}
}
}
}
for (var i in router.options.routes) {
if (router.options.routes[i].children !== undefined) {
var len_router = router.options.routes[i].children.length
var check_len = 0
for (var j in router.options.routes[i].children) {
if (router.options.routes[i].children[j].path !== 'dashboard') {
if (router.options.routes[i].children[j].hidden !== null && router.options.routes[i].children[j].hidden !== undefined && router.options.routes[i].children[j].hidden === true) {
check_len += 1
}
}
}
if (len_router === check_len) {
router.options.routes[i].hidden = true
}
}
}
// 动态路由、动态菜单的结束
state.router = router
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(response => {
const data = response.data
setToken(data.token)
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
alert(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(response => {
const data = response.data
console.log(data)
commit('SET_NAME', data.username)
commit('SET_ROLES', [data.group.en_name])
commit('SET_MENUS', data.group.back_menu) // 将返回的权限数据保存
commit('SET_GROUP', data.group.id)
commit('SET_USER', data.id)
localStorage.setItem('ShopMenus', JSON.stringify(data.group.back_menu))
localStorage.setItem('ShopGroupId', data.group.id)
commit('SET_ROUTE', router)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
// 前端 登出
FedLogOut({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
})
}
}
}
export default user
实现后效果如下图
在 Django 中利用 Permission 结合权限表,在每个接口在被调用之前先根据权限判断,调用接口的用户是否有权限操作,如果有就继续完成后面的操作,否则直返返回 无权操作 的提示语。
Django 端代码示例如下
from rest_framework import permissions
from rest_framework.permissions import DjangoModelPermissions, IsAdminUser
from rest_framework.permissions import BasePermission as BPermission
from shiyouAuth.models import GroupMenu
# 最终动态权限类
class BasePermission(object):
def base_permission_check(self, basename):
# 权限白名单
if basename in ['userinfo', 'permissions', 'login', 'confdict']:
return True
else:
return False
def has_permission(self, request, view):
print('请求的path:', request.path.split('/')[1])
basename = request.path.split('/')[1]
if self.base_permission_check(basename):
return True
admin_menu = GroupMenu.objects.filter(object_name=basename).first()
if admin_menu is None and request.user.group.id != 1:
return False
if request.user.group.id == 1:
return True
if view.action == 'list' or view.action == 'retrieve':
# 查看权限
return bool(request.auth and admin_menu.menu_list == True)
elif view.action == 'create':
# 创建权限
return bool(request.auth and admin_menu.menu_create == True)
elif view.action == 'update' or view.action == 'partial_update':
# 修改权限
return bool(request.auth and admin_menu.menu_update == True)
elif view.action == 'destroy':
# 删除权限
return bool(request.auth and admin_menu.menu_destroy == True)
else:
return False
def has_object_permission(self, request, view, obj):
basename = request.path.split('/')[1]
if self.base_permission_check(basename):
return True
admin_menu = GroupMenu.objects.filter(object_name=basename).first()
if admin_menu is None and request.user.group.id != 1:
return False
if request.user.group.id == 1:
return True
if view.action == 'list' or view.action == 'retrieve':
# 查看权限
return bool(request.auth and admin_menu.menu_list == True)
elif view.action == 'create':
# 创建权限
return bool(request.auth and admin_menu.menu_create == True)
elif view.action == 'update' or view.action == 'partial_update':
# 修改权限
return bool(request.auth and admin_menu.menu_update == True)
elif view.action == 'destroy':
# 删除权限
return bool(request.auth and admin_menu.menu_destroy == True)
else:
return False