前端的权限控制
一、权限控制是啥?
权限控制本质上来说就是对的人做对的事,你是boss你就干boss的事儿,你是业务员就干业务员的事,不能说业务员去做一些boss才有权限做的事,这种对人行为的控制就是权限控制,在码农层面来说就是对系统中角色的行为控制。
二、权限控制分类
权限控制分为后端权限控制和前端权限控制。
后端权限控制:咱了解的也不是太多,一般是规定系统中的角色对应的功能板块,然后再把角色分给用户。 通过对数据增删改查来实现对返回的数据的控制,进而达到后端的权限控制
前端权限控制:可以优化界面逻辑,简化项目复杂度,提升项目的运行效率,减轻服务器的压力等等,故,权限控制在前端中也比较重要和基础了
三、权限控制的实现思路:
vue中,比较常见的需要进行权限管控的权限控制实现思路有四条:
菜单的控制,界面的控制,按钮的控制,请求和响应的控制。
1.菜单控制
菜单控制指的是同一套系统,不同的用户登录所能看到的菜单选项是不同的,举个栗子,同一个教学系统,学生和教师的菜单除去一部分共有的之外,还有一部分他们特定的,例如教师就会有班级管理菜单项,而学生就没有,或者学生有作业管理项,老师就没有,这就是菜单控制权限。
2.界面控制
界面控制有两种,第一种就是用户未登录前,在地址栏中输入项目中非登录页的项目地址,这时应该将其访问拦截并转到登录页。第二种就是针对不同的用户,有些特定用户所拥有的特定页面就不应该呈现给用户,即使他非法敲入看到的地址也不行。
3.按钮控制
不同的用户对按钮的操作权限不同,例如有的用户只能查看数据,不能更改数据,有的用户则拥有对数据增删改查的功能
4.请求和响应的控制
对于超出用户权限以外的请求和响应对系统来说都是不必要的,会造成不必要的服务器开销和时间成本,这种请求和响应都是需要控制的,让其根本无法发送
四、各实现思路的vue实现代码
1.菜单控制:
在vue中的逻辑为:用户输入账号和密码登陆系统,账号和密码经过后台核验后,后台返回一项数据,其中包含了该对象所拥有的菜单权限对象以及一个登录凭证token令牌,根据拿到的菜单权限去动态渲染菜单列表
在login.vue的逻辑:其中rightlist就是用户所拥有的菜单权限表
1 methods:{ 2 login(formName){ 3 // 1.验证账号密码合法性 4 // 2.合法后将登录按钮设置为不可用 5 // 3.发送请求至后端获取token和right数据 6 // 4.请求成功后将token和rights存起来 7 // 5.解除按钮禁用状态 8 // 6.动态添加路由 9 // 7.路由跳转 10 this.$refs[formName].validate((valid)=>{ 11 if(valid){//1 12 this.pressFlag=true;//2 13 post('/login',this.loginForm)//3 14 .then(res=>{ 15 sessionStorage.setItem("token",res.data.token)//4 16 this.pressFlag=false;//5 17 sessionStorage.setItem('menulist',JSON.stringify(res.data.rights))//4 18 store.commit("setRights",JSON.parse(sessionStorage.getItem('menulist'))) 19 addDynamic();//6 20 this.$router.push({name:'container'});//7 21 }).catch(err=>{ 22 console.log(err) 23 }) 24 } 25 }) 26 } 27 }
后端返回的admin用户的rightlist数据长这样,因为我是在登录页面获取的数据,但是我要在登陆成功后的主页上使用,跨页面的数据,我们可以自然而然的想到vuex,把数据存放到vuex中就可以使用了
rights: [{ id: '1', authName: "订单管理", path: 'order', children: [{ id: '11', authName: "订单列表", path: '/orderlist' }, { id: '12', authName: "产品管理", path: '/product', children: [{ id: '121', authName: '产品列表', path: '/productlist' }, { id: '122', authName: '回顾', path: '/review' } ] } ], }, { id: '3', authName: "货物管理", children: [{ id: '31', authName: "商品类别", path: '/classify' }, { id: '32', authName: "商品列表", path: '/goodslist' } ] } ],
rightlist存到vuex的代码
export default new Vuex.Store({ state: { menulist: '[]' }, getters: {}, mutations: { setRights(state, data) { state.menulist = data; } }, })
主页上加载菜单的代码,rightlist被存为menulist。循环menulist创建菜单,此时就已经实现菜单数据控制了,但是会刷新丢失,因为刷新时vuex会重新加载,menulist会被赋值为空数组,故还是应将rightlist存到sessionstorage中,所以在登录页面加了第四步,并将vuex中的数据改掉:
更改后的vuex:
export default new Vuex.Store({ state: { menulist: JSON.parse(sessionStorage.getItem('menulist') || '[]') }, getters: {}, mutations: { setRights(state, data) { state.menulist = data; } }, actions: {}, modules: {} })
侧边栏组件: :router="true"和:index="item.path+''"结合可使菜单点击哪跳哪,因为后端返回的path格式不敢保证,所以一般加一个空字符串保证index绑定的是一个字符串。:default-active="this.$route.path"则是默认高亮的位置为当前路由项
1 <el-menu 2 :default-active="this.$route.path" 3 class="el-menu-vertical-demo" 4 active-text-color="#ffd04b" 5 @open="handleOpen" 6 @close="handleClose" 7 :router="true" 8 > 9 <el-submenu :index="item.path+''" v-for="item in menulist" :key="item.id"> 10 <template slot="title"> 11 <i class="el-icon-location"></i> 12 <span>{{ item.authName }}</span> 13 </template> 14 <!-- 判断二级菜单是否有子菜单,有子菜单就创建submenu,没有就直接创建menuitem --> 15 <template v-for="subItem in item.children"> 16 <el-submenu 17 :index="subItem.path+''" 18 v-if="subItem.hasOwnProperty('children')" 19 :key="subItem.id" 20 > 21 <template slot="title"> 22 <i class="el-icon-location"></i> 23 <span>{{ subItem.authName }}</span> 24 </template> 25 <el-menu-item 26 :index="thirdItem.path+''" 27 v-for="thirdItem in subItem.children" 28 :key="thirdItem.id" 29 > 30 <i class="el-icon-setting"></i> 31 <span slot="title">{{ thirdItem.authName }}</span> 32 </el-menu-item> 33 </el-submenu> 34 <el-menu-item 35 v-else 36 :index="subItem.path" 37 :key="subItem.id"> 38 <i class="el-icon-setting"></i> 39 <span slot="title">{{ subItem.authName }}</span> 40 </el-menu-item> 41 </template> 42 </el-submenu> 43 </el-menu> 44 <script> 45 import { mapState } from "vuex"; 46 export default { 47 computed: { 48 ...mapState(["menulist"]), 49 }, 50 }; 51 </script>
2.界面控制
对界面的控制代码层面的逻辑为:
2.1关于未登录不能访问:首次登录时存储token至sessionstorge中,每次进行路由跳转时就要进行一次token查验,只有有令牌的人才能进行页面跳转,没有令牌的访问一律拦到登录页面
实现是靠全局前置守卫来实现的,在校验通过发送请求之后,返回的数据中有一个token令牌,我要在数据获取之后就把其存起来,存好之后在全局前置守卫上完成上述逻辑
1 router.beforeEach((to, from, next) => { 2 if (to.path == '/login') { 3 next() 4 } else { 5 if (sessionStorage.getItem("token")) { 6 next() 7 } else { 8 next('/login') 9 } 10 } 11 })
2.2关于权限范围外的界面无法访问:从路由入手考虑,将所有的权限页面路由单独放入一个路由表中,不导入到根路由项,这样初始化的路由只有一些界面共有的部分,登录未跳转之前,将权限数据中该用户的访问表和动态路由表中的数据做对比,将所有匹配的上的路由项加入到根路由中,最后完成跳转
// 封装一个添加动态路由的方法,写在路由文件index.js中,引入到登陆页面使用 export function addDynamic() { // 获取项目初始化时的全部路由规则,因为我得知道往哪里加 const currentroute = router.options.routes; // 获取用户拥有的权限数据,假如你已经加到sessionstorage中的话,可以直接从里面取 // 作为后续更新路由规则时,动态路由中哪些该加哪些不该加的凭证 const rightlist = store.state.menulist; // 对全部待添加的动态路由表循环遍历 DynamicRoutes.forEach(item => { // 对用户所拥有的权限表遍历 rightlist.forEach(it => { // 看动态路由表中有没有用户权限表的path,有path代表用户能够访问这一地址 if (it.path == item.path) { // 找到了用户权限表中的对应的动态路由,就要将现有路由规则表更新 currentroute[1].children.push(item) } }) }) // 循环结束后,现路由规则表currentroute更新完毕,要将更新完毕的全部路由添加至项目路由中去 // addroute方法只能添加一级路由,不能接受整个路由数组表作为参数,故要循环添加 // 也即router.addRoute(currentroute)的方法是参数非法的 currentroute.forEach(item => { console.log(item); router.addRoute(item); })
// 导出后即可在别的页面上使用
动态路由刷新丢失问题:
因为页面刷新时,所有路由会被重新初始化,但是并不代表项目重启,也就是说在登陆页面写的动态添加路由只在有登录行为时才触发,而登录刷新后,页面的全部路由都为静态路由,故会造成动态路由丢失。解决方案:在路由守卫中,用户正常登陆后,判断页面是否有刷新行为,有的话,则重新调用动态添加路由的方法
1 router.beforeEach((to, from, next) => { 2 // 判断是否访问登录页,是的话放行 3 if (to.path == '/login') { 4 next() 5 // 不访问登录页看是否有token 6 } else { 7 if (sessionStorage.getItem("token")) { 8 // 用户以登录,没有动态路由项,说明页面刷新 9 if (routes[1].children.length <= 1) { 10 // 调用添加动态路由方法放行 11 addDynamic() 12 next({...to, replace: true }) 13 } else { 14 // 无刷新行为,已正常登录,放行 15 next() 16 } 17 } else { 18 // 未登录 19 next('/login') 20 } 21 } 22 })
未完待续
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现