preface
Recently, I did a government project, and there was a very serious security BUG in the security test. The BUG is described as follows:
- You can enter an address in the address bar to access pages that are not authorized by the login user.
- You can modify the function permissions cached in the Local Storage to display the function permissions of non-login users.
Of course, these security issues in the back end to do permission interception is the best, but the company’s back end is lazy do not want to do, so we have to front-end to achieve real permission. The problem of direct operation through the interface is out of my consideration.
The techniques used in this column are the router.addRoutes instance method in Vue Router and the router.beforeEach navigation guard, action in Vuex. If you are not familiar with these usages, it is recommended to read the documentation on the official website.
How to cache permission data globally
Since the browser cache is no longer available, the only way to cache permissions globally is to use Vuex. Let’s create a Vuex to store permissions.
1. Install Vuex
If you are using the Vue CLI to build a Vue project and install it by default, you will usually install Vuex. If no, run CMD NPM install vuex –save to install vuex.
Create the store folder in the project SRC folder, create the Modules folder and the index.js file inside, introduce Vuex in the index.js file, and create a store to expose it.
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const debug = process.env.NODE_ENV ! == 'production'; const store = new Vuex.Store({ strict: debug, state: { }, getters: { }, mutations: { }, actions: { }, }); export default store;Copy the code
Vuex.Store constructor option strict can make vuex. Store enter strict mode, in strict mode, any modification of Vuex state outside the mutation processing function will throw an error, so in the production environment set to false to close. Set this to true in the development environment. The mutation processing functions can only be committed using the instance method COMMIT. These basic API knowledge can be read by the official documentation.
2. Create Vuex sub-module Module
The store in the index.js file is the store of the main module. If all the state of the application is concentrated in the main module, when the application becomes very complex, the store object of the main module will become quite bloated and difficult to maintain.
To solve these problems, Vuex allows us to split the Store into modules. Each module has its own state, mutation, action, and getter.
Create the prower.js and userinfo.js files in the modules folder. Create a store inside it and export it.
const state = {};
const getters = {};
const mutations = {};
const actions = {};
export default {
state,
getters,
mutations,
actions,
}
Copy the code
Then introduce these modules in index.js.
The traditional approach is as follows
import prower from './modules/prower';
import userInfo from './modules/userInfo';
const store = new Vuex.Store({
//...
modules: {
prower,
userInfo,
}
});
Copy the code
A lazy approach would be to automate the introduction of these modules using require.context in the WebPack API.
Require. context is used to get a specific context. You can pass three parameters to require.context: a directory to search for, a flag to indicate whether subdirectories should also be searched, and a regular expression to match files.
Get the modules folder context with const require_modules = require.context(‘./modules’, false, /\.js$/).
therequire.context
Returns a function assigned to a constantrequire_modules
, it has three attributes, wherekyes()
Property is a function that returns an array that is printed out on the console as["./prower.js", "./userInfo.js"]
. Pass in the subtermrequire_modules
The return function executes, for examplerequire_modules('./prower.js')
, as shown in the following figure.
It can be seen that the content of the default attribute is the content exported by each sub-module. Add them to vuex.store constructor option modules to introduce these modules. The specific implementation code is as follows:
import camelCase from 'lodash/camelCase'; const require_modules = require.context('./module', false, /\.js$/); const modules = {}; require_modules.keys().forEach(item => { const fileName = item.slice(2, -3); const module_name = camelCase(fileName); const module_config = require_modules(item).default; modules[module_name] = { ... module_config, }; }); const store = new Vuex.Store({ //... modules: { ... modules, } }); export default store;Copy the code
3. Access and processing of permission data
When the browser cache is available, the access and processing of permission data are generally performed in the login page, and then the permission data is cached in the browser Local Storage, so that the permission data can be obtained in each module of the project.
When browser caching is not possible, permission data should be available in each module of the project. Therefore, permission data should be cached in Vuex. However, Vuex has a disadvantage that all data stored in Vuex will be lost once the page is refreshed. So every time you refresh the page, retrieve the permission data and process it.
The logic of obtaining and processing permission data should be written in a globally accessible place, whereas in Vuex actions can be performed asynchronously. You can also call the XXXX method of actions with this.$store.dispatch(‘XXXX’,data) in each module of the project.
The processed permission data is stored in the state of Vuex, and getters are defined for external use of permission data.
Mutations can only be submitted through COMMIT in actions to set the data in state, so it is necessary to define mutations, and the code implementation is as follows, which is written in the prower.js file.
Const state = {menuPower: [],// menuPowerMap: {},// menuUrl: ApiPower: {},// function permission map, key is function path}; const getters = { menuPower: state => { return state.menuPower; }, menuPowerMap: state => { return state.menuPowerMap; }, menuUrl: state => { return state.menuUrl; }, apiPower: state => { return state.apiPower; }}; const mutations = { SET_MENUPOWER(state, data) { state.menuPower = data; }, SET_MENUPOWERMAP(state, data) { state.menuPowerMap = data; }, SET_MENUURL(state, data) { state.menuUrl = data; }, SET_APIPOWER(state, data) { state.apiPower = data; }}; const actions = { GET_ROLE({commit}, data) { } }; export default { state, getters, mutations, actions, }Copy the code
GET_ROLE in actions to write the logic for obtaining and processing permission data. Authority data is generally tree structured data, and authority data is generally processed recursively.
Get permission data by calling the interface in GET_ROLE
import { getRole } from 'api/common';
const actions = {
GET_ROLE({commit}, data) {
return new Promise((resolve, reject) => {
getRole()
.then(res => {
if (res.code == 200) {
resolve()
}
})
.catch(err => {
reject()
})
})
}
}
Copy the code
1. Obtain the function permission map
Since there are menu permissions and function permissions in the permission data, the function permission data is separated first, and the implementation code is shown as follows:
function getApiPower(data, result) {
data.forEach(item => {
if (item.type == 2 && item.url) {
result[item.url] = true;
}
if (item.children && Array.isArray(item.children) && item.children.length) {
getApiPower(item.children, result)
}
})
}
Copy the code
Use result to store the functional permission map. ForEach traverses permission data, result[item.url] = true if type = 2 and URL exists, to form a map set whose key is function path and value is true. GetApiPower (item.children, result) is then executed when children exist, making a recursive call. Result [item.url] = true is iterated layer after layer through the permission data, resulting in a map of functional permissions.
2. Get the menu path map
The implementation code is as follows:
function getMenuUrl(data, result) {
data.forEach(item => {
if (item.type == 1 && item.url) {
result[item.url] = true;
}
if (item.children && Array.isArray(item.children) && item.children.length) {
getMenuUrl(item.children, result)
}
})
};
Copy the code
Use result to store the functional permission map to prevent permission data from being changed. ForEach traverses permission data, result[item.url] = true if type = 1 and URL exists, to form a map set whose key is menu path and value is true. GetMenuUrl (item.children, result) is then executed to determine the existence of children, which constitutes a recursive call. Result [item.url] = true is iterated layer by layer through the permission data, resulting in a map of menu paths.
3. Get the menu permission data
The implementation code is as follows:
function getMenuPower(data) {
let menuPower = '';
menuPower = data.filter(item => {
if (item.type == 1) {
if (item.children && Array.isArray(item.children) && item.children.length) {
item.children = getMenuPower(item.children);
}
return true
}
})
return menuPower
};
Copy the code
Filter menu permission data with item.type == 1 as condition. The first layer of permission data processing is very simple, a filter is done, but each of the parent permissions have child permissions, put in children, that filter traversal children, after completion of the traversal value is re-assigned to children. Item. children = getMenuPower(item.children) {item.children = getMenuPower(item.children); This creates a recursive call, layer by layer through the permission data to filter out the menu permission data.
4. Sorting of menu permission data
Menu permission data generally have the order field, so to sort, the implementation code is as follows:
function sortMenuPower(data) {
data.sort((a, b) => {
if (a.sort < b.sort) {
return -1;
} else if (a.sort == b.sort) {
return 0;
} else {
return 1;
}
})
data.forEach(item => {
if (item.children && Array.isArray(item.children) && item.children.length) {
sortMenuPower(item.children)
}
})
};
Copy the code
SortMenuPower (item.children) is executed when children are judged to exist, which constitutes a recursive call, traversing menu permission data layer by layer for sorting.
5. Obtain the menu permission map
Finally, process the menu permission data and get the menu permission map, whose key is the menu permission ID.
function handleMenuPowerMap(data, result) {
data.forEach(item => {
result[item.authNodeId] = item;
if (item.children && Array.isArray(item.children) && item.children.length) {
handleMenuPowerMap(item.children, result)
}
})
};
Copy the code
Result [item.url] = true; result[item.url] = true; result[item.url] = true; HandleMenuPowerMap (item.children, result) is then executed when children exist, which constitutes a recursive call. Result [item.authNodeId] = item is iterated over the permission data layer by layer, and a map of menu permissions is created at the end of the iteration.
6. Process permission data in GET_ROLE Action
import { getRole } from 'api/common';
const actions = {
GET_ROLE({commit}, data) {
return new Promise((resolve, reject) => {
getRole()
.then(res => {
if (res.code == 200) {
if (res.data) {
let apiPower = {};
getApiPower(res.data, apiPower);
commit('SET_APIPOWER', apiPower);
let menuUrl = {};
getMenuUrl(res.data, menuUrl);
commit('SET_MENUURL', menuUrl);
let menuPower = {};
menuPower = getMenuPower(res.data);
sortMenuPower(menuPower);
commit('SET_MENUPOWER', menuPower);
let menuPowerMap = {};
handleMenuPowerMap(menuPower, menuPowerMap);
commit('SET_MENUPOWERMAP', menuPowerMap);
resolve()
} else {
reject('none');
}
}
})
.catch(err => {
reject()
})
})
}
}
Copy the code
How to dynamically add routes
One of the bugs is that you can enter an address in the address bar to access a page outside the menu permission of the logged-in user. This is because the route is created by static route. If you know the page address, you can enter the address in the browser to access the page directly. The ability to access static pages is also a security risk, although the back end is restricted from getting any data.
The above BUG can be solved with dynamic routing, which is implemented with router.addroutes, an instance method of Vue Router, whose parameter must be an array that meets the requirements of routes option.
Note that the 404 route cannot be added to the static route file, but must be added dynamically through router.addroutes. The specific implementation code is as follows:
import routeData from 'router/routeData'; const actions = { GET_ROLE({commit}, data) { return new Promise((resolve, reject) => { getRole() .then(res => { if (res.code == 200) { //... const noFind = { path: '*', redirect: { path: '/404' } } routeData['/layout'].children = []; For (let k in menuUrl) {if (k == 'parent path ') {routeData['/layout'].children. Push (routeData[' parent path ']); RouteData ['/layout'].children. Push (routeData[' children ']); }else { if (routeData[k]) { routeData['/layout'].children.push(routeData[k]) } } } let layout = routeData['/layout']; const routes = [layout, noFind]; vm.$router.options.routes.push(... routes); vm.$router.addRoutes(routes); resolve() } else { reject('none'); } }) .catch(err => { reject() }) }) } }Copy the code
Where, routeData is a routing rule with the routing path as the key and the value as a routing rule. The specific structure is as follows:
function load(component) { return () => import(`views/${component}`) } const routeData = { '/layout':{ path: '/layout', name: 'layout', component: load('layout'), children:[] }, '/home': { path: '/home', name: 'home', component: Load ('home'), meta: {title: 'home'},}, 'parent routing path ': {path:' parent routing path ', name: 'parent routing name ', Component: Load (' parent page file path '),}, 'child routing path ': {path:' child routing path ', name: 'Child routing name ', Component: Load (' sub-page file path '),},} export default routeData;Copy the code
The routing path is the key because the routing path is fixed. If you modify the routing path in the system permission configuration, you also need to modify the routing path in the code, compile, package and re-publish the routing path.
Run routeData[‘/layout’]. Children = [] to delete the original route before adding a route.
Execute const routes = [Layout, noFind] to add the route data as an array because router.addroutes can only receive an array.
Implement vm. $router. Options. Routes. Push (… routes); vm.$router.addRoutes(routes); To dynamically add routes.
In addition, configure a static route as the default route as follows:
function load(component) { return () => import(`views/${component}`) } const routes = [ { path: '/404', name: '404', Component: Load ('404'), meta: {title: '404 page '}}, {path: '/', name: 'login', Component: Load ('login'), meta: {title: 'login'}},]; export default routes;Copy the code
You can configure routes for pages that do not require permission control, such as 400 pages, login pages, and so on.
Time to obtain permission data and dynamically add routes
Vuex: GET_ROLE (‘GET_ROLE’); Vuex: GET_ROLE (‘GET_ROLE’); And because a Promise is created to monitor the execution result state, it can be realized to jump to the home page after the permission data is obtained and the dynamic route is added, the implementation code is as follows:
this.$store.dispatch('GET_ROLE').then(res => {
this.$router.push({
path: '/home'
})
})
Copy the code
This.$store.dispatch(‘GET_ROLE’) is executed after each page refresh to retrieve permission data and dynamically add route. Each page refresh will re-enter route.
router.beforeEach((to, from, next) => { if (to.path == '/' || to.path == '/404') { next(); } else { setTimeout(() => { const menuUrl = vm.$store.getters.menuUrl; if (JSON.stringify(menuUrl) === '{}') { vm.$store.dispatch('GET_ROLE'); } next(); }}}, 100));Copy the code
To access a page in a static route, run next(). In addition, setTimeout should be used to perform an asynchronous operation to obtain the Vue instantiation object VM, and determine whether menuUrl in Vuex is an empty object. If it is an empty object, execute vm.$store.dispatch(‘GET_ROLE’) to obtain permission data again and dynamically add routes. Finally, execute next() to jump.
4. Use of permission data
As for how the menu permission data is rendered as a header or side menu bar, there are different ways to write it using different UI components, which I won’t cover here.
As for the functional permission data, since it is used on every page, the permission data is introduced with global blending and written in the index.js file in the mixins folder
import { mapGetters } from 'vuex'; export default { computed: { ... mapGetters(['apiPower']), }, }Copy the code
In the page, the following sample code is used in this way
<el-button type="primary" @click="handleAdd" v-if="apiPower "> </el-button>Copy the code
Five, the subsequent
In fact, there is a way, is to use AES encryption rights and other related data (recommend the use of crypto-JS plug-in), in the cache to the browser, each time read these data decrypted. This approach, however, is less secure than what this column describes.