随着前后端分离架构的流行,在 web 应用中,RESTful API 几乎已经成为了开发者主要选择,它使得客户端和服务端不需要保存对方的详细信息,也就是无状态性,但是这样在项目中需要动态菜单和动态权限就困难起来,本场Chat就是为大家提供一种思路来解决实际项目中如何实现动态菜单和权限。
因为 RESTful API 通常是无状态性,服务器怎么样才能知道用户已经登录呢?这个时候常用的做法就是每个请求都会携带一个 access token 来在服务端认证用户。最常用的就是 JWT 了,有感兴趣的小伙伴可以再做深入学习。通俗来讲,使用了 JWT 用户在每次请求时都会在请求头中携带一个 Token ,服务端会在执行操作之前先解析这个 Token 进行认证,认证完成之后服务端就会知道过来请求的用户详情,从而做出需要的返回。
用户与用户组的架构设计
通常在一个 web 应用设计中,首先都是从用户、用户组开始的。用户就是 web 应用的核心,JWT 认证也是因为用户才存在的。用户在使用 MySQL 的 web 应用中就是一张用户表,每一个用户就是用户表中的一条数据。用户组就相当于是用户的权限了,例如 一般的系统中都会有超级管理员、管理员、普通用户等用户,用户就是这些用户组下的集合,那么用户组和用户就是一对多的关系,或者说每个用户都有一个外键指向某个用户组 ID。当某个用户是管理员时就表示他拥有管理员用户组下的所有权限。
用户-用户组表设计图如下
在这样用户组-用户的架构设计下,如何设计权限和菜单呢?首先,菜单就类似是用户操作的一些功能的集合,集合内的每个元素就相当于是权限了。例如有个菜单名为 员工管理 ,在它下面就存在四个基本权限:查看员工、新增员工、编辑员工、删除员工。也就是说把这四个权限想象成四个方法或功能,这些功能是关联某个用户组还是某个用户呢?显然是关联某个用户组是比较好的选择。因为这样用户组可以携带着很多菜单以及菜单的权限,当多个用户属于这个用户组时,这些用户就拥有了该用户组下的所有权限。否则关联某个用户的话,每个用户在新增的时候都需要设置菜单和权限的话,不仅浪费时间,还浪费资源 占有数据库空间。
用 RESTful API 的做法就是:用户组的菜单和权限会作为记录存放在权限表中,当需要的时候服务端会将这些数据转成 Json 返回给客户端使用,当然在服务端需要使用权限的接口也会使用权限表中的数据来判断,请求接口的用户是否有权限操作,根据权限表数据做不同处理。
权限-用户组-用户设计图如下
动态菜单和权限的设计思路与实现
那么动态菜单和权限在服务端和客户端究竟怎样完成这些交互的呢?
用户登录系统时输入用户名、密码等进行登录,登录成功之后会将该用户的 Token 返回给用户。用户携带着这个 Token 请求服务端的一个获取用户详细信息接口,服务端在认证通过后就将该用户的用户信息、用户组信息以及用户组的权限信息全部返回,此时客户端用户就得到了权限表的 Json 数据,根据这些数据客户端会展示不同的菜单项。客户端在收到权限 Json 数据时,根据权限中的菜单项设置,动态的设置前端菜单。做到不同的用户显示不同的菜单项。当用户在请求其他接口时,服务端会先进行用户认证,在得到当前请求用户的同时 也会得到该用户的用户组,从而得到该用户的所有权限信息。然后服务端会根据接口对应的权限详情做不同的处理,最终再返回给用户相应信息。例如 当查到该用户对当前接口只有查看权限时,如果用户发起的是 POST 请求想新增数据,那会服务端会直接返回 没有执行该操作的权限。
流程如下图
Vue 端如何实现动态路由
以基于 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 = truefor (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.lengthvar check_len = 0for (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.datasetToken(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.dataconsole.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 端如何实现动态权限
在 Django 中利用 Permission 结合权限表,在每个接口在被调用之前先根据权限判断,调用接口的用户是否有权限操作,如果有就继续完成后面的操作,否则直返返回 无权操作 的提示语。
Django 端代码示例如下
from rest_framework import permissionsfrom rest_framework.permissions import DjangoModelPermissions, IsAdminUserfrom rest_framework.permissions import BasePermission as BPermissionfrom shiyouAuth.models import GroupMenu# 最终动态权限类class BasePermission(object):def base_permission_check(self, basename):# 权限白名单if basename in ['userinfo', 'permissions', 'login', 'confdict']:return Trueelse:return Falsedef has_permission(self, request, view):print('请求的path:', request.path.split('/')[1])basename = request.path.split('/')[1]if self.base_permission_check(basename):return Trueadmin_menu = GroupMenu.objects.filter(object_name=basename).first()if admin_menu is None and request.user.group.id != 1:return Falseif request.user.group.id == 1:return Trueif 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 Falsedef has_object_permission(self, request, view, obj):basename = request.path.split('/')[1]if self.base_permission_check(basename):return Trueadmin_menu = GroupMenu.objects.filter(object_name=basename).first()if admin_menu is None and request.user.group.id != 1:return Falseif request.user.group.id == 1:return Trueif 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