Vue routing permission control

sky124380729 2020-11-12 15:36:19
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]所创,转载请带上原文链接,感谢

  1. [front end -- JavaScript] knowledge point (IV) -- memory leakage in the project (I)
  2. This mechanism in JS
  3. Vue 3.0 source code learning 1 --- rendering process of components
  4. Learning the realization of canvas and simple drawing
  5. gin里获取http请求过来的参数
  6. vue3的新特性
  7. Get the parameters from HTTP request in gin
  8. New features of vue3
  9. vue-cli 引入腾讯地图(最新 api,rocketmq原理面试
  10. Vue 学习笔记(3,免费Java高级工程师学习资源
  11. Vue 学习笔记(2,Java编程视频教程
  12. Vue cli introduces Tencent maps (the latest API, rocketmq)
  13. Vue learning notes (3, free Java senior engineer learning resources)
  14. Vue learning notes (2, Java programming video tutorial)
  15. 【Vue】—props属性
  16. 【Vue】—创建组件
  17. [Vue] - props attribute
  18. [Vue] - create component
  19. 浅谈vue响应式原理及发布订阅模式和观察者模式
  20. On Vue responsive principle, publish subscribe mode and observer mode
  21. 浅谈vue响应式原理及发布订阅模式和观察者模式
  22. On Vue responsive principle, publish subscribe mode and observer mode
  23. Xiaobai can understand it. It only takes 4 steps to solve the problem of Vue keep alive cache component
  24. Publish, subscribe and observer of design patterns
  25. Summary of common content added in ES6 + (II)
  26. No.8 Vue element admin learning (III) vuex learning and login method analysis
  27. Write a mini webpack project construction tool
  28. Shopping cart (front-end static page preparation)
  29. Introduction to the fluent platform
  30. Webpack5 cache
  31. The difference between drop-down box select option and datalist
  32. CSS review (III)
  33. Node.js学习笔记【七】
  34. Node.js learning notes [VII]
  35. Vue Router根据后台数据加载不同的组件(思考->实现->不止于实现)
  36. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  37. 【JQuery框架,Java编程教程视频下载
  38. [jQuery framework, Java programming tutorial video download
  39. Vue Router根据后台数据加载不同的组件(思考->实现->不止于实现)
  40. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  41. 【Vue,阿里P8大佬亲自教你
  42. 【Vue基础知识总结 5,字节跳动算法工程师面试经验
  43. [Vue, Ali P8 teaches you personally
  44. [Vue basic knowledge summary 5. Interview experience of byte beating Algorithm Engineer
  45. 【问题记录】- 谷歌浏览器 Html生成PDF
  46. [problem record] - PDF generated by Google browser HTML
  47. 【问题记录】- 谷歌浏览器 Html生成PDF
  48. [problem record] - PDF generated by Google browser HTML
  49. 【JavaScript】查漏补缺 —数组中reduce()方法
  50. [JavaScript] leak checking and defect filling - reduce() method in array
  51. 【重识 HTML (3),350道Java面试真题分享
  52. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  53. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  54. [re recognize HTML (3) and share 350 real Java interview questions
  55. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  56. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  57. 【重识 HTML ,nginx面试题阿里
  58. 【重识 HTML (4),ELK原来这么简单
  59. [re recognize HTML, nginx interview questions]
  60. [re recognize HTML (4). Elk is so simple