系统权限按需访问路由几个完整方案(含addRoutes的填坑)

前言

当你的系统须要作权限验证时,每每有一个很常见的需求:系统的某些页面或者资源(按钮、操做等),须要该用户有对应的权限才能可见可用。javascript

这就涉及到如何根据用户的权限来判断可否进入某个路由页面的问题了。vue

网上有不少零散的方案,并无横向对比几种方案,且不少细节没解释到位,此处提供完整的几个方案流程,并总结优缺点,你可自行选择java

本篇是针对vue-router来讲明如何实现。git

解决方案

根据各类资料,这里分为三种解决方案来分别描述其优缺点。github

beforeEach中限制

你能够注册所有路由,在router.beforeEach中即进入路由前进行判断,即将进入的路由是有权限进入,不能的话手动重定向到某个静态路由(不须要权限就能进入的页面,即任何用户都能进入的页面,如404页或首页)vue-router

因为每一个系统的权限方案不同,判断条件也不同,这里就仅仅简单举个例子,万变不离其宗,但愿你们触类旁通,举一反三。vuex

咱们在router.beforeEach判断是否有权限进入,须要有三点:api

  1. 在路由配置中作标识,告知该路由须要的权限
  2. 须要一处地方记录该用户所拥有的权限信息
  3. router.beforeEach结合第1点和第2点进行判断

1)路由配置中作标识

假设项目的权限是用ID来表示,即每一个权限,用一个ID值来表示。数组

我采用路由配置的props项来作标识,authorityId值表示权限对应的ID值。bash

import Vue from 'vue';
import Router from 'vue-router';

import home from 'home.vue';
import exam1 from 'example1.vue';
import exam2 from 'example2.vue';

Vue.use(Router);

const routes = [
    {
        path: '/',
        component: home
    },
    {
        path: '/exam1',
        component: exam1,
        props: {
            authorityId: 100
        }
    },
    {
        path: '/exam2',
        component: exam2,
        props: {
            authorityId: 200
        }
    }
];

const router = new Router({
    routes
});

export default router;
复制代码

上面是设置路由的主文件,从中咱们看到,两个页面分别有两个不一样的权限ID值,要可以进入页面,就得拥有这两个权限。

若是看过个人这篇文章 如何写出一个利于扩展的vue路由配置 ,就知道我喜欢按照功能模块来把路由配置细分不少个模块,若是你是按功能模块区分权限,即一个功能模块下好多个页面都是一个权限ID,那么能够在routes数组的最后统一加上authorityId,而不用一个个都写,累赘!

routes.forEach(item => {
    item.props = {
        ...item.props,
        authorityId: 100
    };
});
复制代码

2)存储权限信息

接着咱们要找个地方来存储一下用户所拥有的权限信息,若是你对用户的权限信息是保存在持久化的一个地方如sessionStorage、localStorage、cookie或url中的话,刷新后还能继续能拿到这些值,那么再根据这些值控制路由访问,这是没多大问题的。可是,这种重要的信息就暴露在外面?万一别人恶心修改了,把本身不能访问的权限改为能够访问呢?

所以上述方法是不建议的。

我通常会存在vuex中,那么存在这里的话,就会面临刷新页面了,vuex的信息也会丢失的问题。

为了解决这个问题,咱们一样须要保存一些信息到持久化的一个地方中,可是与上面不一样的是,咱们不要直接保存权限信息,而是保存一些能发请求获取权限信息的信息,常见的如用户id等。刷新后,根据保存的这些信息发请求从新获取权限信息并存储。

如这里的例子我就设置sessionStorage.setItem('userId', 1012313);

如下为存储权限信息的vuex内容:

// authority.js

import * as types from '../mutation-types';

// state
const state = {
    // 权限id值数组,null为初始化状况,若是为[]表明该用户没有任何权限
    rights: null
};

// getters
const getters = {
    rights: state => state.rights
};

// actions
const actions = {
    /** * 设置用户访问权限 */
    setRights ({ commit }, value) {
        commit(types.SET_RIGHTS, value);
    }
};

// mutations
const mutations = {
    [types.SET_RIGHTS] (state, value) {
        state.rights = value;
    }
};

export default {
    state,
    getters,
    actions,
    mutations
};
复制代码

这里值得一提的是,为何rights默认值是null而不是[],缘由是用来区分是初始化状态仍是真的无任何权限状态。这个有使用场景,特别是针对刷新页面。

就是当你目前在一个非权限路由页面上时,若是你刷新了页面,用户的鉴权还有效,理应仍是停留在这个动态路由的页面。

那你怎么判断如今是因为刷新了页面呢,就是经过判断rightsnull而不是[],若是rights初始值自己就是[]的话,这是没法判断出来的。

那么为null是有两种状况的:

  • 从空tab或别的网站进入到你的网站(如输入url、sso登陆跳转过来);
  • 刷新页面

因此为了进一步区分是刷新行为,则需进一步经过判断sessionStorage里有没有登录后存储的userId信息,由于若是userId存在了表明登陆了,登陆了就会进行权限的设置,就天然rights会有值,就算没权限也会是个[]

上面讨论的这些判断行为,都会在router.beforeEach中体现应用到。

3)判断是否有权限进入路由

仍是在路由主文件中,在全局前置守卫中作判断。

import store from '/store';

/** * 检查进入的路由是否须要权限控制 * @param {Object} to - 即将进入的路由对象 * @param {Object} from - 来自的路由对象 * @param {Function} next - 路由跳转的函数 */
const verifyRouteAuthority = async (to, from, next) => {
    // 获取路由的props下的authorityId信息
    const defaultConfig = to.matched[to.matched.length - 1].props.default;
    const authorityId = (defaultConfig && defaultConfig.authorityId) ? defaultConfig.authorityId : null;

    // authorityId存在,表示须要权限控制的页面
    if (authorityId) {
        // 获取vuex中存储权限信息的模块,authority为该模块名
        const authorityState = store.state.authority;
        // 为null的场景: 从空tab或别的网站进入到eod(如输入url、sso登陆跳转过来);刷新页面;
        if (authorityState.rights === null) {
            const userId = sessionStorage.getItem('userId');
            // 若是是刷新了致使存储的权限路由配置信息没了,则要从新请求获取权限,判断刷新页是否拥有权限
            if (userId) {
                // 从新获取权限,如下为例子
                const res = await loginService.getRights();
                store.dispatch('setRights', res);
            } else { // 若是是非当页刷新,则跳转到首页
                next({ path: '/' });
                return true;
            }
        }

        // 若是是要进行权限控制的页面,判断是否有对应权限,无则跳转到首页
        if (!authorityState.rights.includes(authorityId)) {
            next({ path: '/' });
            return true;
        }
    }

    return false;
};

/** * 能进入路由页面的处理 */
const enterRoute = async (to, from, next) => {
    // 进行权限控制校验
    const res = await verifyRouteAuthority(to, from, next);
    // 若是通不过检验已进行内部跳转,则退出该流程
    if (res) {
        return;
    }

    // 进行登陆验证以及获取必要的用户信息等操做
    // ...
};

router.beforeEach((to, from, next) => {
    // 无匹配路由
    if (to.matched.length === 0) {
        // 跳转到首页 添加query,避免手动跳转丢失参数,例如token
        next({
            path: '/',
            query: to.query
        });
        return;
    }
    enterRoute(to, from, next);
});
复制代码

4)退出清空权限信息

完整的一个方案,别忘了还要针对登出,清空权限信息这步。也很简单,清空,意味着把rights从新置为null,所以执行store.dispatch('setRights', null);便可

小结

  • 优势:对于注册路由的处理没有额外的操做,全部处理逻辑集中在router.beforeEach中判断
  • 缺点:注册了多余的路由(但彷佛,没啥关系?)

刷新页面从新注册路由

这是一个极其简单粗暴的方式:

在网站vue app实例化时,router也初始化了,这时候只注册了静态路由(如登陆页、404页等不须要权限的页面),当用户登陆了以后,拿到用户的权限的接口,把这些权限的信息储存在某个持久化的地方如sessionStorage、cookie甚至url中,而后手动刷新页面location.reload,在建立路由实例时,拿到刚存的权限信息,而后才建立新的路由实例。

可能有人会问,为何不像上一个方案说的,只存储如userId这样的信息而不是直接存权限信息。若是存了userId,再经过请求获取权限信息,这是一个异步的过程,网站vue app实例化时,router也初始化了,很难找到一个时机在router初始化前就拿到权限的信息。

因为这种方式十分简单粗暴,我我的不喜欢,体验也很差,因此我仅提供思路,具体实现就不写代码了。

  • 缺点:容易泄露权限信息,便于别人恶意篡改,除非你能够作什么加密处理把,可是还要解密,挺麻烦的;多刷新了一次,用户体验很差。

addRoutes动态注册路由

目前vue-router 3.0要实现动态路由(即视状况注册路由),仅仅提供addRoutes一个api,在官方github中也有许多人提issue但愿新增一些其余实现动态路由的功能,如删除已注册路由替换同名路由等,可是维护者的回复大概意思是目前vue-router是以静态路由为主而设计的,不可能一会儿就考虑很全面,一步登天,给点时间后面在慢慢完善。

下面就说,在如此背景下,如何利用addRoutes来实现动态路由,以知足权限的变更

addRoutes函数说白了,就是用来追加路由注册的。最简单的思路是,当用户登陆到系统后,就根据用户的权限来追加注册TA能访问的路由。

可是,一套完整的方案,会有如下几个方面你须要考虑的:

  • 1)切换用户后,权限发生变化,注册的路由也应该要变化,理想状况是删除已注册的动态路由,而后才从新追加新路由。
  • 2)刷新页面时,若是用户鉴权还经过,那么其权限所容许的页面应该还能继续访问
  • 3)登出系统,即用户退出,须要清除已注册路由

针对问题一

上面也说了,目前vue-router不提供删除已注册路由的api,只有一个addRoutes能够动态改变注册路由,其接受一个参数,是个路由配置的数组。

那么若是不作处理,直接采用addRoutes追加注册,就会可能发生追加剧复路由的状况

例如用户1拥有 a,b 权限,用户2拥有 a,c 权限。当用户1登陆上了,此时路由已注册 a,b 权限对应的路由,而后用户1退出切换到用户2,经过addRoutes把 a,c 权限对应的路由追加注册了,这时候,就会重复注册了a路由,在控制台中会有警告信息。

其实若是路由都是彻底同样的话,不会影响到实际应用,用户也无是无感知的,只是路由变得累赘。可是若是假设同name的路由倒是对应不一样的页面路径,这时候我就会有问题了。

若是你知道存在有同名name路由,存在什么隐形后果,请告诉我。

所以,咱们须要找一个方案,解决可能添加剧复路由的问题。

有很多资料会让你在切换用户时,在跳转到登陆界面时,刷新一下页面,就会变回整个网站初始化的状况,即路由也从新初始化实例,这样登陆后就再用addRoutes追加路由就好。

其实上述方案不失为一个好方案,若是你不介意会刷新一下页面的话。甚至你的登陆界面就是跟系统不在一个单页面应用的话就更加不用手动刷新了(若有专门的单点登陆平台),天然就能在登陆后从新进入系统初始化了。

要说缺点的话:

  • 要从新刷新页面,若是系统网站自己初始化加载很慢的话,那么用户体验不好。
  • 若是你的系统权限方面比较复杂,像我开发的系统,权限不只仅在用户之间,在用户里,不一样任务下也有不一样权限,这时,就不能用这种方式了,由于切换任务并不会要从新登陆

若是你不喜欢上面这个简单的方案的话,不妨继续往下看

import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);

// 建立路由实例的函数
// 这里的staticRoutes表示你系统的静态路由
const createRouter = () => {
    return new Router({
        routes: staticRoutes
    });
};
/** * 重置注册的路由导航map * 主要是为了经过addRoutes方法动态注入新路由时,避免重复注册相同name路由 */
const resetRouter = () => {
    const newRouter = createRouter();
    router && (router.matcher = newRouter.matcher);
};

// 这是伴随vue app实例化的初始化路由实例
const router = createRouter();

export { resetRouter };
export default router;
复制代码

上面是建立路由的一份代码,除了resetRouter,其他部分跟你本来建立路由的代码并没有什么不一样。而resetRouter的做用就是解决重复问题的关键(router.matcher = newRouter.matcher),这句至关于重置了路由映射关系,抹去了已注册的路由映射关系,跟新的路由实例的映射同样。

所以,在每次经过addRoutes追加注册路由前,都要使用resetRouter方法来重置一下路由映射,再追加。可是这样仍然是不能百分百避免重复问题,为何呢?

以上述代码为例子,若是staticRoutes中有一个路由是拥有children子路由的,如

{
    path: '/tsp',
    name: 'TSP',
    component: TSP,
    children: [
        {
            path: 'analysis',
            name: 'analysis',
            component: Analysis
        }
    ]
}
复制代码

而后你要追加的路由恰好就是在这children中的子路由的话,你就须要追加整个nameTSP的路由了,这时就会发生重复追加已存在的TSPAnalysis的路由了。

为了不该问题,人为的约定,静态路由staticRoutes中不能是有可能被追加路由(包含子孙路由)的路由。

真要发生上面要追加在子路由的状况,那么把该TSP路由在初始化路由实例后,而后手动追加一次,伪装是静态路由,这样在使用resetRouter重置后就不包含TSP路由了,而后再追加这个TSP路由就不会警告重复了。

针对问题二

这个问题,咱们在第一个方案中也说过了,关于刷新带来的问题以及思考。

思路咱们有了,那么在代码的具体什么时机进行操做呢?因为须要进行异步请求,因此不适宜在路由实例初始化时进行,咱们在beforeEach中作处理,如下为例子(具体说明是注释中):

先看vuex中定义存储权限信息的关键代码

// authority.js

const state = {
    functionModules: null, // 功能模块权限id值数组,null为初始化状况,若是为[]表明该用户没有任何权限
};

const getters = {
    functionModules: state => state.functionModules
};

const actions = {
    /** * 设置用户所拥有的的功能模块访问权限 */
    setFunctionModules ({ commit, state }, value) {
        // ... 这里省略了实现代码,由于此节重点不在这,后面再详说
    }
};

const mutations = {
    // 设置用户所拥有的的功能模块访问权限
    [types.SET_FUNCTION_MODULES] (state, value) {
        state.functionModules = value;
    }
};
复制代码

下面是在beforeEach的处理逻辑

router.beforeEach((to, from, next) => {
    // 判断是否有匹配路由
    // 因为刷新了页面,路由从新初始化,只有静态路由被注册了,
    // 因此进入这个动态路由页面时,是找不到路由匹配项的
    if (to.matched.length === 0) {
        // 获取存储的用来获取权限信息的信息
        const userId = sessionStorage.getItem('userId');
        // 若是是刷新了致使存储的权限路由配置信息没了,则要从新请求获取权限,判断刷新页是否拥有权限
        // 这里的store.state.authority.functionModules是vuex中存在权限信息的state,是个数组
        // 刷新页面会变回初始值,例子中是null
        // 这个条件判断的目的是区分 1.用户胡乱输入根本不会存在的路由 2. 在某个动态路由上刷新了页面
        // functionModules为null,且保存了userId就表明是第二种状况,
        // 由于若是userId存在了表明登陆了,就天然functionModules会有值,就算没权限也会是个[]
        if (store.state.authority.functionModules === null && userId) {
            // 从新获取权限,如下为例子
            http.get('/rights').then(res => {
                // vuex中用于保存权限信息的action
                store.dispatch('setFunctionModules', res);
                router.replace(to);
            });
            return;
        }
        // 跳转到首页 添加query,避免手动跳转丢失参数,例如token
        next({
            path: '/',
            query: to.query
        });
        return;
    }
    // ... 其他的一些有匹配路由的操做
});
复制代码

针对问题三

登出系统,即用户退出,须要清除已注册的动态路由。因为问题二的解决,也须要清除在vuex中的存储信息。

这个问题其实没啥难度的,清空动态路由,用上述的resetRouter便可,清空vuex的信息就置为初始值就。

我为啥这里一提,就是为了提示你还有这么一个流程,别忘记了,一整套完整的方案不能漏了这个。

addRoutes的缺陷

上述基本已经描述完一整套实现动态路由的解决方案。可是有些小细节,能够注意一下,提升方案的全面性。

关于addRoutes的详细解释,官方文档也是简单一笔带过,实际动态注入路由是怎么一回事,你会不会以为注入后,咱们写配置里的routes选项值,就是添加了咱们追加的内容?很遗憾,并非这样的。

咱们在控制台上打印路由实例router,能够看到其下有个options属性,里面有个routes属性。这个就是咱们建立路由实例时的routes选项内容。咱们觉得经过addRoutes动态注册路由后,新注册的内容也会出如今这个属性里,但结果倒是没有。

$router.options.routes的内容只会是在建立实例时生成,后面追加的不会出如今这里。这意味着,在这个版本下的vue-router你无法经过路由实例对象来获知当前已注册的全部路由。假设你的系统有须要利用固然已注册的全部路由来转一些处理的话,你此时就没有这个数据了。所以,咱们要本身作一个备份,记录当前已注册的路由,以防不时之需。

咱们在刚才的vuex文件中存储这个已注册路由信息,并补充具体的setFunctionModules逻辑

// authority.js

import staticRoutes from '@/router/staticRoutes.js';

// 因为vuex的检查机制,不容许存在在mutation外部能改变state值的可能性(特别是赋值类型是数组或对象时),因此要深拷贝一下
const _staticRoutes = JSON.parse(JSON.stringify(staticRoutes));

const state = {
    functionModules: null,
    // 当前已注册的路由,由于经过addRoutes追加的路由不会更新到router对象上,须要本身作记录,以避免不时之需
    // _staticRoutes为系统的静止路由
    registeredRoutes: _staticRoutes
};

const getters = {
    functionModules: state => state.functionModules,
    registeredRoutes: state => state.registeredRoutes
};

const actions = {
    /** * 设置用户所拥有的的功能模块访问权限 */
    setFunctionModules ({ commit, state }, value) {
        // 若是和旧值同样,那么就不需从新注册路由
        // 这里举例的系统的权限信息是由一个个权限id组成的数组,因此用如下逻辑判断是否重复,具体项目具体实现
        if (state.functionModules) {
            const _functionModules = state.functionModules.concat();
            _functionModules.sort(Vue.common.numCompare);
            value.sort(Vue.common.numCompare);
            if (_functionModules.toString() === value.toString()) {
                return;
            }
        }
        // 若是没有任何权限
        if (value.length === 0) {
            resetRouter(); // 重置路由映射
            return;
        }
        // 根据权限信息生成动态路由配置
        // createRoutes函数不展开说明,具体项目具体实现
        const dynamicRoutes = createRoutes();
        resetRouter(); // 重置路由映射
        router.addRoutes(dynamicRoutes); // 追加权限路由
         // 因为vuex的检查机制,不容许存在在mutation外部能改变state值的可能性(特别是赋值类型是数组或对象时),因此要深拷贝一下
        const _dynamicRoutes = JSON.parse(JSON.stringify(dynamicRoutes));
        // 记录当前已注册的路由配置
        commit(types.SET_REGISTERED_ROUTES, [..._staticRoutes, ..._dynamicRoutes]);
        // 保存权限信息
        commit(types.SET_FUNCTION_MODULES, value);
    }
};

const mutations = {
    // 生成当前已注册的路由副本
    [types.SET_REGISTERED_ROUTES] (state, value) {
        state.registeredRoutes = value;
    },
    // 设置用户所拥有的的功能模块访问权限
    [types.SET_FUNCTION_MODULES] (state, value) {
        state.functionModules = value;
    }
};

export default {
    state,
    getters,
    actions,
    mutations
};
复制代码

对了,若是在VUEX中存储了当前注册路由信息的话,在问题三中,退出登陆,也要清除这个信息,把它置为默认状况,即只有静态路由的状况。

// 重置已注册的路由副本
[types.RESET_REGISTERED_ROUTES] (state) {
    state.registeredRoutes = _staticRoutes;
}
复制代码

还有一点可能须要知道:

若是经过addRoutes加入的新路由有在静态路由中的某个路由children中,那么$router.options.routes会更新上去。

小结

以上即为一个完整的动态加载路由的方案,这个方案中要注意的东西,要处理好的细节,都已一一说明了。

总结

三个方案都已经说明了,优缺点你们也能知道。没有说哪一个方案更好,甚至最好的方案,选择的标准就是:能知足你项目需求的,在你接受缺陷范围内的最简单的方案 ,这就是对你来讲最好的方案。

若是对你有帮助,可点赞支持下。

未经容许,请勿私自转载

相关文章
相关标签/搜索