BuildAdmin管理员后台路由及权限节点渲染过程分析

BuildAdmin

基本接口查询

  • 操作成功与失败
    app\common\controller\Api.php

BuildAdmin权限管理控制实现分析

登录界面和进入系统后的界面属于两个路由:

{
   // 首页
   path: '/',
   redirect: '/admin'
},
    {
        // 管理员登录页
        path: '/admin/login',
        name: 'adminLogin',
        component: () => import('/@/views/backend/login.vue'),
        meta: {
            title: pageTitle('adminLogin'),
        },
    },

管理员界面路由,最外层是Layout:

const adminBaseRoute: RouteRecordRaw = {
    path: '/admin',
    name: 'admin',
    // 在写 redirect 的时候,可以省略 component 配置,因为它从来没有被直接访问过,
    // 所以没有组件要渲染。
    // 唯一的例外是嵌套路由:如果一个路由记录有 children 和 redirect 属性,它也应该有 component 属性。
    component: () => import('/@/layouts/backend/index.vue'),
    redirect: '/admin/loading',
    meta: {
        title: pageTitle('admin'),
    },
    children: [
        {
            path: 'loading',
            name: 'adminMainLoading',
            component: () => import('/@/layouts/common/components/loading.vue'),
            meta: {
                title: pageTitle('Loading'),
            },
        },
        {
            path: 'iframe/:url',
            name: 'layoutIframe',
            component: () => import('/@/layouts/common/router-view/iframe.vue'),
            meta: {
                title: pageTitle('Embedded iframe'),
            },
        },
    ],
}
  • 需要登录后才能进入组件的最外层:Layout组件web\src\layouts\backend\index.vue,它决定使用哪种布局,在该组件中会调用index.pnp/index/index接口。

    该组件是静态的,但是会进行token验证,如果没有token会跳转到登录界面。存在token的话会调用上述接口,得到路由菜单。

    由于axios被封装成自动携带token,如果存在本地token的话,会自动携带并在后端进行校验,校验成功后端会返回menus字段,用于更新路由(左侧菜单)。

在这里插入图片描述

  • 管理员进行登录操作,如果账号信息正确,会重定向(已经登录)或跳转(第一次登录)到/admin,请求路由菜单。
    web\src\views\backend\login.vue

    后端接口:

    app\admin\controller\Index.php

    app\admin\library\Auth.php具体权限逻辑,包括信息验证,菜单生成

    如果已经登录,会重定向到/admin

    $this->success(__('You have already logged in. There is no need to log in again~'), [
    	'routePath' => '/admin'
    ], 302);
    
    if (response.data.code == 302) {
                        if (response.data.data.routeName) {
                            router.push({ name: response.data.data.routeName })
                        } else if (response.data.data.routePath) {
                            router.push({ path: response.data.data.routePath })
                        }
                    }
    
  • 路由菜单生成
    app\admin\controller\auth\Menu.php
    extend\ba\Auth.php 负责权限方面的具体逻辑
    首先获取用户的组id:gorunp_ids,组是最顶层的权限分类,包括超级管理员、一级管理员、二级管理员,组内进行规则(rules)配置,规则规定该级别的管理员可以访问哪些路由。
    menu_rule中包含了系统中所有路由信息以及操作信息,每条数据都有类型字段,表示该条数据是一个路由还是一个操作,每条信息有一个规则id,用于配置用户组的rules

    后端后把信息封装成为树结构返回。

  • 菜单项内权限管理(操作权限管理)

    最基本的查看功能用于获取所有信息列表。

    此外,例如编辑、新增、删除都对应一条路由信息
    在这里插入图片描述

    type字段决定了这条路由被渲染成菜单还是按钮。

    在添加路由的函数中(``web\src\utils\router.ts),只会处理type: menu的数据:
    权限节点(编辑、删除等)被单独保存,菜单路由被返回(在Layout组件中处理):

    这里路由要添加到两个地方,主要为了解耦,首先肯定要添加到路由上(addRouteAll),才能支持页面页面跳转,其中权限节点被过滤,不会被添加到路由;

    此外将路由添加到全局状态navTabs上(handleMenuRule),这个store会被用于在页面上渲染路由菜单,其中权限节点会被保存在authNode中。

    /**
     * 动态添加路由-带子路由
     */
    export const addRouteAll = (viewsComponent: Record<string, { [key: string]: any }>, routes: any, parentName: string) => {
        for (const idx in routes) {
            if (routes[idx].extend == 'add_menu_only') {
                continue
            }
    
            // 添加路由项 只有类型为menu 或tab 才添加
            if (routes[idx].type == 'menu' && routes[idx].menu_type == 'tab' && viewsComponent[routes[idx].component]) {
                addRouteItem(viewsComponent, routes[idx], parentName)
            }
            
            // 添加子路由
            if (routes[idx].children && routes[idx].children.length > 0) {
                addRouteAll(viewsComponent, routes[idx].children, parentName)
            }
        }
    }
    
    /**
     * 动态添加路由
     */
    export const addRouteItem = (viewsComponent: Record<string, { [key: string]: any }>, route: any, parentName: string) => {
        if (parentName) {
            console.log(parentName, ' add route path:', route.path)
            console.log('route name:', route.name,)
            console.log('component', viewsComponent[route.component].default)
            router.addRoute(parentName, {
                path: route.path,
                name: route.name,
                component: viewsComponent[route.component].default,
                meta: {
                    title: route.title,
                },
            })
        } else {
            router.addRoute({
                path: '/' + route.path,
                name: route.name,
                component: viewsComponent[route.component].default,
                meta: {
                    title: route.title,
                },
            })
        }
    }
    

    处理路由菜单:

    let menuRule = handleAdminRoute(res.data.menus)
    // 更新stores中的路由菜单数据
    navTabs.setTabsViewRoutes(menuRule)
    
    /**
     * 处理后台的路由
     */
    export const handleAdminRoute = (routes: any) => {
        const viewsComponent = import.meta.globEager('/src/views/backend/**/*.vue')
        // adminBaseRoute.name: admin
        // console.log('routes all ', routes)
    
        // 递归添加路由
        addRouteAll(viewsComponent, routes, adminBaseRoute.name as string)
        let menuAdminBaseRoute = '/' + (adminBaseRoute.name as string) + '/'
        return handleMenuRule(_.cloneDeep(routes), menuAdminBaseRoute, menuAdminBaseRoute)
    }
    
    
    /**
     * 后台菜单处理
     */
    const handleMenuRule = (routes: any, pathPrefix = '/', parent = '/', module = 'admin') => {
        let menuRule = []
        let authNode = []
        for (const key in routes) {
            if (routes[key].extend == 'add_rules_only') {
                continue
            }
            if (routes[key].type == 'menu' || routes[key].type == 'menu_dir') {
                if (routes[key].type == 'menu_dir') {
                    // 如果是菜单目录 但没有下级路由 则过滤
                    if (!routes[key].children) {
                        continue
                    }
                    // 否则将其设置为菜单项
                    routes[key].menu_type = 'tab'
                }
    
                routes[key].type = routes[key].menu_type
                if (routes[key].type == 'tab') {
                    routes[key].path = pathPrefix + routes[key].path
                } else {
                    routes[key].path = routes[key].url
                }
                delete routes[key].url
                delete routes[key].menu_type
                if (routes[key].children && routes[key].children.length > 0) {
                    routes[key].children = handleMenuRule(routes[key].children, pathPrefix, routes[key].path)
                }
                menuRule.push(routes[key])
            } else {
                // 权限节点
                authNode.push(pathPrefix + routes[key].name)
            }
        }
        if (authNode.length) {
            if (module == 'admin') {
                const navTabs = useNavTabs()
                navTabs.setAuthNode(parent, authNode)
            } else if (module == 'user') {
                const memberCenter = useMemberCenter()
                memberCenter.setAuthNode(parent, authNode)
            }
        }
        return menuRule
    }
    

navBar渲染——路由导航菜单

web\src\layouts\backend\components\menuVertical.vue:获取navTabs列表

web\src\layouts\backend\components\menuTree.vue:接收数据,执行具体渲染工作

Table渲染——权限节点

以表格header作为分析(表格内部也有操作列),header部分显示了添加、编辑等可以执行的权限操作。

web\src\components\table\header\index.vue

权限验证函数web\src\utils\common.ts ---> auth

/**
 * 页面按钮鉴权
 * @param name
 */
export const auth = (name: string) => {
    const navTabs = useNavTabs()
    // console.log(navTabs.state.authNode)
    if (navTabs.state.authNode.has(router.currentRoute.value.path)) {
        if (navTabs.state.authNode.get(router.currentRoute.value.path)!.some((v: string) => v == router.currentRoute.value.path + '/' + name)) {
            return true
        }
    }
    return false
}

回顾添加auth节点时的操作:

if (module == 'admin') {
	const navTabs = useNavTabs()
	navTabs.setAuthNode(parent, authNode)
}

回顾authNode类型以及添加操作:

authNode: new Map()const setAuthNode = (key: string, data: string[]) => {
	state.authNode.set(key, data)
}

authNode用map保存了路由下所有的权限操作数组;

权限路径是 router.currentRoute.value.path + '/' + name)

返回来看Header组件的渲染过程:

首先定义默认的权限操作:

const props = withDefaults(defineProps<Props>(), {
    buttons: () => {
        return ['refresh', 'add', 'edit', 'delete']
    },
    quickSearchPlaceholder: '',
})

然后在组件模板中进行权限验证:

在这里插入图片描述

Table内部的权限按钮渲染

相关逻辑在web\src\components\table\fieldRender\index.vue中:

在这里插入图片描述

这里使用了自定指令v-authweb\src\utils\directives.ts

/**
 * 页面按钮鉴权指令
 * @description v-auth="'name'",name可以为:index,add,edit,del,...
 */
function authDirective(app: App) {
    app.directive('auth', {
        mounted(el, binding) {
            if (!binding.value) return false
            const navTabs = useNavTabs()
            if (navTabs.state.authNode.has(router.currentRoute.value.path)) {
                if (
                    !navTabs.state.authNode
                        .get(router.currentRoute.value.path)!
                        .some((v: string) => v == router.currentRoute.value.path + '/' + binding.value)
                ) {
                    el.parentNode.removeChild(el)
                }
            }
        },
    })
}

总结

上述内容主要讲解了后端怎么返回经过权限过滤的路由菜单和权限按钮,以及前端的渲染过程。

前端渲染的核心是navTabs store对象,路由菜单的渲染和权限节点的渲染都需要使用它。

当然后端接收到请求时也会进行权限验证,这部分上述内容没有涉及。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值