华为云POC的权限控制是如何做的?

4,172 阅读5分钟

最近一段时间一直投入于对接合作伙伴的POC项目,虽然不是主要的开发人员,但是我需要对整个项目进行把控,涉及到的一些核心功能点也需要我去做一个把关。我们这边需要做一个OCR 文字识别的自定义模板配置平台,本不涉及到用户权限这块的内容,奈何客户爸爸执意要加,那好吧,我们就来做一个权限控制吧......

权限控制设计

其实大体上权限设计都是通过前后台协作一起完成,这个过程我们细致的分为:api访问权限控制 和 页面权限控制,进一步的细粒度区分,页面权限控制又包含页面是否能访问、页面中的按钮权限等等。我们一起看看这块功能的实现吧。

api 访问权限控制

实际上就是对用户信息的校验。在用户登录时服务器需要给前台返回一个Token,以后前台每次调用接口时都需要带上这个Token,服务端获取到这个Token后进行比对,如果通过则可以访问。现有的通常做法就是在登陆成功之后将后台返回的 Token 存储在前端缓存中,例如 sessionStorage,在请求时将 Token 取出来放在请求头 headers 中传给后台。下图以一个正常的请求数据接口作为示例代码:

this.httpRequest({
          method: 'get',
          url: 'test/query?id=llz',
          withCredentials: true,
          headers: {
            token: sessionStorage.getItem('tokenKey')   // 每次请求时都在headers 塞入 Token 信息
          }
        }).then(res => {
          //请求成功后的操作
        })

后来axios 中可以在拦截器中直接塞入,作为全局传入,方便了很多(注意此时的Token 信息是同vuex 缓存中取到的,我们将在下一步展开)。

//main.js  

import axios from 'axios'

// 实例化Axios,并进行超时设置
const instance = axios.create({
    timeout: 5000
})
axios.defaults.baseURL = 'https://api.xxx.com';

// 每次请求都为http头增加Authorization字段,其内容为token;
instance.interceptors.request.use(
    config => {
        if (store.state.user.token) {
            config.headers.Authorization = `token ${store.state.user.token}`;  // vuex 缓存的Token信息塞入请求头
        }
        return config
    },
    err => {
        return Promise.reject(err)
    }
);
export default instance

页面权限控制

我们针对页面访问权限展开讨论,我们希望实现一个效果:只显示当前用户能访问的权限内的菜单,当用户通过URL强制访问会直接进入 404 页面。针对此效果我们首先要做的就是配置好路由表信息。因为涉及到有些页面需要访问权限,有些页面不需要访问权限,所以我们将登录、404、维护等非权限操作页面写在默认路由,将其他权限操作页面写到一个变量中。(404 页面一定要最后加载,如果放在 constantRouterList 一同声明了404,后面的所以页面都会被拦截到404)

// router/index.js

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

// 默认不需要权限的页面
const constantRouterList = [{
        path: '/',
        name: '登录',
        component: (resolve) => require(['@/components/login'], resolve)
    },
    {
        path: '/index',
        name: '主页',
        component: (resolve) => require(['@/components/index'], resolve)
    },
    {
        path: '/template',
        name: '模板页面',
        component: (resolve) => require(['@/components/Template/template'], resolve)
    }
]

// 注册路由
export const router = new Router({
  routes: constantRouterList
});

// 需要权限控制的页面
export const asyncRouterList = [
    {
        path: '/resource',
        name: 'Resource',
        meta: {
            permission: []
        },
        component: (resolve) => require(['@/components/Resource/resource'], resolve)
    },
    {
        path: '',
        name: 'Log',
        component: App,
        children: [{
                path: '/userLog',
                name: 'UserLog',
                meta: {
                    permission: []
                },
                component: (resolve) => require(['@/components/Log/userLog'], resolve),
            },
            {
                path: '/operatingLog',
                name: 'operatingLog',
                meta: {
                    permission: []
                },
                component: (resolve) => require(['@/components/Log/operatingLog'], resolve),
            },
        ]
    }
];

页面访问的的大致流程我们可以用一下流程图表示:

graph LR
用户登录 --> 获取用户的信息并缓存 --> 根据用户的权限渲染对应权限的菜单 --> 点击菜单进入到制定权限页面 

通过请求后台接口,我们拿到所有的权限数据并将数据缓存在 vuex 中,然后再利用返回的数据匹配之前写的异步路由表 asyncRouterList,最终得到实际路由表。下面是 vuex 中的缓存数据的逻辑代码:

// store/index.js

import Axios from 'axios'
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);
const axios = Axios.create();

const state = {
    mode: 'login',
    list: []
};

const getters = {};

const mutations = {
    setMode: (state, data) => {
        state.mode = data
    },
    setList: (state, data) => {
        state.list = data
    }
};

const actions = {
    // 获取权限列表
    getPermission({commit}) {
        return new Promise((resolve, reject) => {
            axios({
                url: '/xxx/getInfo?id=' + sessionStorage.getItem('privId'),
                methods: 'get',
                headers: {
                    token: sessionStorage.getItem('token'),
                }
            }).then((res) => {
                // 存储权限列表
                commit('setList', res.data);
                resolve(res.data);
            }).catch(() => {
                reject()
            })
        })
    }
};

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
})

我们刚才说过需要将后台数据和前端的异步路由对象进行匹配,其实这个过程就是一个路由动态加载的过程,此处我们提供一个路由匹配的函数:

// router/index.js

/**
 * 根据权限匹配路由
 * @param {array} permission 权限列表(菜单列表)
 * @param {array} asyncRouter 异步路由对象
 */
function routerMatch(permission, asyncRouter) {
    return new Promise((resolve) => {
        const routers = [];
        // 创建路由
        function createRouter(permission) {
            // 根据路径匹配到的router对象添加到routers中即可
            permission.forEach((item) => {
                if (item.children && item.children.length) {
                    createRouter(item.children)
                }
                let path = item.path;
                // 循环异步路由,将符合权限列表的路由加入到routers中
                asyncRouter.find((s) => {
                    if (s.path === '') {
                        s.children.find((y) => {
                            if (y.path === path) {
                                y.meta.permission = item.permission;
                                routers.push(s);
                            }
                        })
                    }
                    if (s.path === path) {
                        s.meta.permission = item.permission;
                        routers.push(s);
                    }
                })
            })
        }
        createRouter(permission)
        resolve([routers])
    })
}

对于路由匹配的函数调用,可以放在路由守卫中,即在每一次路由跳转的时候,都会进行一次动态路由匹配:

// router/index.js

router.beforeEach((to, form, next) => {
    if (sessionStorage.getItem('token')) {
        if (to.path === '/') {
            router.replace('/index')
        } else {
            if (store.state.list.length === 0) {
                //如果没有权限列表,将重新向后台请求一次
                store.dispatch('getPermission').then(res => {
                    //调用权限匹配的方法
                    routerMatch(res, asyncRouterList).then(res => {
                        //将匹配出来的权限列表进行addRoutes
                        router.addRoutes(res[0]);
                        next({
                            ...to,
                            replace: true
                        })
                    })
                }).catch(() => {
                    router.replace('/')
                })
            } else {
                if (to.matched.length) {
                    next()
                } else {
                    router.replace('/')
                }
            }
        }
    } else {
        if (whiteList.indexOf(to.path) >= 0) {
            next()
        } else {
            router.replace('/')
        }
    }
});

基于此,我们完成了对页面访问权限的控制。我们可以总结出一条完整的权限控制流程图。

路由权限控制的完整流程图

image.png