概述
前端权限的控制本质上来说,就是控制端的视图层的展示和前端所发送的请求。但是只有前端权限控制没有后端权限控制是万万不可的。前端权限控制只可以说是达到锦上添花的效果。
前端权限的意义:
- 降低非法操作的可能性,在页面中展示出一个就算点击了也最终会失败的按钮,势必会增加有心者非法操作的可能性;
- 尽可能排除不必要清求,减轻服务器压力。没必要的请求,操作失败的清求,不具备权限的清求,应该压根就不需要发送。请求少了,自然也会减轻服务器的压力。
前端权限控制实现
登录退出与权限菜单栏控制
用户登录之后服务端返回一个数据,这个数据有菜单列表和token,我们把这个数据放入到vuex中,然后主页根据vuex中的数据进行菜单列表的渲染。
代码实现:
- 首先,在 vuex mutations对象里面定义一个方法 setUser 用于在登录之后接收服务端返回一个数据,并将这个数据存储在vuex state user 里面;
export default new Vuex.Store({ state: { user: JSON.parse(sessionStorage.getItem('v-user') || "{}") }, mutations: { setUser(state, data) { state.user = data; sessionStorage.setItem("v-user", JSON.stringify(data)); sessionStorage.setItem( "token", encodeURIComponent(JSON.stringify(data.token)) ); } }, })
- 在 login.vue 里面引入这个方法,在 submitForm 方法里,发送 login api 后,根据返回的数据给出相应的提示,如果是错误信息,给出错误提示。如果正确,调用 setUser 方法保存返回的数据,返回的数据里面包含用户名,token和侧边栏菜单数据,并且通过“this.$router.push("/")”跳转到home页面;
返回的数据:methods: { ...mapMutations(["setUser"]), submitForm() { this.$refs.form.validate((valid) => { if (valid) { this.isLogin = true; this.$api .login({ username: this.userData.username, password: this.userData.password, }) .then((res) => { this.isLogin = false; if (!res.data) { this.$message({ message: "用户名或密码错误!", type: "error", }); return false; } this.setUser(res.data); initDynamicRouter(); this.$router.push("/"); }); } return false; }); }, },
- 将保存的数据在侧边栏页面 Aside.vue 接收;
将其渲染在页面上,注意,数据返回的path路径最好和前端路由中定义的页面一样,这样,使用编程式导航时可以直接使用:import { mapState } from "vuex"; export default { data() { return { menus: [], }; }, computed: { ...mapState(["user"]), }, mounted() { this.menus = this.user.rights; }, };
<el-submenu v-for="(item, idx) in menus" :key="idx" class="nav-title" :index="String(idx)" > <template slot="title"> <i class="el-icon-location"></i> <span slot="title">{{ item.authName }}</span> </template> <el-menu-item v-for="(item, idx) in item.children" :key="idx" @click=" $router.push({ path: item.path, query: { name: item.authName } }) " >{{ item.authName }}</el-menu-item > </el-submenu>
- 在刷新后,如果 vuex 中的 user 数据只存储在内存中,刷新界面vuex数据消失,菜单栏消失。所以,将数据存储在sessionStorage中,并让其和vuex中的数据保持同步;
- 在退出时,清空所有数据并且跳转到登录页面。
logout() { this.$confirm("你是否要退出登录?", "登出", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }) .then(() => { sessionStorage.clear(); this.$router.push("/login"); window.location.reload(); }) .catch((err) => err); },
路由界面的控制
如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录界面。如果用户已经登录,如果手动敲入非权限内的地址,则需要跳转404 界面。
使用路由导航守卫,进入登录页面,不需要任何限制,进入其他页面,先从 vuex 中拿到token字段,有token字段就放行,没有,就跳转到登录页面,重新登录。
router.beforeEach((to, from, next) => {
const user = sessionStorage.getItem('token') ;
if (to.path === '/login') {
next();
} else {
if (!token) {
next({
path: '/login',
query: {
message: '请先登录!',
time: +new Date()
}
})
} else {
next();
}
}
这样用户在登录之后就才能访问相应的界面了,但如果用户A登录之后他只能访问a页面,他不能访问b页面,但是这时候他还是可以通过地址栏输入进入到b页面,这该怎么办呢?
当然我们也可以设置路由导航守卫,但是如果有多个页面,设置会非常不方便,并且对于用户A来说,它是不用访问b页面的,这时候我们何不对A不显示b页面,这个时候我们就用到了动态路由。
我们可以根据当前用户所拥有的的权限数据来动态添加所需要的路由。
定义动态路由
首先,我们将home页面下的静态路由抽离出来,先定义好所有的路由规则:
const map = {
'/menu/one': { path: 'menu/one', component: () => import('@/views/Page1.vue') },
'/menu/two': { path: 'menu/two', component: () => import('@/views/Page1.vue') },
'/menu/three': { path: 'menu/three', component: () => import('@/views/Page1.vue') },
'/menu/four': { path: 'menu/four', component: () => import('@/views/Page1.vue') },
'/menu/five': { path: 'menu/five', component: () => import('@/views/Page1.vue') }
}
动态匹配
我们需要根据服务端返回的数据进行动态匹配,首先,我们来看看服务端返回的数据。
需要根据 rights-->children-->path 的值进行动态匹配,首先我们编写一个动态匹配的方法 initDynamicRouter :
import store from '@/store';
const routes = [
{
path: '/',
name: 'Home',
component: Home,
redirect: '/menu/one',
children: []
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '*',
name: 'NotFound',
component: NotFound
}
]
const router = new VueRouter({ routes });
export const initDynamicRouter = () => {
const currentRoutes = router.options.routes
const homeRoute = currentRoutes.find(route => route.name === 'Home');
const rights = store.state.user.rights;
rights.forEach(right => {
right.children.forEach(child => {
if (map[child.path]) {
homeRoute.children.push(map[child.path])
}
})
})
router.addRoutes(currentRoutes)
}
执行 initDynamicRouter 方法
在登录成功后执行 initDynamicRouter :
methods:{
.........
submitForm() {
this.$refs.form.validate((valid) => {
if (valid) {
this.isLogin = true;
this.$api
.login({
username: this.userData.username,
password: this.userData.password,
})
.then((res) => {
this.isLogin = false;
if (!res.data) {
this.$message({
message: "用户名或密码错误!",
type: "error",
});
return false;
}
this.setUser(res.data);
initDynamicRouter(); //登录成功后执行initDynamicRouter方法,给用户添加动态路由。
this.$router.push("/");
});
}
return false;
});
},
.........
}
如果我们重新刷新的话动态路由就会消失,动态路由是在登录成功之后才会调用的,刷新的时候并没有调用,所以动态路由没有添加上。可以在 app.vue 中的 created 中调用添加动态路由的方法。
import { initDynamicRouter } from "@/router";
export default {
name: "app",
created() {
initDynamicRouter();
},
};
在地址栏输入没有权限的路由:
按钮的控制
虽然用户可以看到相关权限的界面了, 但是这个界面的一些按钮该用户可能是没有权限的。 因此, 我们需要对组件中的一些按钮进行控制, 用户不具备权限的按钮就隐藏或者禁用, 而在这块的实现中, 可以把该逻辑放到自定义指令中。
修改动态路由添加方法 initDynamicRouter
在配置动态路由时,可以将服务端返回数据的关于按钮的操作权限放到动态路由中,首先,让我们看一下数据:
返回的数据条目都有一个rights选项,里面包含的就是每条路由所拥有的按钮操作权限,这个用户用户["view","edit","add","delete"]这几个权限,修改 initDynamicRouter 方法,将他们放置到动态路由的 meta 对象中。
const map = {
'/menu/one': { path: 'menu/one', component: () => import('@/views/Page1.vue') },
'/menu/two': { path: 'menu/two', component: () => import('@/views/Page1.vue') },
'/menu/three': { path: 'menu/three', component: () => import('@/views/Page1.vue') },
'/menu/four': { path: 'menu/four', component: () => import('@/views/Page1.vue') },
'/menu/five': { path: 'menu/five', component: () => import('@/views/Page1.vue') }
}
export const initDynamicRouter = () => {
const currentRoutes = router.options.routes
const homeRoute = currentRoutes.find(route => route.name === 'Home');
const rights = store.state.user.rights;
console.log(JSON.stringify(rights, null, 2));
rights.forEach(right => {
right.children.forEach(child => {
if (map[child.path]) {
if (!map[child.path].meta) {
map[child.path].meta = {}
}
map[child.path].meta.rights = child.rights
homeRoute.children.push(map[child.path])
}
})
})
router.addRoutes(currentRoutes)
}
编写自定义指令 v-permission
import Vue from 'vue'
import router from '@/router/index.js';
Vue.directive('permission', {
inserted(el, binding) {
const action = binding.value.action;
const effect = binding.value.effect;
//当前路由元信息是否包含action权限
if (router.currentRoute.meta.rights.indexOf(action) === -1) {
if (effect === 'disabled') {
el.disabled = true; // 禁用元素
el.classList.add('is-disabled');
} else {
el.parentNode.removeChild(el); // 直接移除
}
}
}
})
这是一个添加新消息的按钮,所以给 v-permission 传入 一个对象,包含“{action:'add'}” , 在自定义指令中通过“router.currentRoute.meta.rights”拿出当前动态路由所拥有的权限看是否包含这个“add”选项,如果是,在页面上保留着按钮,如果不是,则删除它。
如果还包含了“effect: 'disabled'”选项,这不删除它,但是给它添加一个禁用的css class 类。
请求和响应的控制
请求控制
除了登录请求都得要带上token ,这样服务器才可以鉴别你的身份。
import router from '@/router'
axios.interceptors.request.use(config => {
if (config.url !== 'login') {
config.headers.Authorization = sessionStorage.getItem('token');
}
return config
})
这块使用的就是asiox的请求拦截器设置。
如果发出了非权限内的请求, 应该直接在前端范围内阻止, 虽然这个请求发到服务器也会被拒绝。 非权限内的请求:比如a用户是不能够操作该页面的按钮的,但是他通过f12调试把按钮改为可点击,如果我们不对这个请求进行处理,那么这个请求就会发送出去。
import router from '@/router'
const map = {
get: 'view',
post: 'add',
put: 'edit',
delete: 'delete'
}
// 修改asiox的请求拦截器
axios.interceptors.request.use(config => {
const rights = router.currentRoute.meta.rights || []
if (config.url !== 'login') {
config.headers.Authorization = sessionStorage.getItem('token');
}
if (config.url !== '/login' && !rights.includes(map[config.method])) {
return Promise.reject(new Error('没有权限操作!'))
}
return config
})
响应控制
得到了服务器返回的状态码401, 代表token 超时或者被篡改了,此时应该强制跳转到登录界面
import router from '@/router'
axios.interceptors.response.use(res => {
if (res.data.meta.status === 401) {
sessionStorage.clear();
this.$router.push("/login");
window.location.reload();
}
return config
})
总结
前端权限的实现之须要后端提供数据支持, 否则无法实现。返回的权限数据的结构, 前后端需要沟通协商怎样的数据便用起来才最方便。
菜单控制
- 权限的数据需要在多组件之间共享,因此采用vuex;
- 防止刷新界面, 权限数据丢失,所以需要存在sessionStorage,并目要保证两者的同步。
界面控制
- 路由的导航守卫可以防止跳过登录界面;
- 动态路由可以让不具备权限的界面的路由规则压根就不存在。
按钮控制
- 路由规则中可以增加路由元数据meta;
- 通过路由对象可以得到当前的路由规则以及存在此规则中的meta 数据;
- 自定义指令可以很方便的实现按钮控制。
请求和响应控制
- 使用axios请求拦截器和响应拦截器;
- 请求方式的约定restful 。