一般后台系统都会有权限限制,权限控制需要前后端一起控制,前端的权限管理校验主要的目的是过滤不该有的请求和操作,减少服务端压力。
前端权限控制应该分为四个方面,接口权限、按钮权限,页面权限,路由权限。
接口权限
一般会进行接口返回参数校验,如果参数返回401 (根据后端定义的返回值),返回登录页重新登录。
登录完成拿到token,将token存在本地存储中,接口在请求时头部携带token(axios请求拦截器实现)。
import axios, {AxiosRequestConfig} from "axios";import store from "@/config/store";import {Logout} from "@/pages/login/logout"// 环境变量const API_PATH = process.env.REACT_APP_API_PATH;axios.defaults.validateStatus = () => true;axios.defaults.timeout = 60000;axios.defaults.baseURL = API_PATH;// 请求前axios.interceptors.request.use((config) => {// 获取全局数据const globalState = store.getState().globalState;// 设置请求头config.headers = config.headers || {};// 设置参数添加tokenconfig.headers.Authorization = globalState.userData.token || ""return config;}, (error => {}))// 请求后axios.interceptors.response.use((config) => {return config}, error => {})//eslint-disable-next-lineexport default (config: AxiosRequestConfig) => new Promise((resolve) => {axios(config).then(res => {if (res?.data?.meta?.code === 401) {return Logout()} else {resolve(res?.data || {})}}).catch(() => ({}))});export const Logout = () => {window.location.href = "/login";}
按钮权限
一个页面会有新增,删除,编辑等等按钮。不同用户应该是有不同操作权限的。
我们不妨定义权限列表或者权限码
//把所有的按钮级别的权限文本存放在这里,统一管理export const permissionsMaps = {add_shop: 'add_shop',add_account: 'add_account',del_account: 'del_account'}
react
- 我们提前需要和后端沟通好,通过给用户分配权限(权限码),通过接口的形式,将后端返回的权限码或者按钮权限列表存起来。
- 通过自定义组件和插槽
后端返回结果
import React from 'react'/*** @description:* @param {*} children 子组件内容* @param {*} permission 权限类型* @param {*} type 无权限处理类型,disabled为禁用,visible为不显示,默认不显示* @return {*}*/export default function AuthComponents ({ children, permission, type = 'visible', ...rest }) {// children为插槽的内容// rest接收外部组件传入的方法和其他属性const permissions = JSON.parse(localStorage.getItem('permissions'))// 判断显示类型if (type === 'visible') {// 复制出一个新节点,将组件库中某些给按钮添加的事件绑定到新节点上return permissions.includes(permission) ? React.cloneElement(children, rest) : null} else {// 复制出一个新节点,在新节点上添加禁用属性return React.cloneElement(children, {disabled: true,title: "对不起,您没有权限"})}}
import { permissionsMaps } from '../../configs/constants'import AuthComponents from '../../components/AuthComponents'// Popconfirm 为antd中的组件,给Button传递了事件,所以在自定义组件中需要用rest参数接收,并传递给自定义组件<Popconfirm title="确定删除吗?" onConfirm={() => del(item._id)} ><AuthComponents permission={permissionsMaps.del_account} type={'disabled'}><Button type='primary' size='small'>删除</Button></AuthComponents></Popconfirm><AuthComponents permission={permissionsMaps.add_account} type={'visible'}><Button>添加账号</Button></AuthComponents>
vue
- 我们提前和后端约定好按钮的名字,后端会返回一个按钮权限列表,通过vuex存储在全局store中。
- 然后我们根据权限列表使用v-if指令或者 绑定disabled属性达到相应权限效果。
- 当然更好的最好是自己写一个自定义权限指令,实质就是根据相应权限操作dom
比如概览页面的编辑按钮 名字先和后端定义好叫做overview-edit
// overviwe.vue overview是概览页面的路由名...<button v-auth='edit'>...//util.js 全局注册自定义指令Vue.directive('auth', {inserted: function (el, binding, vnode) {const optName = binding.argconst authName = `${routeName}-${optName}`//这里根据路由名和操作类型拼出按钮名 overview-editconst btnAuthList = store.state.auth.btnAuthListif (btnAuthList[authName]===0) {// 按钮权限为0则移除domel.parentNode.removeChild(el)} else if (btnAuthList[authName]===1) {// 按钮权限为1则禁用按钮vnode.componentInstance.disabled = true}}})// 登录的时候接受按钮权限并存在vuex里面const {btnAuthList} = login()vuex.state.btnAuthList = btnAuthList
菜单权限
获取菜单权限列表,动态递归生成菜单
这个菜单权限列表可以是后台直接返回你的,也可以是你注册路由的时候写在meta里面的菜单信息,后台返回路由权限,你根据meta信息动态算出的菜单权限。
至于菜单肯定是根据菜单权限递归生成的
react
//模拟后端返回的菜单数据const items = [{ label: '首页', key: '/nav/home' },{ label: '订单管理', key: '/nav/orderManage' },{label: '店铺管理', key: 'shop-management', icon: <ShopOutlined />,children: [{ label: '店铺列表', key: '/nav/shopList' },{ label: '添加店铺', key: '/nav/shopAdd', roles: ['超级管理员'] }]},{label: '账号管理', key: 'accounts-management', icon: <UserOutlined />,children: [{ label: '账号列表', key: '/nav/accountList' },{ label: '添加账号', key: '/nav/accountAdd', roles: ['超级管理员'] }]},{label: '商品管理', key: 'goods-management', icon: <ShoppingOutlined />,children: [{ label: '商品列表', key: '/nav/goodsList' },{ label: '新增商品', key: '/nav/goodsAdd', roles: ['超级管理员'] }]},{label: '销售统计', key: 'sales-statistics', icon: <BarChartOutlined />,children: [{ label: '订单统计', key: '/nav/orderStatistics' },{ label: '商品统计', key: '/nav/goodsStatistics' }]},{ label: '组件通信', key: '/nav/communication', roles: ['超级管理员'] },{ label: '按钮权限', key: '/nav/auth' }]
根据当前用户的权限等级筛选
export default function filterAuth (array) {const userInfo = JSON.parse(localStorage.getItem('userInfo'))if (!userInfo) {return array}const { role } = userInforeturn array.filter(item => !item.roles || item.roles.includes(role)).map(item => {// 如果有子菜单// 单独定义变量,避免修改原数组let o = {...item,}if (item.children) {o.children = filterAuth(item.children)}return o})}
vue
// 所有菜单menus: [{id: '1',name: '学生管理',icon: '',children: [{ id: '1-1', name: '学生列表', icon: '', path: 'StudentsList' },{ id: '1-2', name: '新增学生', icon: '', path: 'StudentsAdd' }]},{id: '2',name: '班级管理',icon: '',children: [{id: '2-1',name: '班级列表',icon: '',path: 'ClassesList'},{id: '2-2',name: '新增班级',icon: '',path: 'ClassesAdd'}]},{id: '3',name: '专业管理',icon: '',children: [{id: '3-1',name: '专业列表',icon: '',path: 'SubjectsList'}]},{id: '4',name: '课程管理',icon: '',children: [{id: '4-1',name: '课程列表',icon: '',path: 'CoursesList'},{id: '4-2',name: '新增课程',icon: '',path: 'CoursesAdd'}]},{id: '5',name: '教师管理',icon: '',children: [{id: '5-1',name: '教师列表',icon: '',path: 'teachersList'},{id: '5-2',name: '新增教师',icon: '',path: 'teachersAdd'}]},{id: '6',name: '其他功能',icon: '',children: [{id: '6-1',name: '计数器',icon: '',path: 'Counter'}]}]// 假设当前用户权限下可访问的菜单authMenus: ['SubjectsList', 'ClassesList', 'ClassesAdd']
// 用户身份权限菜单筛选filterMenus () {return this.menus.map(item => {const children = item.children.filter(child => {return this.authMenus.includes(child.path)})return {...item,children}}).filter(item => {return item.children.length > 0})}},
刷新后界面的菜单消失问题:
- 因为菜单数据是登录之后才获取到的,获取菜单数据之后,就存放在vuex中,vuex中的rightList的默认值就是空数组。一刷新界面,Vuex中的数据会重新初始化,所以会变成空的数组
- 因此,需要将权限数据存储在sessionstorage中,并让其和vuex中的数据保持同步
- 退出登录时,需要清除sessionStorage中的数据,并将vuex中的数据重置(调用location.reload()重新刷新页面就可以)
路由权限
上面的菜单权限虽然做到能看不见菜单,但是可以通过直接输入url的方式去没有权限的页面,这种情况需要路由权限来阻止。react
和上面菜单权限一样,也是根据当前用户的权限筛选可以访问的路由
react中没有路由守卫// 所有路由菜单const routes = [{path: "/",element: <Navigate to='/login' />},{path: "/login",element: <Login />},{path: "/nav",element: <Nav />,children: [{ path: "home", index: true, element: <Home /> },{ path: "orderManage", element: <OrderManage /> },{ path: "shopList", element: <ShopList /> },{ path: "shopAdd", element: <ShopAdd />, roles: ['超级管理员'] },{ path: "accountList", element: <AccountList /> },{ path: "accountAdd", element: <AccountAdd />, roles: ['超级管理员'] },{ path: "goodsList", element: <GoodsList /> },{ path: "goodsAdd", element: <GoodsAdd />, roles: ['超级管理员'] },{ path: "orderStatistics", element: <OrderStatistics /> },{ path: "goodsStatistics", element: <GoodsStatistics /> },{ path: "profile", element: <Profile /> },{ path: "communication", element: <Communication />, roles: ['超级管理员'] },{ path: "auth", element: <Auth /> },]},{path: "*",element: <NotFound />}]// filterAuth权限筛选函数 见上菜单权限-reactexport default function MyRoutes () {const authRoutes = filterAuth(routes);const element = useRoutes(authRoutes);return element}
vue
使用动态路由 ```jsx import HomeView from ‘../views/home/HomeView.vue’ import StudentsList from ‘../views/students/StudentsList.vue’ import StudentsAdd from “../views/students/StudentsAdd.vue”; import StudentsUpdate from “../views/students/StudentsUpdate.vue”; import ChartsView from ‘../views/charts/ChartsView.vue’ import Counter from “../views/counter/Counter.vue”; import SubjectsList from “../views/subjects/SubjectsList.vue”; import ClassesList from “../views/classes/ClassesList.vue”; import ClassesAdd from “../views/classes/ClassesAdd.vue”; import CoursesList from “../views/courses/CoursesList.vue”; import CoursesAdd from “../views/courses/CoursesAdd.vue”; import TeachersList from “../views/teachers/teachersList.vue”; import TeachersAdd from “../views/teachers/teachersAdd.vue”; import store from ‘@/store’; import router from ‘@/router’;
// 所有动态子路由 const fullRoutes = [ { path: ‘’, name: ‘default’, component: ChartsView }, { path: ‘studentsList’, name: ‘StudentsList’, component: StudentsList, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘学生列表’ } }, { path: ‘studentsAdd’, name: ‘StudentsAdd’, component: StudentsAdd, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘新增学生’ } }, { path: ‘studentsUpdate’, name: ‘StudentsUpdate’, // props: true, component: StudentsUpdate, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘修改学生’ } }, { path: ‘classesList’, name: ‘ClassesList’, component: ClassesList, meta: { title: ‘班级管理’, subTitle: ‘班级列表’ } }, { path: ‘classesAdd’, name: ‘ClassesAdd’, component: ClassesAdd, meta: { title: ‘班级管理’, subTitle: ‘新增班级’ } }, { path: ‘subjectsList’, name: ‘SubjectsList’, component: SubjectsList, meta: { title: ‘专业管理’, subTitle: ‘专业列表’ } }, { path: ‘coursesList’, name: ‘CoursesList’, component: CoursesList, meta: { notKeepAlive: true, title: ‘课程管理’, subTitle: ‘课程列表’ } }, { path: ‘coursesAdd’, name: ‘CoursesAdd’, component: CoursesAdd, meta: { title: ‘课程管理’, subTitle: ‘新增课程’ } }, { path: ‘teachersList’, name: ‘TeachersList’, component: TeachersList, meta: { title: ‘教师管理’, subTitle: ‘教师列表’ } }, { path: ‘teachersAdd’, name: ‘TeachersAdd’, component: TeachersAdd, meta: { title: ‘教师管理’, subTitle: ‘新增教师’ } }, { path: ‘counter’, name: ‘Counter’, component: Counter, meta: { title: ‘其他功能’, subTitle: ‘计数器’ } } ] export default async () => { // 首次进入时获取动态路由 if (store.state.authMenus.length === 0) { // 发送请求获取菜单并保存在仓库中 await store.dispatch(‘getAuthMenusAsync’) console.log(store.state.authMenus); const authRoutes = fullRoutes.filter(item => store.state.authMenus.includes(item.path)); console.log(authRoutes); // 添加动态路由 router.addRoute({ path: ‘/home’, component: HomeView, children: authRoutes }) router.addRoute({ path: ‘*’, component: () => import(‘@/views/errors/NotFound.vue’) }) } } ```
