文章目录
- 前端权限控制思路
- 1. 菜单的权限控制
- Vue的权限控制实现
- 1. 菜单的控制
- 2. 界面的控制
- 3. 按钮的控制
- 4. 请求和响应的控制
- 请求控制
- 响应控制
- 小结
前端权限控制思路
1. 菜单的权限控制
- 菜单的控制
在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持。前端根据权限数据展示对应的菜单,点击菜单才能查看相关的界面 - 界面的控制
如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录页。如用户已经登录,可是手动敲入非权限内的地址,则需要跳转404界面 - 按钮的控制
在某个菜单的界面,还得根据权限数据,展示出可进行操作的按钮,比如删除,修改增加 - 请求和响应的控制
如果用户通过非常规的操作,比如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求,也应当被前端所拦截
Vue的权限控制实现
1. 菜单的控制
-
查看登录之后获取到的数据
{"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
import 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 = datasessionStorage.setItem('rightList', JSON.stringify(data))},setUsername(state, data) {state.username = datasessionStorage.setItem('username', data)}},actions: {},getters: {} })
-
Login.vue
login() {this.$refs.loginFormRef.validate(async (valid) => {if (!valid) returnconst { 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
import { mapState } from 'vuex' computed: {...mapState(['rightList', 'username'])}, created() {this.activePath = window.sessionStorage.getItem('activePath')// 初始化menulist菜单栏的数据this.menulist = this.rightList},
-
-
刷新界面菜单消失
-
原因分析
因为菜单数据是登录之后才获取的,存放在vuex中 一旦刷新界面,vuex中的数据会初始化为空 因此,需要将权限数据存储在sessionStorage中,并让其和vuex中的数据保持同步
-
代码解决
export default new Vuex.Store({state: {rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),username: sessionStorage.getItem('username')},mutations: {setRightList(state, data) {state.rightList = datasessionStorage.setItem('rightList', JSON.stringify(data))},setUsername(state, data) {state.username = datasessionStorage.setItem('username', data)}},actions: {},getters: {} })
-
退出按钮的逻辑
logout() {// 删除sessionStorage中的数据sessionStorage.clear()this.$router.push('/login')// 删除vuex中的数据,让当前的界面刷新即可window.location.reload()},
-
2. 界面的控制
- 正常的逻辑是通过登录界面,登录成功之后跳转到管理平台界面,但是如果用户直接敲管理平台的地址,也是可以跳过登录的步骤,所以应该在某个时机判断用户是否登录
-
如何判断是否登录
login() {// 登录时存储tokensessionStorage.setItem('token', res.data.token)},
-
什么时机
-
路由导航守卫
router.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判断用户将要访问的界面有没有权限。不过从另一个角度来说,这个用户不具备权限的路由,是否也应该压根就不存在呢? -
动态路由
- 登录成功之后动态添加
// 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.routesconst rightList = store.state.rightListrightList.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
// Login.vue import { initDynamicRoutes } from "@/router.js" login() {this.$refs.loginFormRef.validate(async (valid) => {if (!valid) returnconst { 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中添加,防止登录后再次刷新后重新路由规则重新加载,菜单被初始化
export default {name: 'app',created() {initDynamicRoutes() //动态添加路由规则} }
3. 按钮的控制
虽然用户可以看到某些界面了,但是这个界面的一些按钮,该用户可能是没有权限的,因此,我们需要对组件中的一些按钮进行控制。用户不具权限的按钮就隐藏或者禁用,而在这块中,可以把该逻辑放到自定义指令中
-
permission.js 注册自定义指令
import Vue from 'vue' import router from '@/router.js' Vue.directive('permission', {inserted(el, binding) {console.log(binding)const action = binding.value.actionconst effect = binding.value.effect// 判断 当前路由所对应的组件中,如何判断用户是否具备action的权限console.log(router.currentRoute.meta)if (router.currentRoute.meta.indexOf(action) == -1) {if (effect === 'disabled') {el.disabled = trueel.classList.add('is-disabled')} else {el.parentNode.removeChild(el)}}} })
-
main.js
import './utils/permission.js' //引入到入口文件permission才会被加载
-
router.js 把路由元信息添加进来
export function initDynamicRoutes() {// 根据二级权限,对路由规则进行动态的添加console.log(router)const currentRoutes = router.options.routesconst rightList = store.state.rightListrightList.forEach(item => {item.children.forEach(item => {// item 二级权限const temp = ruleMapping[item.path]// 把路由元信息添加进来temp.meta = item.rightscurrentRoutes[2].children.push(temp)})})currentRoutes.forEach(item => {router.addRoute(item)}) }
-
使用自定义指令
v-permission="{action:'add'}" v-permission="{action:'edit', effect:'disabled'}"
4. 请求和响应的控制
请求控制
-
除了登录请求都要带上token,这样服务器才可以鉴别你的身份
// 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
-
如果发出了非权限内的请求,应该直接在前端范围内组织,虽然这个请求发送到服务器也会被拒绝
// 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请求 viewpost请求 addput请求 editdelete请求 delete[add view edit delete]*/const action = actionMapping[req.method]// 判断 action 是否存在当前路由的权限中const rights = router.currentRoute.metaif (rights && rights.indexOf(action) === -1) {// 没有权限alert('没有权限')return Promise.reject(new Error('没有权限'))}}return req }) Vue.prototype.$http = axios
响应控制
-
得到了服务器返回的状态码401,代表token超时或者被篡改了,此时应该强制跳转登录页
axios.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