完整项目地址:vue-element-adminhtml
系列文章:前端
拖更有点严重,过了半个月才写了第二篇教程。无奈本身是一个业务猿,天天被我司的产品虐的死去活来,以前又病了一下休息了几天,你们见谅。vue
进入正题,作后台项目区别于作其它的项目,权限验证与安全性是很是重要的,能够说是一个后台项目一开始就必须考虑和搭建的基础核心功能。咱们所要作到的是:不一样的权限对应着不一样的路由,同时侧边栏也需根据不一样的权限,异步生成。这里先简单说一下,我实现登陆和权限验证的思路。webpack
上述全部的数据和操做都是经过vuex全局管理控制的。(补充说明:刷新页面后 vuex的内容也会丢失,因此须要重复上述的那些操做)接下来,咱们一块儿手摸手一步一步实现这个系统。ios
首先咱们无论什么权限,来实现最基础的登陆功能。git
随便找一个空白页面撸上两个input的框,一个是登陆帐号,一个是登陆密码。再放置一个登陆按钮。咱们将登陆按钮上绑上click事件,点击登陆以后向服务端提交帐号和密码进行验证。 这就是一个最简单的登陆页面。若是你以为还要写的更加完美点,你能够在向服务端提交以前对帐号和密码作一次简单的校验。详细代码github
click事件触发登陆操做:web
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/' }); //登陆成功以后重定向到首页
}).catch(err => {
this.$message.error(err); //登陆失败提示错误
});
复制代码
action:vue-router
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之中,这样下次打开页面或者刷新页面的时候能记住用户的登陆状态,不用再去登陆页面从新登陆了。vuex
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 钩子
})
复制代码
就如前面所说的,我只在本地存储了一个用户的token,并无存储别的用户信息(如用户权限,用户名,用户头像等)。有些人会问为何不把一些其它的用户信息也存一下?主要出于以下的考虑:
假设我把用户权限和用户名也存在了本地,但我这时候用另外一台电脑登陆修改了本身的用户名,以后再用这台存有以前用户信息的电脑登陆,它默认会去读取本地 cookie 中的名字,并不会去拉去新的用户信息。
因此如今的策略是:页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程从新登陆,若是有token,就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。 固然若是是作了单点登陆得功能的话,用户信息存储在本地也是能够的。当你一台电脑登陆时,另外一台会被提下线,因此总会从新登陆获取最新的内容。
并且从代码层面我建议仍是把 login
和get_user_info
两件事分开比较好,在这个后端全面微服务的年代,后端同窗也想写优雅的代码~
先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每个路由可访问的权限。当用户登陆以后,经过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再经过router.addRoutes
动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么作权限控制都不是绝对安全的,后端的权限验证是逃不掉的。
我司如今就是前端来控制页面级的权限,不一样权限的用户显示不一样的侧边栏和限制其所能进入的页面(也作了少量按钮级别的权限控制),后端则会验证每个涉及请求的操做,验证其是否有该操做的权限,每个后台的请求无论是 get 仍是 post 都会让前端在请求 header
里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操做。若没有权限则抛出一个对应的状态码,前端检测到该状态码,作出相对应的操做。
有不少人表示他们公司的路由表是于后端根据用户的权限动态生成的,我司不采起这种方式的缘由以下:
在以前经过后端动态返回前端路由一直很难作的,由于vue-router必须是要vue在实例化以前就挂载上去的,不太方便动态改变。不过好在vue2.2.0之后新增了router.addRoutes
Dynamically add more routes to the router. The argument must be an Array using the same route config format with the routes constructor option.
有了这个咱们就可相对方便的作权限控制了。(楼主以前在权限控制也走了很多歪路,能够在项目的commit记录中看到,重构了不少次,最先没用addRoute整个权限控制代码里都是各类if/else的逻辑判断,代码至关的耦合和复杂)
首先咱们实现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
,详细的问题见addRoutes when you've got a wildcard route for 404s does not work
关键的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'); // 不然所有重定向到登陆页
}
}
});
复制代码
这里的router.beforeEach也结合了上一章讲的一些登陆逻辑代码。
addRoutes
方法以前的权限判断,很是的繁琐,由于我是把全部的路由都挂在了上去,全部我要各类判断当前的用户是否有权限进入该页面,各类
if/else
的嵌套,维护起来至关的困难。但如今有了
addRoutes
以后就很是的方便,我只挂载了用户有权限进入的页面,没权限,路由自动帮我跳转的
404
,省去了很多的判断。
这里还有一个小hack的地方,就是router.addRoutes
以后的next()
可能会失效,由于可能next()
的时候路由并无彻底add完成,好在查阅文档发现
next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.
这样咱们就能够简单的经过next(to)
巧妙的避开以前的那个问题了。这行代码从新进入router.beforeEach
这个钩子,这时候再经过next()
来释放钩子,就能确保全部的路由都已经挂在完成了。
就来就讲一讲 GenerateRoutes Action
// 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;
复制代码
这里的代码说白了就是干了一件事,经过用户的权限和以前在router.js里面asyncRouterMap的每个页面所须要的权限作匹配,最后返回一个该用户可以访问路由有哪些。
最后一个涉及到权限的地方就是侧边栏,不过在前面的基础上已经很方便就能实现动态显示侧边栏了。这里侧边栏基于element-ui的NavMenu来实现的。 代码有点多不贴详细的代码了,有兴趣的能够直接去github上看地址,或者直接看关于侧边栏的文档。
说白了就是遍历以前算出来的permission_routers
,经过vuex拿到以后动态v-for渲染而已。不过这里由于有一些业务需求因此加了不少判断 好比咱们在定义路由的时候会加不少参数
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
role: ['admin','editor'] will control the page role (you can set multiple roles)
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
noCache: true if fasle ,the page will no be cached(default is false)
}
**/
复制代码
这里仅供参考,并且本项目为了支持无限嵌套路由,全部侧边栏这块使用了递归组件。如须要请你们自行改造,来打造知足本身业务需求的侧边栏。
侧边栏高亮问题:不少人在群里问为何本身的侧边栏不能跟着本身的路由高亮,其实很简单,element-ui官方已经给了default-active
因此咱们只要
:default-active="$route.path" 将
default-active
一直指向当前路由就能够了,就是这么简单
有不少人一直在问关于按钮级别粒度的权限控制怎么作。我司如今是这样的,真正须要按钮级别控制的地方不是不少,如今是经过获取到用户的role以后,在前端用v-if手动判断来区分不一样权限对应的按钮的。理由前面也说了,我司颗粒度的权限判断是交给后端来作的,每一个操做后端都会进行权限判断。并且我以为其实前端真正须要按钮级别判断的地方不是不少,若是一个页面有不少种不一样权限的按钮,我以为更多的应该是考虑产品层面是否设计合理。固然你强行说我想作按钮级别的权限控制,你也能够参照路由层面的作法,搞一个操做权限表。。。但我的以为有点画蛇添足。或者将它封装成一个指令都是能够的。
这里再说一说 axios 吧。虽然在上一篇系列文章中简单介绍过,不过这里仍是要在唠叨一下。如上文所说,我司服务端对每个请求都会验证权限,因此这里咱们针对业务封装了一下请求。首先咱们经过request拦截器在每一个请求头里面塞入token,好让后端对请求进行权限验证。并建立一个respone拦截器,当服务端返回特殊的状态码,咱们统一作处理,如没权限或者token失效等操做。
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 建立axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})
// request拦截器
service.interceptors.request.use(config => {
// Do something before request is sent
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每一个请求携带token--['X-Token']为自定义key 请根据实际状况自行修改
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
// respone拦截器
service.interceptors.response.use(
response => response,
/** * 下面的注释为经过response自定义code来标示请求状态,当code返回以下状况为权限有问题,登出并返回到登陆页 * 如经过xmlhttprequest 状态码标识 逻辑可写在下面error中 */
// const res = response.data;
// if (res.code !== 20000) {
// Message({
// message: res.message,
// type: 'error',
// duration: 5 * 1000
// });
// // 50008:非法的token; 50012:其余客户端登陆了; 50014:Token 过时了;
// if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// MessageBox.confirm('你已被登出,能够取消继续留在该页面,或者从新登陆', '肯定登出', {
// confirmButtonText: '从新登陆',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// store.dispatch('FedLogOut').then(() => {
// location.reload();// 为了从新实例化vue-router对象 避免bug
// });
// })
// }
// return Promise.reject('error');
// } else {
// return response.data;
// }
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
})
export default service
复制代码
文章一开始也说了,后台的安全性是很重要的,简简单单的一个帐号+密码的方式是很难保证安全性的。因此我司的后台项目都是用了两步验证的方式,以前咱们也尝试过使用基于 google-authenticator 或者youbikey
这样的方式但难度和操做成本都比较大。后来仍是准备借助腾讯爸爸,这年代谁不用微信。。。安全性腾讯爸爸也帮我作好了保障。 楼主建议两步验证要支持多个渠道不要只微信或者QQ,前段时间QQ第三方登陆就出了bug,官方两三天才修好的,害我背了锅/(ㄒoㄒ)/~~ 。
这里的两部验证有点名存实亡,其实就是帐号密码验证过以后还须要一个绑定的第三方平台登陆验证而已。 写起来也很简单,在原有登陆得逻辑上改造一下就好。
this.$store.dispatch('LoginByEmail', this.loginForm).then(() => {
//this.$router.push({ path: '/' });
//不重定向到首页
this.showDialog = true //弹出选择第三方平台的dialog
}).catch(err => {
this.$message.error(err); //登陆失败提示错误
});
复制代码
登陆成功以后不直接跳到首页而是让用户两步登陆,选择登陆得平台。 接下来就是全部第三方登陆同样的地方经过 OAuth2.0 受权。这个各大平台大同小异,你们自行查阅文档,不展开了,就说一个微信受权比较坑的地方。注意你连参数的顺序都不能换,否则会验证不经过。具体代码,同时我也封装了openWindow方法你们自行看吧。 当第三方受权成功以后都会跳到一个你以前有一个传入redirect——uri的页面
因此咱们后台也须要开一个authredirect页面:代码。他的做用是第三方登陆成功以后会默认跳到受权的页面,受权的页面会再次重定向回咱们的后台,因为是spa,改变路由的体验很差,咱们经过window.opener.location.href
的方式改变hash,在login.js里面再监听hash的变化。当hash变化时,获取以前第三方登陆成功返回的code与第一步帐号密码登陆以后返回的uid一同发送给服务端验证是否正确,若是正确,这时候就是真正的登陆成功。
created() {
window.addEventListener('hashchange', this.afterQRScan);
},
destroyed() {
window.removeEventListener('hashchange', this.afterQRScan);
},
afterQRScan() {
const hash = window.location.hash.slice(1);
const hashObj = getQueryObject(hash);
const originUrl = window.location.origin;
history.replaceState({}, '', originUrl);
const codeMap = {
wechat: 'code',
tencent: 'code'
};
const codeName = hashObj[codeMap[this.auth_type]];
this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
this.$router.push({
path: '/'
});
});
}
复制代码
到这里涉及登陆权限的东西也差很少讲完了,这里楼主只是给了你们一个实现的思路(都是楼主不断摸索的血泪史),每一个公司实现的方案都有些出入,请谨慎选择适合本身业务形态的解决方案。若是有什么想法或者建议欢迎去本项目下留言,一同讨论。
常规占坑,这里是手摸手,带你用vue撸后台系列。
完整项目地址:vue-element-admin