preface

When your system requires permission authentication, there is a common requirement: certain pages or resources (buttons, actions, etc.) in the system need the corresponding permission to be visible and available.

This involves how to judge whether a routing page can be entered according to the user’s permission.

There are many scattered schemes on the Internet, and there is no horizontal comparison of several schemes, and many details are not explained in place. Here we provide a complete process of several schemes, and summarize the advantages and disadvantages, so that you can choose by yourself

This article is for vue-router to explain how to implement.

The solution

Based on various sources, there are three solutions to describe their advantages and disadvantages.

BeforeEach restrictions in

You can register all routes and determine before entering a route in router.beforeEach. The incoming route is entered with permission, if not, manually redirect to a static route (a page that can be accessed without permission, i.e. a page that any user can access, such as 404 or home page).

Because each system of authority scheme is not the same, the judgment conditions are not the same, here is just a simple example, all changes are inseparable from its ancestor, I hope we can draw inferential examples, by analogy.

We need three things to determine whether we have permission to enter in router.beforeEach:

  1. An identifier is added in the route configuration to inform the required permission of the route
  2. You need a place to record the permissions that the user has
  3. inrouter.beforeEachCombine points 1 and 2 to judge

1) Identify routes in the configuration

Assume that project permissions are represented by ids, that is, each permission is represented by an ID value.

I use the props item of the route configuration as the identification, and the authorityId value represents the ID value corresponding to the permission.

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;
Copy the code

There are two different permission ID values for each page. To enter the page, you need to have these two permissions.

If you have read my article on how to write a vUE routing configuration for expansion, you will know that I like to subdivide the routing configuration into many modules according to function modules. If you distinguish permissions by function modules, that is, many pages under a function module are the same permission ID. Add authorityId to the end of routes array.

routes.forEach(item => { item.props = { ... item.props, authorityId: 100 }; });Copy the code

2) Storage permission information

Then we need to find a place to store the user’s permissions. If your permissions are stored in a persistent place such as sessionStorage, localStorage, cookie, or URL, and you can still get those values after refresh, then we can control the routing access based on those values. That’s not a big problem. But is this important information out there? What if someone else changes it and changes the permission they can’t access to something they can access?

Therefore, the above method is not recommended.

I usually save in VUex, so if I save here, I will face the problem of refreshing the page, and vuex information will be lost.

In order to solve this problem, we also need to save some information in a persistent place, but different from above, we do not directly save permission information, but save some information that can request permission information, such as user ID, etc. After the refresh, send a request to obtain and store the permission information again based on the saved information.

In this example I set sessionStorage.setitem (‘userId’, 1012313);

The vuEX content of storage permission information is as follows:

// authority.js

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

// state
const state = {
    // Array of permission ID values, null indicates initialization, if [] indicates that the user does not have any permissions
    rights: null
};

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

// actions
const actions = {
    /** * Set user access permission */setRights ({ commit }, value) { commit(types.SET_RIGHTS, value); }};// mutations
constmutations = { [types.SET_RIGHTS] (state, value) { state.rights = value; }};export default {
    state,
    getters,
    actions,
    mutations
};
Copy the code

It is worth mentioning here that the default value of rights is null instead of []. The reason is to distinguish between the initialized state and the true no permission state. This has usage scenarios, especially for refreshing pages.

When you are currently on a non-permission routing page, if you refresh the page, the user’s authentication is still valid, and you should still stay on the dynamic routing page.

How can you tell if the page is refreshed? You can tell if the rights value is null instead of [], which is impossible if the initial value of rights itself is [].

There are two cases of null:

  • Enter your website from empty TAB or other websites (e.g. enter URL, sso login to jump to);
  • Refresh the page

Therefore, in order to further distinguish the refresh behavior, it is necessary to further determine whether there is userId information stored after login in sessionStorage, because if there is userId, it means login, then the permission will be set after login, and the natural rights will have a value, even if there is no permission, it will be [].

All of the judgment behaviors discussed above are reflected in router.beforeeach.

3) Check whether you have the permission to enter routes

Again, in the route master file, in the global front-guard.

import store from '/store';

/** * Check whether the incoming route requires permission control * @param {Object} to - incoming route Object * @param {Object} from - Incoming route Object * @param {Function} next - Route jump function */
const verifyRouteAuthority = async (to, from, next) => {
    // Get the authorityId information under the props of the route
    const defaultConfig = to.matched[to.matched.length - 1].props.default;
    const authorityId = (defaultConfig && defaultConfig.authorityId) ? defaultConfig.authorityId : null;

    // authorityId exists, indicating the page requiring permission control
    if (authorityId) {
        // Obtain the vuEX module for storing permission information. Authority is the name of the module
        const authorityState = store.state.authority;
        // Null scenario: enter a website from an empty TAB or other website (e.g. enter url, sso login to jump to); Refresh the page;
        if (authorityState.rights === null) {
            const userId = sessionStorage.getItem('userId');
            // If refresh results in the loss of the stored permission route configuration information, request permission again to determine whether the refresh page has permission
            if (userId) {
                // Get permission again, as shown in the following example
                const res = await loginService.getRights();
                store.dispatch('setRights', res);
            } else { // If the page is not current, jump to the home page
                next({ path: '/' });
                return true; }}// If the page is for permission control, check whether the page has the corresponding permission. If no, go to the home page
        if(! authorityState.rights.includes(authorityId)) { next({path: '/' });
            return true; }}return false;
};

/** * can enter the routing page processing */
const enterRoute = async (to, from, next) => {
    // Verify permission control
    const res = await verifyRouteAuthority(to, from, next);
    // If the internal jump fails, exit the process
    if (res) {
        return;
    }

    // Perform login authentication and obtain necessary user information
    // ...
};

router.beforeEach((to, from, next) = > {
    // There is no matching route
    if (to.matched.length === 0) {
        // Jump to the home page and add query to avoid manual jump to lose parameters, such as token
        next({
            path: '/'.query: to.query
        });
        return;
    }
    enterRoute(to, from, next);
});
Copy the code

4) Exit and clear permission information

A complete solution, do not forget to log out, clear permission information this step. It is also simple to empty, which means reset rights to NULL, so execute store.dispatch(‘setRights’, null); Can be

summary

  • Advantages: There is no additional operation for the processing of registered routes, and all processing logic is centralized inrouter.beforeEachIn the judgment
  • Disadvantages: Registered redundant routes.

Refresh the page and register routes again

This is a brutally simple way to do it:

When the website vue app is instantiated, the router is also initialized. At this time, only static routes are registered (such as login page, 404 page and other pages that do not require permissions). After the user logs in, the interface with the user’s permissions is obtained. Store these permission information in a persistent place such as sessionStorage, cookie or url, then manually refresh the page location.reload, when creating the route instance, get the newly saved permission information, and then create a new route instance.

One might ask why you don’t just store information like userId instead of permission information, as described in the previous scenario. If the userId is saved and the permission information is obtained through request, it is an asynchronous process. When the website vue APP is instantiated, the router is also initialized. It is difficult to find a time to get the permission information before the router is initialized.

Since this approach is very simple and crude, I personally don’t like it and the experience is not good, so I just provide ideas and don’t write code for the implementation.

  • Disadvantages: easy to divulge authority information, facilitate others maliciously tamper with, unless you can do what encryption processing, but also decryption, very troublesome; The user experience is not good.

AddRoutes dynamically registers routes

At present, vue-Router 3.0 only provides an API of addRoutes to implement dynamic routing (i.e. registering routes according to the situation). Many people hope to add some other functions to implement dynamic routing, such as deleting registered routes and replacing routes with the same name, etc. However, the maintainer’s reply probably means that vue-Router is mainly designed for static routes, so it is impossible to consider all aspects of the router in one step. It will be improved gradually after some time.

In this context, how can addRoutes be used to implement dynamic routing to meet the change of permissions

The addRoutes function is specifically used to append route registrations. The simplest idea is that when a user logs in to the system, it registers the routes that TA can access according to the user’s permission.

However, in a complete scheme, there are several aspects you need to consider:

  • 1) After the user is changed, the registered route should also be changed. Ideally, the registered dynamic route should be deleted before adding a new route.
  • 2) When the page is refreshed, if the user still passes the authentication, the page allowed by the user’s permission should still be accessible
  • 3) If the user logs out of the system, clear the registered routes

For question one

As mentioned above, vue-Router does not currently provide an API for deleting registered routes, only an addRoutes can dynamically change registered routes, which takes one parameter, an array of route configurations.

If no processing is done and addRoutes is used to append registration directly, the situation of appending duplicate routes may occur

For example, user 1 has rights A and B, and user 2 has rights A and C. When user 1 logs in, the route has registered the route corresponding to user A and user B. Then user 1 logs out, switches to user 2, and registers the route corresponding to user A and user C through addRoutes. At this time, route A will be registered repeatedly, and a warning message will be displayed in the console.

In fact, if all routes are the same, the actual application is not affected, and users are not aware of the routes, but the routes become redundant. But if I assume that a route with the same name corresponds to a different page path, then I have a problem.

If you know of a route with the same name, please tell me the hidden consequences.

Therefore, we need to find a solution to the problem of adding duplicate routes.

There are a lot of things that you can do when you switch users, when you jump to the login screen, refresh the page, and it will revert to the whole site initialization, that is, re-initialize the route instance, so that you can log in and then append the route with addRoutes.

It’s a good idea, if you don’t mind refreshing the page. Even if your login interface is not a single page application with the system, there is no need to manually refresh (such as a special single sign-on platform), naturally can re-enter the system initialization after login.

To speak of shortcomings:

  • To refresh the page, the user experience will be poor if the system website itself initializes slowly.
  • If you have complex system permissions, like the system I developed, where permissions are not only between users, but also between users for different tasks, then you can’t do this, because switching tasks doesn’t require re-logging in

If you don’t like this simple solution, read on

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

// A function to create a route instance
// staticRoutes here represents the static route of your system
const createRouter = (a)= > {
    return new Router({
        routes: staticRoutes
    });
};
/** * Resets the registered route navigation map * to avoid re-registering routes with the same name */ while dynamically injecting new routes through the addRoutes method
const resetRouter = (a)= > {
    const newRouter = createRouter();
    router && (router.matcher = newRouter.matcher);
};

// This is the initial route instance that accompanies vue app instantiation
const router = createRouter();

export { resetRouter };
export default router;
Copy the code

Above is the code for creating the route, except for resetRouter, which is no different from the code you would have created the route. Router.matcher = Newrouter.matcher resetRouter resets the route mapping and removes the registered route mapping, just like the new route instance mapping.

Therefore, the resetRouter method is used to reset the route map and append each time a route is appended via addRoutes. But it’s still not a hundred percent way to avoid repeating problems. Why?

Using the code above as an example, if there is a route in staticRoutes that has a children route, as in

{
    path: '/tsp',
    name: 'TSP',
    component: TSP,
    children: [
        {
            path: 'analysis',
            name: 'analysis',
            component: Analysis
        }
    ]
}
Copy the code

If the route you want to append happens to be the child route in the children group, you need to append the entire route with the name TSP, and repeat the append to the existing TSP and Analysis routes.

To avoid this problem, the static route staticRoutes cannot be a route that can be appended (including descendant routes) by artificial convention.

If it is true that the TSP route is appended to the child route, then the TSP route is appended manually after initializing the route instance, pretending to be a static route, so that the TSP route is not included after resetRouter is reset, and then the TSP route is appended again without warning.

For question two

This is a question that we talked about in the first scenario, about the problems and thinking about the refresh.

Now that we have the idea, what is the exact time in the code to do something? Asynchronous requests are not appropriate for route instance initialization because they need to be made asynchronously. We do this in beforeEach as an example:

Let’s start with the key code that defines the storage permission information in VUEX

// authority.js

const state = {
    functionModules: null.// Function module permission ID value array, null indicates initialization, if [] indicates that the user does not have any permission
};

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

const actions = {
    /** * Set the user's access to the function module */
    setFunctionModules ({ commit, state }, value) {
        / /... The implementation code is omitted here because that is not the focus of this section, but more on that later}};const mutations = {
    // Set the user's access permission to function modules[types.SET_FUNCTION_MODULES] (state, value) { state.functionModules = value; }};Copy the code

Here is the processing logic in beforeEach

router.beforeEach((to, from, next) = > {
    // Check whether there is a matching route
    // Because the page is refreshed, the route is reinitialized, only the static route is registered,
    // There is no route match when entering the dynamic routing page
    if (to.matched.length === 0) {
        // Get the information stored to get permission information
        const userId = sessionStorage.getItem('userId');
        // If refresh results in the loss of the stored permission route configuration information, request permission again to determine whether the refresh page has permission
        / / the store here. The state. Authorities. FunctionModules is vuex permissions exist in the state of information, is an array
        // Refreshing the page returns to its original value, null in this case
        // The purpose of this condition judgment is to distinguish between (1) the user enters a random route that does not exist (2). The page was refreshed on a dynamic route
        // functionModules is null and the userId is saved.
        // Because if userId exists to represent login, functionModules will naturally have a value, even if there is no permission will be a []
        if (store.state.authority.functionModules === null && userId) {
            // Get permission again, as shown in the following example
            http.get('/rights').then(res= > {
                // Action for vuex to save permission information
                store.dispatch('setFunctionModules', res);
                router.replace(to);
            });
            return;
        }
        // Jump to the home page and add query to avoid manual jump to lose parameters, such as token
        next({
            path: '/'.query: to.query
        });
        return;
    }
    / /... Others have operations that match routes
});
Copy the code

For question 3

When a user logs out of the system, you need to clear registered dynamic routes. Due to the resolution of problem two, the stored information in VUEX also needs to be cleared.

This problem is actually not difficult, empty the dynamic route, use the above resetRouter can be empty vuEX information set as the initial value.

Why I mention it here is to remind you that there is a process, don’t forget, a complete set of solutions can not miss this.

The defect of addRoutes

The above has basically described a complete set of solutions for implementing dynamic routing. But there are some small details that can be paid attention to to improve the comprehensiveness of the scheme.

The official documentation also gives a brief explanation of the addRoutes. What is the actual dynamic route injection? Do you think that after the injection, we write the value of the routes option in the configuration to add the content we added? Unfortunately, that is not the case.

The router has an options attribute and a Routes attribute in it. This is the content of the Routes option when we create the route instance. We thought that after dynamically registering the route with addRoutes, the newly registered content would also appear in this property, but it didn’t.

The content of $router.options.routes is only generated when the instance is created. This means that with vue-Router in this version you can’t tell all the routes currently registered from route instance objects. If your system needs to take advantage of all the routes that are of course registered for some processing, you don’t have this data at this point. Therefore, we need to make a backup of the current registered routes in case we need them.

We store this registered routing information in the vuex file we just saw, supplemented with the specific setFunctionModules logic

// authority.js

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

// Due to vuEX's checking mechanism, it is not possible to change the state value outside of mutation (especially if the assignment type is array or object), so a deep copy is required
const _staticRoutes = JSON.parse(JSON.stringify(staticRoutes));

const state = {
    functionModules: null.// The route that is currently registered is not updated to the Router object because of the route appended by addRoutes
    // _staticRoutes indicates the static route of the system
    registeredRoutes: _staticRoutes
};

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

const actions = {
    /** * Set the user's access to the function module */
    setFunctionModules ({ commit, state }, value) {
        // If the value is the same as the old value, there is no need to re-register the route
        // The system's permission information is an array of permission ids, so use the following logic to determine whether it is repeated, specific project specific implementation
        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 you do not have any permissions
        if (value.length === 0) {
            resetRouter(); // Reset the route mapping
            return;
        }
        // Generate dynamic routing configuration based on permission information
        // createRoutes function does not expand description, specific project specific implementation
        const dynamicRoutes = createRoutes();
        resetRouter(); // Reset the route mapping
        router.addRoutes(dynamicRoutes); // Add permission route
         // Due to vuEX's checking mechanism, it is not possible to change the state value outside of mutation (especially if the assignment type is array or object), so a deep copy is required
        const _dynamicRoutes = JSON.parse(JSON.stringify(dynamicRoutes));
        // Records the configuration of the registered routes
        commit(types.SET_REGISTERED_ROUTES, [..._staticRoutes, ..._dynamicRoutes]);
        // Save permission informationcommit(types.SET_FUNCTION_MODULES, value); }};const mutations = {
    // Generate a copy of the currently registered route
    [types.SET_REGISTERED_ROUTES] (state, value) {
        state.registeredRoutes = value;
    },
    // Set the user's access permission to function modules[types.SET_FUNCTION_MODULES] (state, value) { state.functionModules = value; }};export default {
    state,
    getters,
    actions,
    mutations
};
Copy the code

By the way, if VUEX stores the current registered route information, this information should also be cleared in question 3, when logging out, and it should be set to the default, that is, only static routes.

// Reset the registered route copy
[types.RESET_REGISTERED_ROUTES] (state) {
    state.registeredRoutes = _staticRoutes;
}
Copy the code

One more thing you might want to know:

$router.options.routes is updated if the new route added via addRoutes is in a children route in the static route.

summary

The above is a complete dynamic load routing scheme, this scheme to pay attention to things, to deal with the details, have been a clear.

conclusion

All three options have been explained, and you can see the pros and cons. There is no better solution, or even the best solution. The criteria for choosing a solution is: the simplest solution that meets the needs of your project and is within the range of defects you accept is the best solution for you.

If it helps you, you can like it.

Please do not reprint without permission