编程知识 cdmana.com

Vue routing permission control

Vue Route authority control

When we are doing the background management system , It will involve how the menu tree on the left side of the system is displayed dynamically . At present, it's basically RBAC Solutions for , namely Role-Based Access Control, Permissions are associated with roles , Users get access to these roles by becoming members of the appropriate roles . This greatly simplifies the management of permissions .

vue There are many excellent background management system templates , These open source projects provide RBAC The idea of authority control , But in the actual project , The way you write dead characters may not be appropriate .

I saw a lot of solutions on the Internet , Personal feelings have disadvantages , In many cases, the front end registers the complete routing table into the project first , Then through the background back to the tree filter display scheme , In fact, it just hides the menu on the left , But the route is still registered , The user guesses that the access path can still enter the page easily , It's not really done Dynamic route loading


Here is the solution I designed , My little white , For reference only -.-

Let's take a look at the completed system architecture , User binding role ( One to many ), Role binding menu ( One to many )

User menu

image

Select role

image

Role menu

image

Menu selection , Because this project is multi system , So there will be ADMIN and HMI Two subsystems , I'll explain later

image

Resource management ( I don't call it menu management here , Because it involves subsystems, I call it module , There is a menu under the module , There are buttons under the menu )

image
image

Okay , After reading a few pictures, you may also understand that this is a typical RBAC. How does the interior work

To achieve dynamic addition of routes , That is, only authorized routes can be registered to Vue In the example , The core is vue-router Of addRoutes And navigation hooks beforeEach Two methods
image.png
image.png
The general idea is , stay beforeEach In the method ( That is, make a judgment before each route jump ), If the routing table is already loaded , The routing table is registered in the instance , without , Then pull the routing table from the back end and register it into the instance . So why not load it once when you log in ? This is because if you only load once at login time , The registered routing table will be lost when the web page is refreshed , So we are unified in beforeEach This hook to achieve

// permission.js, This file is in main.js You can import it directly , The idea here is copied element-admin

import router from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import Cookies from 'js-cookie'
import screenfull from 'screenfull'

router.beforeEach(async (to, from, next) => {
    const token = Cookies.get('token')
    NProgress.start()
    if (token) {
        //  If you are already logged in , Jump to the login page and redirect to the home page 
        if (to.path === '/login') {
            next({ path: '/' })
            NProgress.done()
        } else {
            if (!store.state.authorized) {
                try {
                    router.addRoutes(await store.dispatch('setAccessRoutes'))
                    store.dispatch('setAllDict')
                    next({ ...to, replace: true })
                } catch (e) {
                    Cookies.remove('token')
                    Cookies.remove('userInfo')
                    next({ path: '/login' })
                    NProgress.done()
                }
            } else {
                next()
                //  The full screen parameter determines whether the page is full screen 
                if (!screenfull.isEnabled) return
                if (to.meta && to.meta.fullScreen) {
                    screenfull.request().catch(() => null)
                } else {
                    if (screenfull.isFullscreen) {
                        screenfull.exit()
                    }
                }
            }
        }
    } else {
        next(to.path !== '/login' ? { path: '/login' } : true)
    }
})

router.afterEach(() => {
    NProgress.done()
})

Because routing is dynamically registered , So the initial routing of the project will be simple , Just provide the basic routing , Other routes are dynamically registered after returning from the server

// router.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from 'modules/Login'
import NoPermission from 'modules/NoPermission'
Vue.use(Router)

// Fixed NavigationDuplicated Problem
const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onComplete, onAbort) {
    if (onComplete || onAbort) return originalPush.call(this, location, onComplete, onAbort)
    return originalPush.call(this, location).catch(err => err)
}

const createRouter = () =>
    new Router({
        mode: 'history',
        scrollBehavior: () => ({ y: 0 }),
        routes: [
            {
                path: '/',
                redirect: '/platform'
            },
            {
                path: '/noPermission',
                component: NoPermission
            },
            {
                path: '/login',
                component: Login
            }
        ]
    })

const router = createRouter()

export function resetRouter() {
    const newRouter = createRouter()
    router.matcher = newRouter.matcher // reset router
}

export default router

webpack Dynamic compilation was not supported before , So many projects maintain a mapping table in the routing table as follows :

const routerMap = {
    user: () => import('/views/user'),
    role: () => import('/views/role'),
    ...
}

I don't think it's very nice, the latest version vue-cli Integrated webpack It can support dynamic import , Therefore, all routing information can be put into the database for configuration , The front end is no longer in need of maintenance router Relation table of mapping , If you're the old version CLI, have access to dynamic-import

Let's take a look at this amazing document , You can also take a look at the following
// menu.json

// id: Whatever the rules , As long as it's unique , The front end here is dead ID It is not generated every time the database is imported, because if the database is regenerated every time, the previous association relationship will be lost 
// title: The title of the menu 
// name: Unique identification 
// type:'MD' For module ( Subsystem ),'MN' For the menu ,'BT' For buttons , If you need to control the button permissions, you need to configure to BT Level 
// icon: The menu icon 
// uri: The routing address of the menu 
// componentPath: The menu in the corresponding front-end project path , In the subsequent store.js You'll see the usage , It's not necessary to write a copy of routerMap
// hidden: Whether the menu is displayed on the left , Some menus, such as the details page of a list , It needs to be registered in the instance , But you don't need to show it in the left menu bar 
// noCache: Due to the project page added cache control , Therefore, this field is used to determine whether the current page needs to be cached 
// fullScreen: Some menus , When you enter, it will be displayed in full screen , For example, some large screen display pages , This field is configured by 
// children: Same as the above fields 

[
    {
        "id": "00b82eb6e50a45a495df301b0a3cde8b",
        "title": "SV ADMIN",
        "name": "ADMIN",
        "type": "MD",
        "children": [
            {
                {
                "id": "06f1082640a0440b97009d536590cf4f",
                "title": " System management ",
                "name": "system",
                "icon": "el-icon-setting",
                "uri": "/system",
                "componentPath": "modules/Layout",
                "type": "MN",
                "children": [
                    {
                        "id": "b9bd920263bb47dbbfbf4c6e47cc087b",
                        "title": " User management ",
                        "name": "principal",
                        "uri": "principal",
                        "componentPath": "views/system/principal",
                        "type": "MN",
                        "children": [
                            { "id": "b37f971139ca49ab8c6506d4b30eddb3", "title": " newly added ", "name": "create", "type": "BT" },
                            { "id": "d3bcee30ec03432db9db2da999bb210f", "title": " edit ", "name": "edit", "type": "BT" },
                            { "id": "7c2ce28dcedf439fabc4ae9ad94f6899", "title": " Delete ", "name": "delete", "type": "BT" },
                            { "id": "bdf4d9e8bf004e40a82b80f0e88c866c", "title": " Change Password ", "name": "resetPwd", "type": "BT" },
                            { "id": "ba09f8a270e3420bb8877f8def455f6f", "title": " Select role ", "name": "setRole", "type": "BT" }
                        ]
                    },
                    {
                        "id": "c47c8ad710774576871739504c6cd2a8",
                        "title": " Role management ",
                        "name": "role",
                        "uri": "role",
                        "componentPath": "views/system/role",
                        "type": "MN",
                        "children": [
                            { "id": "81c0dca0ed2c455d9e6b6d0c86d24b10", "title": " newly added ", "name": "create", "type": "BT" },
                            { "id": "19a2bf03e6834d3693d69a70e919d55e", "title": " edit ", "name": "edit", "type": "BT" },
                            { "id": "6136cc46c45a47f4b2f20e899308b097", "title": " Delete ", "name": "delete", "type": "BT" },
                            { "id": "ad5cf52a78b54a1da7c65be74817744b", "title": " The Settings menu ", "name": "setMenu", "type": "BT" }
                        ]
                    },
                    {
                        "id": "8b5781640b9b4a5cb28ac616da32636c",
                        "title": " Resource management ",
                        "name": "resource",
                        "uri": "resource",
                        "componentPath": "views/system/resource",
                        "type": "MN",
                        "children": [
                            { "id": "d4182147883f48069173b7d173e821dc", "title": " newly added ", "name": "create", "type": "BT" },
                            { "id": "935fcb52fffa45acb2891043ddb37ace", "title": " edit ", "name": "edit", "type": "BT" },
                            { "id": "3f99d47b4bfd402eb3c787ee10633f77", "title": " Delete ", "name": "delete", "type": "BT" }
                        ]
                    }
                ]
            },
            }
        ]
    },
    {
        "id": "fc8194b529fa4e87b454f970a2e71899",
        "title": "SV HMI",
        "name": "HMI",
        "type": "MD",
        "children": [
            { "id": "eb5370681213412d8541d171e9929c84", "title": " Start detection ","name": "001" },
            { "id": "06eb36e7224043ddbb591eb4d688f438", "title": " Equipment information ","name": "002" },
            { "id": "76696598fd46432aa19d413bc15b5110", "title": "AI model base ","name": "003" },
            { "id": "2896f3861d9e4506af8120d6fcb59ee1", "title": " Maintenance ","name": "004" },
            { "id": "91825c6d7d7a457ebd70bfdc9a3a2d81", "title": " continue ","name": "005" },
            { "id": "24694d28b2c943c88487f6e44e7db626", "title": " Pause ","name": "006" },
            { "id": "225387753cf24781bb7c853ee538d087", "title": " end ","name": "007" }
        ]
    }
]

The above is the routing configuration information of the front end , As mentioned before , The route is returned by the back end , Why is there a menu file on the front end ?

  • Because all the contents in the route are needed by the front end , For example, the chart displayed in the menu , The front-end path of the menu and so on ... Since it has a big relationship with the front end , So front end maintenance of the file is more suitable for , Instead of having the backend configure XML perhaps liquibase, Every time you change the menu, you should inform your background to update the database , Then when switching multiple environments, each background must notify ... Backstage may not be happy yet X you ... Then you want to change a small icon must be careful by your backstage big man hate ...

Question:

Since the front end has the file , Does it mean that the source code of routing is exposed again , What's the difference between that and other people's guess path can be accessed ? It's not like pulling menu information from the database , You and I use this directly json, How about the database ? Don't worry. ...

Answer:

  1. It's just the front end for mock Of To configure file ,build The content will not be packaged when .
  2. stay User role menu Before these connections were established , The menu can only be made through mock To establish the , At this time, read the front-end configuration file directly ... forehead , It's delicious , How to do this can be seen in the following store.js How to do it
  3. Menus are always maintained by the front end , When you need to go online , The front end can go through node take menu.json Generate SQL The statement is imported into the database . Later on

The next step is how to register these routing tables

// store.js

import Vue from 'vue'
import Vuex from 'vuex'
import Cookie from 'js-cookie'
import NotFound from 'modules/NotFound'
import { resetRouter } from '../router'
import { getUserResourceTree, getDictAllModel } from 'apis'
import { deepClone } from 'utils/tools'

//  Here IS_TESTING It is used to judge whether to pull the real menu of the database or directly use the front end menu.json, It's very useful before the relationship between resources has been established 
import { IS_TESTING } from '@/config'
import { Message } from 'element-ui'

Vue.use(Vuex)

//  Production of accessible routing tables 
const createRouter = (routes, cname = '') => {
    return routes.reduce((prev, { type, uri: path, componentPath, name, title, icon, redirectUri: redirect, hidden, fullScreen, noCache, children = [] }) => {
        //  If it is a menu item, it will be registered to route in 
        if (type === 'MN') {
            prev.push({
                path,
                //  Here is webpack Dynamic import , Is it right? so easy, Mom, don't worry. I'll write another one routerMap Put it in the source code 
                component: () => import(`@/${componentPath}`),
                name: (cname + '-' + name).slice(1),
                props: true,
                redirect,
                meta: { title, icon, hidden: hidden === 'Y', type, fullScreen: fullScreen === 'Y', noCache: noCache === 'Y' },
                children: children.length ? createRouter(children, cname + '-' + name) : []
            })
        }
        return prev
    }, [])
}

//  Production authority button list 
const createPermissionBtns = router => {
    let btns = []
    const c = (router, name = '') => {
        router.forEach(v => {
            v.type === 'BT' && btns.push((name + '-' + v.name).slice(1))
            return v.children && v.children.length ? c(v.children, name + '-' + v.name) : null
        })
        return btns
    }
    return c(router)
}

export default new Vuex.Store({
    state: {
        collapse: false, //  Whether the menu bar shrinks 
        authorized: false, //  Whether the authorization menu has been pulled 
        dict: {},
        accsessRoutes: [], //  Registered routes 
        permissionBtns: [], //  Button with permission 
        navTags: [], //  Tag navigation list 
        cachedViews: [] //  Cached pages 
    },
    getters: {
        collapse: state => state.collapse,
        cachedViews: state => state.cachedViews,
        accsessRoutes: state => state.accsessRoutes,
        //  menu bar ( To filter out hidden)
        menuList: state => {
            const filterMenus = menus => {
                return menus.filter(item => {
                    if (item.children && item.children.length) {
                        item.children = filterMenus(item.children)
                    }
                    return item.meta && !item.meta.hidden
                })
            }
            return filterMenus(deepClone(state.accsessRoutes))
        },
        navTags: state => state.navTags
    },
    mutations: {
        SET_ACCSESS_ROUTES(state, accsessRoutes) {
            state.authorized = true
            state.accsessRoutes = accsessRoutes
        },
        SET_ALL_DICT(state, dict) {
            state.dict = dict
        },
        SET_PERMISSION_BTNS(state, btns) {
            state.permissionBtns = btns
        },
        SET_COLLAPSE(state, flag) {
            state.collapse = flag
        },
        SET_CACHED_VIEWS(state, cachedViews) {
            state.cachedViews = cachedViews
        },
        //  Log out 
        LOGOUT: state => {
            state.cachedViews = []
            state.authorized = false
            resetRouter()
            Cookie.remove('token')
            Cookie.remove('userInfo')
        }
    },
    actions: {
        setAccessRoutes: ({ commit }) => {
            return new Promise(async (resolve, reject) => {
                // 404 Page selection is registered after dynamically adding routes , It's because if you start registering in the project , stay addRoutes After that, it will be limited to match the 404, cause BUG
                const routerExt = [
                    { path: '*', redirect: '/404' },
                    { path: '/404', component: NotFound }
                ]
                // getUserResourceTree This interface logic is to query the resources contained in the current login role , Filter out the module name ( Here is ADMIN) The following child node ( Contains menus and buttons )
                const res = await (IS_TESTING ? import('@/mock/menu.json') : getUserResourceTree('ADMIN'))
                if (!res) return reject()
                let router
                if (IS_TESTING) {
                    //  Here's the number one 0 Because my system is the first subsystem of a large system , On the menu menu.json You can see 
                    router = res[0].children
                } else {
                    if (!res.data.length) {
                        reject()
                        return Message.error(' The user does not configure the menu or the menu is not configured correctly , Please check and try again ~')
                    } else {
                        router = res.data
                    }
                }
                const accessRoutes = createRouter(router).concat(routerExt)
                commit('SET_ACCSESS_ROUTES', accessRoutes)
                commit('SET_PERMISSION_BTNS', createPermissionBtns(router))
                resolve(accessRoutes)
            })
        },
        setAllDict: async ({ commit }) => {
            if (IS_TESTING) return
            const res = await getDictAllModel()
            if (!res) return
            commit('SET_ALL_DICT', res.data)
        },
        logout: ({ commit }) => {
            return new Promise(resolve => {
                commit('LOGOUT')
                resolve()
            })
        }
    }
})

Okay , The last step is how to put menu.json Become a database of SQL, Then you can put IS_TESTING Change it to false, Really pull the menu of database

// createMenu.js
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const resolve = dir => path.join(__dirname, dir)
const format = (data = new Date(), fmt = 'yyyy-MM-dd') => {
    let o = {
        'M+': data.getMonth() + 1, //  month 
        'd+': data.getDate(), //  Japan 
        'h+': data.getHours(), //  Hours 
        'm+': data.getMinutes(), //  branch 
        's+': data.getSeconds(), //  second 
        'q+': Math.floor((data.getMonth() + 3) / 3), //  quarter 
        S: data.getMilliseconds() //  millisecond 
    }
    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (data.getFullYear() + '').substr(4 - RegExp.$1.length))
    }
    for (var k in o) {
        if (new RegExp('(' + k + ')').test(fmt)) {
            fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
        }
    }
    return fmt
}
//  The location of the exported file directory 
const SQL_PATH = resolve('./menu.sql')
//  export SQL Function of 
function createSQL(data, name = '', pid = '0', arr = []) {
    data.forEach(function(v, d) {
        if (v.children && v.children.length) {
            createSQL(v.children, name + '-' + v.name, v.id, arr)
        }
        arr.push({
            id: v.id,
            created_at: format(new Date(), 'yyyy-MM-dd hh:mm:ss'),
            modified_at: format(new Date(), 'yyyy-MM-dd hh:mm:ss'),
            created_by: 1,
            modified_by: 1,
            version: 1,
            is_delete: 'N',
            code: (name + '-' + v.name).slice(1),
            name: v.name,
            title: v.title,
            icon: v.icon,
            uri: v.uri,
            sort: d + 1,
            parent_id: pid,
            type: v.type,
            component_path: v.componentPath,
            redirect_uri: v.redirectUri,
            full_screen: v.fullScreen === 'Y' ? 'Y' : 'N',
            hidden: v.hidden === 'Y' ? 'Y' : 'N',
            no_cache: v.noCache === 'Y' ? 'Y' : 'N'
        })
    })
    return arr
}

fs.readFile(resolve('src/mock/menu.json'), 'utf-8', (err, data) => {
    const menuList = createSQL(JSON.parse(data))
    const sql = menuList
        .map(sql => {
            let value = ''
            for (const v of Object.values(sql)) {
                value += ','
                value += v ? `'${v}'` : null
            }
            return 'INSERT INTO `t_sys_resource` VALUES (' + value.slice(1) + ')' + '\n'
        })
        .join(';')
    const mySQL =
        'DROP TABLE IF EXISTS `t_sys_resource`;' +
        '\n' +
        'CREATE TABLE `t_sys_resource` (' +
        '\n' +
        '`id` varchar(64) NOT NULL,' +
        '\n' +
        "`created_at` timestamp NULL DEFAULT NULL COMMENT ' Creation time '," +
        '\n' +
        "`modified_at` timestamp NULL DEFAULT NULL COMMENT ' Update time '," +
        '\n' +
        "`created_by` varchar(64) DEFAULT NULL COMMENT ' founder '," +
        '\n' +
        "`modified_by` varchar(64) DEFAULT NULL COMMENT ' Updated by '," +
        '\n' +
        "`version` int(11) DEFAULT NULL COMMENT ' edition ( Optimism lock )'," +
        '\n' +
        "`is_delete` char(1) DEFAULT NULL COMMENT ' Logical deletion '," +
        '\n' +
        "`code` varchar(150) NOT NULL COMMENT ' code '," +
        '\n' +
        "`name` varchar(50) DEFAULT NULL COMMENT ' name '," +
        '\n' +
        "`title` varchar(50) DEFAULT NULL COMMENT ' title '," +
        '\n' +
        "`icon` varchar(50) DEFAULT NULL COMMENT ' Icon '," +
        '\n' +
        "`uri` varchar(250) DEFAULT NULL COMMENT ' route '," +
        '\n' +
        "`sort` int(11) DEFAULT NULL COMMENT ' Sort '," +
        '\n' +
        "`parent_id` varchar(64) DEFAULT NULL COMMENT ' Father id'," +
        '\n' +
        "`type` char(2) DEFAULT NULL COMMENT ' type '," +
        '\n' +
        "`component_path` varchar(250) DEFAULT NULL COMMENT ' Component path '," +
        '\n' +
        "`redirect_uri` varchar(250) DEFAULT NULL COMMENT ' Redirection path '," +
        '\n' +
        "`full_screen` char(1) DEFAULT NULL COMMENT ' Full screen '," +
        '\n' +
        "`hidden` char(1) DEFAULT NULL COMMENT ' hide '," +
        '\n' +
        "`no_cache` char(1) DEFAULT NULL COMMENT ' cache '," +
        '\n' +
        'PRIMARY KEY (`id`),' +
        '\n' +
        'UNIQUE KEY `code` (`code`) USING BTREE' +
        '\n' +
        ") ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=' resources ';" +
        '\n' +
        sql
    fs.writeFile(SQL_PATH, mySQL, err => {
        if (err) return console.log(err)
        console.log(chalk.cyanBright(` congratulations , establish sql Statement success , Location :${SQL_PATH}`))
    })
})
// package.json
"scripts": {
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "dev": "vue-cli-service serve",
    "menu": "node createMenu"
  },
Need generation SQL It's time to execute npm run menu That's it

For convenience , above SQL Yes, it will delete the resource table first and then create it again , Remember to back up before importing the database .

Is the whole process so easy?so easy?so easy?

After the background table is built, the front end will play by itself , Is self-sufficiency fragrant ?

I am a senior Xiaobai , What's more, I hope you can give me some advice ~

版权声明
本文为[sky124380729]所创,转载请带上原文链接,感谢

Scroll to Top