文章目录
-
-
- 前端权限控制思路
-
- [1. 菜单的权限控制](#1. 菜单的权限控制)
- Vue的权限控制实现
- 小结
-
前端权限控制思路
1. 菜单的权限控制
- 菜单的控制
在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持。前端根据权限数据展示对应的菜单,点击菜单才能查看相关的界面 - 界面的控制
如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录页。如用户已经登录,可是手动敲入非权限内的地址,则需要跳转404界面 - 按钮的控制
在某个菜单的界面,还得根据权限数据,展示出可进行操作的按钮,比如删除,修改增加 - 请求和响应的控制
如果用户通过非常规的操作,比如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求,也应当被前端所拦截
Vue的权限控制实现
1. 菜单的控制
-
查看登录之后获取到的数据
json{ "data": { "id": 500, "rid": 0, "username": "admin", "mobile": "13999999999", "email": "123999@qq.com", "token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM" }, "rights": [ { "id": 125, "authName": "用户管理", "icon": "icon-user", "children": [ { "id": 110, "authName": "用户列表", "path": "users", "rights": [ "view", "edit", "add", "delete" ] } ] }, { "id": 103, "authName": "角色管理", "icon": "icon-tijikongjian", "children": [ { "id": 111, "authName": "角色列表", "path": "roles", "rights": [ "view", "edit", "add", "delete" ] } ] }, { "id": 101, "authName": "商品管理", "icon": "icon-shangpin", "children": [ { "id": 104, "authName": "商品列表", "path": "goods", "rights": [ "view", "edit", "add", "delete" ] }, { "id": 121, "authName": "商品分类", "path": "categories", "rights": [ "view", "edit", "add", "delete" ] } ] } ], "meta": { "msg": "登录成功", "status": 200 } }
在这部分数据中,除了该用户的基本信息之外,还有两个字段很关键
- token,由于前端用户的状态保持
- rights:该用户具备的权限数据,一级权限就对应一级菜单,二级权限就对应二级菜单
-
根据rights中的数据,动态渲染左侧菜单栏,数据在Login.vue得到,但是在Home.vue才使用,所以可以把数据用vuex进行维护
-
vuex------index.js
jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'), username: sessionStorage.getItem('username') }, mutations: { setRightList(state, data) { state.rightList = data sessionStorage.setItem('rightList', JSON.stringify(data)) }, setUsername(state, data) { state.username = data sessionStorage.setItem('username', data) } }, actions: { }, getters: { } })
-
Login.vue
jslogin() { this.$refs.loginFormRef.validate(async (valid) => { if (!valid) return const { data: res } = await this.$http.post('login', this.loginForm) if (res.meta.status !== 200) return this.$message.error('登录失败!') console.log(res) this.$store.commit('setRightList', res.rights) this.$store.commit('setUsername', res.data.username) this.$message.success('登录成功') this.$router.push('/home') }) },
-
Home.vue
jsimport { mapState } from 'vuex' computed: { ...mapState(['rightList', 'username']) }, created() { this.activePath = window.sessionStorage.getItem('activePath') // 初始化menulist菜单栏的数据 this.menulist = this.rightList },
-
-
刷新界面菜单消失
-
原因分析
因为菜单数据是登录之后才获取的,存放在vuex中 一旦刷新界面,vuex中的数据会初始化为空 因此,需要将权限数据存储在sessionStorage中,并让其和vuex中的数据保持同步
-
代码解决
jsexport default new Vuex.Store({ state: { rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'), username: sessionStorage.getItem('username') }, mutations: { setRightList(state, data) { state.rightList = data sessionStorage.setItem('rightList', JSON.stringify(data)) }, setUsername(state, data) { state.username = data sessionStorage.setItem('username', data) } }, actions: { }, getters: { } })
-
退出按钮的逻辑
jslogout() { // 删除sessionStorage中的数据 sessionStorage.clear() this.$router.push('/login') // 删除vuex中的数据,让当前的界面刷新即可 window.location.reload() },
-
2. 界面的控制
- 正常的逻辑是通过登录界面,登录成功之后跳转到管理平台界面,但是如果用户直接敲管理平台的地址,也是可以跳过登录的步骤,所以应该在某个时机判断用户是否登录
-
如何判断是否登录
jslogin() { // 登录时存储token sessionStorage.setItem('token', res.data.token) },
-
什么时机
-
路由导航守卫
jsrouter.beforeEach((to, from, next) => { if (to.path === '/login') { next() } else { const token = sessionStorage.getItem('token') if (!token) { next('/login') } else { next() } } })
-
- 虽然菜单项已经被控制住了,但是路由信息还是完整的存在于浏览器,正比如zhangshan这个用户并不具备角色这个菜单,但是在地址栏中敲入/roles的地址,依然可以访问角色界面。
-
路由导航守卫
路由导航守卫固然可以在每次路由地址发生变化的时候,从vuex中取出rightList判断用户将要访问的界面有没有权限。不过从另一个角度来说,这个用户不具备权限的路由,是否也应该压根就不存在呢?
-
动态路由
- 登录成功之后动态添加
js// router.js import Vue from 'vue' import Router from 'vue-router' import Login from '@/components/Login.vue' import Home from '@/components/Home.vue' import Welcome from '@/components/Welcome.vue' import Users from '@/components/user/Users.vue' import Roles from '@/components/role/Roles.vue' import GoodsCate from '@/components/goods/GoodsCate.vue' import GoodsList from '@/components/goods/GoodsList.vue' import NotFound from '@/components/NotFound.vue' import store from '@/store' Vue.use(Router) // 动态路由规则映射 const userRule = { path: '/users', component: Users } const roleRule = { path: '/roles', component: Roles } const goodRule = { path: '/goods', component: GoodsList } const categoryRule = { path: '/categories', component: GoodsCate } const ruleMapping = { 'users': userRule, 'roles': roleRule, 'goods': goodRule, 'categories': categoryRule } const router = new Router({ routes: [ { path: '/', redirect: '/welcome' }, { path: '/login', component: Login }, { path: '/home', component: Home, redirect: '/welcome', children: [ { path: '/welcome', component: Welcome }, // { path: '/users', component: Users }, // { path: '/roles', component: Roles }, // { path: '/goods', component: GoodsList }, // { path: '/categories', component: GoodsCate } ] }, { path: '*', component: NotFound } ] }) // 动态路由 export function initDynamicRoutes() { // 根据二级权限,对路由规则进行动态的添加 console.log(router) const currentRoutes = router.options.routes const rightList = store.state.rightList rightList.forEach(item => { item.children.forEach(item => { // item 二级权限 const temp = ruleMapping[item.path] currentRoutes[2].children.push(temp) }) }) currentRoutes.forEach(item => { router.addRoute(item) }) } // 路由导航守卫 拦截没登录时的权限路由 router.beforeEach((to, from, next) => { if (to.path === '/login') { next() } else { const token = sessionStorage.getItem('token') if (!token) { next('/login') } else { next() } } }) export default router
js// Login.vue import { initDynamicRoutes } from "@/router.js" login() { this.$refs.loginFormRef.validate(async (valid) => { if (!valid) return const { data: res } = await this.$http.post('login', this.loginForm) if (res.meta.status !== 200) return this.$message.error('登录失败!') console.log(res) this.$store.commit('setRightList', res.rights) this.$store.commit('setUsername', res.data.username) sessionStorage.setItem('token', res.data.token) this.$message.success('登录成功') // 根据用户所具备的权限,动态添加路由规则 initDynamicRoutes() this.$router.push('/home') }) },
- App.vue中添加,防止登录后再次刷新后重新路由规则重新加载,菜单被初始化
jsexport default { name: 'app', created() { initDynamicRoutes() //动态添加路由规则 } }
3. 按钮的控制
虽然用户可以看到某些界面了,但是这个界面的一些按钮,该用户可能是没有权限的,因此,我们需要对组件中的一些按钮进行控制。用户不具权限的按钮就隐藏或者禁用,而在这块中,可以把该逻辑放到自定义指令中
-
permission.js 注册自定义指令
jsimport Vue from 'vue' import router from '@/router.js' Vue.directive('permission', { inserted(el, binding) { console.log(binding) const action = binding.value.action const effect = binding.value.effect // 判断 当前路由所对应的组件中,如何判断用户是否具备action的权限 console.log(router.currentRoute.meta) if (router.currentRoute.meta.indexOf(action) == -1) { if (effect === 'disabled') { el.disabled = true el.classList.add('is-disabled') } else { el.parentNode.removeChild(el) } } } })
-
main.js
jsimport './utils/permission.js' //引入到入口文件permission才会被加载
-
router.js 把路由元信息添加进来
jsexport function initDynamicRoutes() { // 根据二级权限,对路由规则进行动态的添加 console.log(router) const currentRoutes = router.options.routes const rightList = store.state.rightList rightList.forEach(item => { item.children.forEach(item => { // item 二级权限 const temp = ruleMapping[item.path] // 把路由元信息添加进来 temp.meta = item.rights currentRoutes[2].children.push(temp) }) }) currentRoutes.forEach(item => { router.addRoute(item) }) }
-
使用自定义指令
jsv-permission="{action:'add'}" v-permission="{action:'edit', effect:'disabled'}"
4. 请求和响应的控制
请求控制
-
除了登录请求都要带上token,这样服务器才可以鉴别你的身份
js// http.js import axios from 'axios' import Vue from 'vue' // 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来 // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/' axios.interceptors.request.use(req => { // console.log(req.url, req.method) if (req.url !== 'login') { // 不是登录的请求,我们应该在请求头中加入token数据 req.headers.Authorization = sessionStorage.getItem('token') } return req }) Vue.prototype.$http = axios
-
如果发出了非权限内的请求,应该直接在前端范围内组织,虽然这个请求发送到服务器也会被拒绝
js// http.js import axios from 'axios' import Vue from 'vue' import router from '@/router.js' // 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来 // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/' // 请求方式和权限的映射 const actionMapping = { 'get': 'view', 'post': 'add', 'put': 'edit', 'delete': 'delete' } axios.interceptors.request.use(req => { // console.log(req.url, req.method) if (req.url !== 'login') { // 不是登录的请求,我们应该在请求头中加入token数据 req.headers.Authorization = sessionStorage.getItem('token') // 判断非权限范围内的请求 // router.currentRoute.meta // resful风格请求 /* get请求 view post请求 add put请求 edit delete请求 delete [add view edit delete] */ const action = actionMapping[req.method] // 判断 action 是否存在当前路由的权限中 const rights = router.currentRoute.meta if (rights && rights.indexOf(action) === -1) { // 没有权限 alert('没有权限') return Promise.reject(new Error('没有权限')) } } return req }) Vue.prototype.$http = axios
响应控制
-
得到了服务器返回的状态码401,代表token超时或者被篡改了,此时应该强制跳转登录页
jsaxios.interceptors.response.use((res) => { if (res.data.meta.status === 401) { router.push('/login') sessionStorage.clear() // 通过Vuex的actions或mutations来清空或重置存储在store中的登录相关状态。 this.$store.dispatch('logout') } return res })
小结
前端权限的实现必须要后端提供数据支持,否则无法实现.
返回的权限数据的结构,前后端需要沟通协商,怎样的数据使用起来才最方便.
4.1.菜单控制
-
权限的数据需要在多组件之间共享,因此采用vuex.
-
防止刷新界面,权限数据丢失,所以需要存储在sessionStorage,并且要保证两者的同步
4.2.界面控制
-
路由的导航守卫可以防止跳过登录界面
-
动态路由可以让不具备权限的界面的路由规则压根就不存在
4.3.按钮控制
-
路由规则中可以增加路由元数据meta
-
通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据.
-
自定义指令可以很方便的实现按钮控制
4.4.请求和响应控制
-
请求拦截器和响应拦截器的使用.
-
请求方式的约定restful