目录
什么是权限控制?
如何实现权限控制
改进的解决方案
基本实现
登出再登入问题
页面刷新问题
首先,我们按照最初的开发模式,在router.js中将所有的组件都注册在路由中.
import Vue from 'vue'
import Router from 'vue-router'
import { extend } from '../utils/router'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/login') //懒加载
},
{
path: '/home',
name: 'home',
roles: ['guest', 'admin'], //用于验证路由能够被访问的角色
component: () => import('../views/home')
}
...
{
path: '*'
redirect: '/404'
}
]
})
export default router
然后你可能会在登录成功后,可以从后台返回的数据中得到用户的权限,通过这个权限,控制显示的导航菜单.
比如从后台接受到的数据是这样的:
{
code: 200,
...
rows: [
{
userCode: AKJUFIANJ,
...
roles: 'admin'
}
]
}
然后把用户信息存储到vuex当中去,通过判断用户的角色,得到导航菜单.
或许你会这样做:
// 导航菜单
{{item.name}}
computed {
menu () {
let menuArray = []
// 用户为消费者,返回商城等导航
if (store.getter.roles === 'guest') {
menuArray = [
{
name: '商城',
url: '/shop'
},
...
]
}
// 用户为管理员,返回管理等导航
else if (store.getter.roles === 'admin') {
menuArray = [
{
name: '管理',
url: '/manage'
},
...
]
}
return menuArray
}
}
问题来了:
假如,我直接访问/manage,就能进入管理员路由页面了.所以这就需要权限控制,限制用户进入禁止访问的路由.
我们接着上面讲,那么如果进行权限控制?
我相信你以前一定这么做过:
main.js:
router.beforeEach((to, from, next) => {
to.roles.includes(store.getter.roles) ? next() : next('/login')
})
通过beforeEach在每次路由跳转的时候判断我们登录成功后存储在vuex中的角色是否包含在目标路由可访问的角色当中.
这样做,虽然能够达到效果.但是如果我将vuex当中的roles值更改,一样能够突破浏览器控制,访问到权限控制路由页面.
千万不要以为这种事做不到,要知道,几乎所有存储在浏览器的数据用户都是能够访问到的.如果你将权限控制完全交给前端,恶意用户很容易就能够突破浏览器,对系统造成危害.
因此,这种方式是行不通的,我们需要确保每次访问的路由都是经过授权的.
使用addRoutes动态添加路由,通过登录之后后台返回的数据动态加载路由,给予能够访问的路由页面.这样一来,即使访问权限控制页面,由于并没有注册到路由当中,也是无法访问的.
补充:
目前vue当中进行权限控制的方案有两种:
第一种:前端进行权限逻辑控制 (路由数据在前端,根据后台返回数据进行选择性动态加载)
第二种:后台进行权限逻辑控制 (路由数据由后台返回给前端进行动态加载)
本文探讨的是第二种实现方式,其实两种方式思想是一致的,只要能够实现一种,另外一种方式的实现也会信手拈来.
假设后台返回的数据如下:
{
code: 200,
token: 'FSDFD21321GVXZV31243',
routes:
{
path: '/',
component: 'XXX',
children:
[
{
path: 'home',
name: '首页',
component: 'home'
},
{
path: 'shop',
name: '购物',
component: 'shop'
},
{
path: 'pay',
name: '支付',
component: 'pay'
}
]
}
}
在登录成功后,将json转换为实例化router时的routes数组.也就是一下形式(具体实现略)
[
{
path: '/login',
name: 'login',
component: () => import('../views/login')
},
...
]
然后使用注册动态路由
// extendsRouter为将json转换为数组的函数
this.$router.addRoutes(extendsRouter(res.roles).concat({
path: '*',
redirect: '/404'
}))
最后我们需要将错误的路由页面重定向到404.
这样就完了吗?当然没有
我们之前说过,权限控制不能完全交给前端,因为浏览器防御很容易被突破(对于懂web的人来说相当于没有防御力),因此我们在每次路由跳转的时候都需要访问后台,通过后台返回的数据判断目标路由是否在权限内,而不是将目标路由与存储在前端的路由做判断。
因此,beforeEach中我们需要改善一下我们的代码:
// 伪代码
router.beforeEach((to, from, next) => {
// 跳往基本路由(白名单路由)不进行拦截
if (isBaseRouter(to.path)) {
next()
return false
}
// 如果没有token则跳转到登录页
else if (!store.getter.token) {
next('/login')
return false
}
// 如果有token,发起请求获取路由信息
// 如果返回的路由信息里包括了目标路由,则放行
// request.get('/routes').includes(to.path) ? next() : next('/login')
if ( request.get('/routes').includes(to.path) ) {
next()
}
// 如果目标路径不在权限内.那么就必须重新登录注册动态路由后才能够访问
else {
next('/login')
}
})
补充:
使用router.addRoutes添加的动态路由并不是响应式的,也就是说在router.options.routes当中没有动态添加的路由,存放的是静态路由.isBaseRouter其实就是目标路径(to.path)是否存在于router.options.routes当中
另外,根据实际情况还需要对token失效问题进行处理,略.
做到这儿,权限控制也差不多了,但是还有一些小问题.
心思缜密的你一定已经意识到了:如果A和B是两个不同的权限用户,只能访问各自的路由页面.那么现在A注销账号,清空数据后,用B账号登录,由于路由未实例化的原因,在我们登录了A和B之后,动态路由当中注册的路由就已经包含了A和B的权限内路由.
虽然我们通过beforeEach可以控制访问权限,但是路由规则中却存在权限外的路由.这样不太严谨,而且也可能出现安全问题.
因此,我们需要在登录成功,添加动态路由前,清除动态添加的路由.
但是,目前为止我并没有查到相关资料能够清除动态添加的路由,如果哪位大佬知道,恳请在评论中告知一下,大家一起学习.
既然没有办法清除动态添加的路由,那么就重新实例化.
1. 重新加载页面
2. 重新将当前路由重新实例化 new Router()
第一种方法那么就是在退出的时候location.reload().但是SPA的话, 首次加载速度一直是一个棘手的问题, 如果没有什么影响的话,项目不大, 可以采取这种形式, 但不推荐.
第二种方法则是在addRoutes之前,将当前router实例重新实例化
// 给Router原型链注册reset函数以重新实例化
// 以便重新生成的Router实例也能够进行重新实例化
import Vue from 'vue'
import Router from 'vue-router'
Router.prototype.reset = function () {
var that = this
that = new Router({
routes: that.options.routes // 新的路由也需要包含静态路由
})
}
Vue.use(Router)
const router = new Router({
routes: [...]
})
export default router
Login.vue:
methods: {
handleLogin () {
...
//后台返回登录成功后
this.$router.reset()
this.$router.addRoutes(...)
}
}
好了,这样一来登入登出就完了
按照以上的思路,权限控制基本上就完成了,但是还有个棘手也是必须解决的问题:页面刷新
假如我在浏览权限路由页面当中,不小心碰到了f5, 那么整个路由重新实例化, 这个时候能通过权限控制, 但是路由并没有注册. 因此我是无法访问到权限控制的路由页面.
那么我的解决思路是:
在main.js中获取路由权限数据并动态注册,这样一来重新刷新的时候, 就能匹配到相对应的路由
// 伪代码
router.addRoutes(req.get('/routes'))
分享一下写的比较好的文章:
用addRoutes实现动态路由
vue实现登录权限