前端权限控制背景:
做后台项目区别于做其它的项目,权限验证与安全性是非常重要的,是后台项目的基础核心功能。我们所要做到的是:不同的权限对应着不同的路由,同时侧边栏也需根据不同的权限,异步生成。这里先简单说一下,我实现登录和权限验证的思路。
- 登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
- 权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。
校验方式
前端对用户的权限信息进行校验往往在两个方面进行限制
- 路由不可见
- 元素不可见 通过以上两个方式,来将用户权限之外的内容隐藏掉。
登录
登录用户名和密码,成功后获取Token,保存Token。
<el-form ref="loginForm" :model="loginForm">
<el-input ref="username" v-model="loginForm.username"></el-input>
<el-input ref="password" v-model="loginForm.password"></el-input>
<el-button @click.native.prevent="handleLogin">login</el-button>
</el-form>
export default {
data(){
return {
loginForm: {
username: '',
password: ''
},
}
}
methods:{
handleLogin(){
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/' }); //登录成功之后重定向到首页
}).catch(err => {
this.$message.error(err); //登录失败提示错误
});
}
}
}
action
LoginByUsername({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
loginByUsername(username, userInfo.password).then(response => {
const data = response.data
Cookies.set('Token', response.data.token) //登录成功后将token存储在cookie之中
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
});
});
}
登录成功后,服务端会返回一个 token(该token的是一个能唯一标示用户身份的一个key),之后我们将token存储在本地cookie之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不用再去登录页面重新登录了。 ps:为了保证安全性,现在后台所有token有效期(Expires/Max-Age)都是Session,就是当浏览器关闭了就丢失了。重新打开游览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。
获取用户信息
用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,判断是否已获得token,在获得token之后我们就要去获取用户的基本信息。
//router.beforeEach
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => { // 拉取user_info
const roles = res.data.role;
next();//resolve 钩子
})
}
页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有token,就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。 当然如果是做了单点登录得功能的话,用户信息存储在本地也是可以的。
权限篇
前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过router.addRoutes动态挂载路由。
不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。
vue中的新的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 }
];
通过meta标签来标示改页面能访问的权限有哪些,如meta: { role: ['admin','super_editor'] }表示该页面只有admin和超级编辑才能有资格进入.
注意事项:这里有一个需要非常注意的地方就是 404 页面一定要最后加载,如果放在constantRouterMap一同声明了404,后面的所以页面都会被拦截到404。
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'); // 否则全部重定向到登录页
}
}
});
store/permission.js
// store/permission.js
import { asyncRouterMap, constantRouterMap } from 'src/router';
function hasPermission(roles, route) {
if (route.meta && route.meta.role) {
return roles.some(role => route.meta.role.indexOf(role) >= 0)
} else {
return true
}
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers;
state.routers = constantRouterMap.concat(routers);
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data;
const accessedRouters = asyncRouterMap.filter(v => {
if (roles.indexOf('admin') >= 0) return true;
if (hasPermission(roles, v)) {
if (v.children && v.children.length > 0) {
v.children = v.children.filter(child => {
if (hasPermission(roles, child)) {
return child
}
return false;
});
return v
} else {
return v
}
}
return false;
});
commit('SET_ROUTERS', accessedRouters);
resolve();
})
}
}
};
export default permission;