前言
后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。我们所要做到的是:不同的权限对应着不同的路由,同时侧边栏也需根据不同的权限,异步生成。这里先简单说一下,我实现登录和权限验证的思路。
登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。
上述所有的数据和操作都是通过vuex全局管理控制的。(补充说明:刷新页面后 vuex的内容也会丢失,所以需要重复上述的那些操作)接下来,我们一起手摸手一步一步实现这个系统。
登录篇
绘制两个input输入框,在前端预校验后发送给服务器端。登录成功后,服务器端返回一个token(该token是一个能唯一标示用户身份的一个key),将token储存在cookie中。之后我们打开或者刷新页面的时候就不用再重新登录了。
(为了保证安全,token的有效期应设置为Session,在用户关闭浏览器时销毁。后端也会在每周固定一个时间点重新刷新token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。)
获取用户信息
用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有token,就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。
权限篇
权限控制主体思路
前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过router.addRoutes动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。
不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。
addRoutes
在之前通过后端动态返回前端路由一直很难做的,因为vue-router必须是要vue在实例化之前就挂载上去的,不太方便动态改变。不过好在vue2.2.0以后新增了router.addRoutes。有了这个我们就可相对方便的做权限控制了。
具体实现
- 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
- 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
- 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
- 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
router.js
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Login from '../views/login/';
const dashboard = resolve => require(['../views/dashboard/index'], resolve);
//使用了vue-routerd的[Lazy Loading Routes
](https://router.vuejs.org/en/advanced/lazy-loading.html)
//所有权限通用路由表
//如首页和登录页和一些不用权限的公用页面
export const constantRouterMap = [
{ path: '/login', component: Login },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: '首页',
children: [{ path: 'dashboard', component: dashboard }]
},
]
//实例化vue的时候只挂载constantRouter
export default new Router({
routes: constantRouterMap
});
//异步挂载的路由
//动态需要根据权限加载的路由表
export const asyncRouterMap = [
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: { role: ['admin','super_editor'] }, //页面需要的权限
children: [
{
path: 'index',
component: Permission,
name: '权限测试页',
meta: { role: ['admin','super_editor'] } //页面需要的权限
}]
},
{ path: '*', redirect: '/404', hidden: true }
];
这里我们根据 vue-router官方推荐 的方法通过meta标签来标示改页面能访问的权限有哪些。如meta: { role: ['admin','super_editor'] }
表示该页面只有admin和超级编辑才能有资格进入。
注意事项:这里有一个需要非常注意的地方就是 404
页面一定要最后加载,如果放在constantRouterMap
一同声明了404
,后面的所以页面都会被拦截到404
。
main.js
关键的main.js
// main.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判断是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch(err => {
console.log(err);
});
} else {
next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next();
} else {
next('/login'); // 否则全部重定向到登录页
}
}
});
main.js的详细注释
router.beforeEach(async (to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// 如果已经登录则重定向到首页
next({ path: '/' })
NProgress.done()
} else {
// 判断当前用户是否已获取到 user_info 信息
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
// 未获取到 user_info 信息
try {
// 获取 user_info
// 权限列表必须是一个数组 例如: ['admin'] 或者 ['developer','editor']
const { roles } = await store.dispatch('user/getInfo')
// store.dispatch('user/getInfo') 代码如下:
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
// getInfo 代码如下:
// 携带 token 发送 get 请求来获取 user_info
function getInfo(token) {
return request({
url: '/vue-element-admin/user/info',
method: 'get',
params: { token }
})
}
// getInfo 代码结束
const { data } = response
if (!data) {
// 如果data不存在则报错
reject('Verification failed, please Login again.')
}
// 解构出data中的 roles, name, avatar, introduction
const { roles, name, avatar, introduction } = data
// 权限列表必须是一个非空数组
if (!roles || roles.length <= 0) {
// 如果为空数组则报错
reject('getInfo: roles must be a non-null array!')
}
// 将获取到的user_info存在state中
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
// 上述4行代码如下:
SET_ROLES: (state, roles) => {
state.roles = roles
}
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
}
// 4行代码结束
resolve(data)
}).catch(error => {
reject(error)
})
})
}
//生成当前角色可访问的路由表
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
// store.dispatch('permission/generateRoutes', roles)代码如下:
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
// 如果roles包含admin,则accessedRoutes等于asyncRoutes
// asyncRoutes为router中定义的所有需要权限的路由表
// 也就是说admin拥有访问所有页面的权限
accessedRoutes = asyncRoutes || []
} else {
//过滤出拥有roles权限的路由
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
// filterAsyncRoutes代码如下:
function filterAsyncRoutes(routes, roles) {
const res = []
//遍历需要权限的路由表
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
// hasPermission 代码如下:
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
// 如果route中有roles权限则返回true,都没有则返回false
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
// hasPermission 代码结束
//如果tmp中存在子路由
if (tmp.children) {
//递归 为tmp添加children属性(相当于添加子路由)
//属性值为含有该roles权限的路由
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
// 将含有roles权限的路由添加到res中
res.push(tmp)
}
})
// 返回所有含有roles权限的路由表
// accessedRoutes = res
return res
}
// filterAsyncRoutes代码结束
}
commit('SET_ROUTES', accessedRoutes)
// SET_ROUTES 代码如下:
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
// 将不需要权限的路由和需要权限的路由拼接到一起
// constantRoutes为router中不需要权限的路由表
state.routes = constantRoutes.concat(routes)
}
// SET_ROUTES 代码结束
resolve(accessedRoutes)
})
}
// generateRoutes 代码结束
//动态添加可访问路由表
router.addRoutes(accessRoutes)
// hack方法 确保addRoutes已完成 设置replace: true 所以导航不会留下历史记录
next({ ...to, replace: true })
} catch (error) {
// 删除token回到登录页面重新登录
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* 没有 token*/
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单中继续跳转
next()
} else {
// 否则全部重定向到登录页
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
康雀西威自: https://juejin.im/post/591aa14f570c35006961acac
作者:花裤衩
来源:掘金