找回密码
 会员注册
查看: 32|回复: 0

转转统一权限系统的设计与实现(前端实现篇)

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-20 07:53:28 | 显示全部楼层 |阅读模式
之前推送了转转统一权限系统的设计与实现(设计篇)和转转统一权限系统的设计与实现(后端实现篇)。本文是该系列最后一篇,希望可以带给大家一些收获,正文开始。权限前端SDK设计本次新版设计,EHR系统会向权限系统同步用户数据,不用再提供用户注册能力。在保证对外接口不变的情况下简化sdk逻辑,对外提供用户信息和用户权限数据。目前sdk提供以下接口,利用login和getUserPermssion获取用户信息和权限数据,并保存在全局变量中,并提供一个特殊接口routerFilter可利用实现对菜单树状数据进行权限过滤。权限接入方式接入方式可以有两种方式,对于普通React项目可以使用普通接入,对于Umi项目则可以更加简洁。一、普通React项目接入请务必在挂载入口前同步调用,保证数据返回后才渲染import { commonLogin } from '@zz-common/zz-permission'(async() => {  const { userInfo = {}, permissionInfo = {} } = await commonLogin({ appCode: '' }); const { resources } = permissionInfo  render()})()获取到权限resources后,就可以对菜单进行权限过滤。为了方便使用,sdk内部提供了默认的处理方式import { routerFilter } from '@zz-common/zz-permission'const router = []const newRouter = routerFilter(router)二、Umi项目接入首先需要安装Umi插件npm install @umijs/plugin-access -D安装完成后,需要修改相应代码1、在src/app.ts文件接入权限sdk// app.tsimport { commonLogin } from '@zz-common/zz-permission'export async function getInitialState() {  // permissionInfo-权限信息,userInfo-当前登录用户信息  const { permissionInfo, userInfo } = await commonLogin({ appCode: '' })  const { resources = [] } = permissionInfo  return {    userInfo,    permissionList: resources  }}调用权限sdk获取到当前登录用户信息和权限数据,放在initialState中。2、创建src/access.ts文件// 权限定义文件// https://umijs.org/zh-CN/plugins/plugin-accessimport routes from '../config/routes'function findRouteAccessList() {  const permissions = []  const stack = [...routes]  while (stack.length > 0) {    const route = stack.pop()    if (route.access) {      permissions.push(route.access)    }    if (route.routes) {      stack.push(...route.routes)    }  }  return permissions}export default (initialState = {}) => {  const { permissionList = [] } = initialState  const accessList = permissionList.reduce((access, item) => {    access[item.code] = true    return access  }, {})  const routePermissions = findRouteAccessList()  routePermissions.forEach((key) => {    if (!(key in accessList)) {      accessList[key] = false    }  })  return accessList}”在这里需要注意下,对于路由配置的access对应的权限编码,umi插件认为只有显示的设置false才认为是没有权限。”现在我们已经完成权限sdk和umi的结合,告知umi项目权限配置。但是如果想更便利的快速生成导航菜单,则需要搭配@umijs/plugin-layout插件npm install @umijs/plugin-layout -D这样我们在路由配置文件里增加access字段配置// config/route.tsexport const routes =  [  {    path: '/pageA',    component: 'PageA',    access: 'canReadPageA', // 对应的权限编码  }]权限插件会将用户在这里配置的access字符串与当前用户所有权限做匹配,如果找到相同的项,并当该权限的值为false,则当用户访问该路由时,默认展示403页面。除了菜单受权限控制,当然还有按钮级别的权限控制,我们可以创建一个公共组件CheckAuth来实现对按钮控制//src/components/CheckAuthimportReactfrom'react'import{useAccess,Access}from'umi'constCheckAuth=({children,permissionCode})=>{constaccess=useAccess()constaccessible=access[permissionCode]return}exportdefaultCheckAuth使用CheckAuth组件对按钮进行包裹来进行渲染,当前用户没有权限就不显示按钮importReactfrom'react'import{Button}from'antd'importCheckAuthfrom'@/components/CheckAuth'constAuthButton=()=>(测试权限控制)动态权限菜单实现接入Umi项目系统左侧菜单默认是根据本地路由生成,如果想通过接口来完全生成菜单、注册路由,则可以使用运行时动态路由。我们在权限系统配置菜单相关权限,通过接口请求到树状结构权限数据,这里就会有个问题,如何使远程数据和本地的路由配置关联,如何重组Umi路由对象。首先,我们创建几个辅助函数// dynamicRoute.ts/* eslint-disable @typescript-eslint/no-unused-vars */import type { Route } from '@ant-design/pro-layout/lib/typings'import type { MenuDataItem } from '@ant-design/pro-layout'import NotFound from '../../pages/404'function flatRoutesByName(routes: Route[]) {  const resultRoutes = routes.reduce((obj, item) => {    if (item.path) {      obj[item.path] = item    }    return obj  }, {})  return resultRoutes}/** * 渲染路由组件 * * @param {*} route 远程路由对象 * @param {*} flatRoutes 本地拍平路由集合 * @param {*} hasChildren 是否有子路由 */function renderComponent(route: Route, flatRoutes: Route, hasChildren: boolean) {  let component  // 当前路由没有子路由  if (!hasChildren) {    const { path } = route    if (path & flatRoutes[path]) {      component = flatRoutes[path].component    }  }  return component}/** * 远程路由和本地路由结合组成新路由 * * @param {*} sourceRoutes 远程接口路由配置 * @param {*} localRoutes 本地路由 */export function renderRoutes(sourceRoutes: Route[] = [], localRoutes: Route[] = []) {  const flatRoutes = flatRoutesByName(localRoutes)  sourceRoutes = sourceRoutes    .filter((item) => item.url)    .map((route) => ({      ...route,      path: route.url.toLowerCase()    }))  const hideRoutes = localRoutes.filter((route) => route.hideInMenu || route.showInMenu)  sourceRoutes.push(...hideRoutes)  const routes: any[] = []  sourceRoutes.forEach((route) => {    const { path, name, children, image, type } = route    const hasChildren = children & children.length > 0 & children.every((item: Route) => item.url)    const localRoute = (path & flatRoutes[path]) || {}    const childLocalRoutes = localRoute.routes    const routeItem: any = {      ...localRoute,      name,      path,      icon: image,      exact: !hasChildren,      component: renderComponent(route, flatRoutes, !!hasChildren)    }    if (hasChildren) {      routeItem.routes = renderRoutes(children, childLocalRoutes)      routeItem.routes.push({ component: NotFound })    }    routes.push(routeItem)  })  return routes}// 组建重定向路由export function getRedirectRoute(routes: Route = [], cataloguePath: string = '/') {  const getRedirectPath: (childRoutes: any) => void = (childRoutes) => {    const firstParentRoute = childRoutes[0] || {}    return firstParentRoute?.routes?.length > 0      ? getRedirectPath(firstParentRoute.routes)      : firstParentRoute.path  }  const redirectPath = getRedirectPath(routes)  return { path: cataloguePath, redirect: redirectPath, exact: true }}然后在app.ts文件修改路由配置import React from 'react'import type { RunTimeLayoutConfig } from 'umi'import type { Route } from '@ant-design/pro-layout/lib/typings'import { parse } from 'query-string'import { roBreadcrumb } from '@ant-design/pro-layout'import { commonLogin } from '@zz-common/zz-permission'import NotFound from '@/pages/404'import UnAccessiblePage from '@/pages/403'import { renderMenuItem, renderBreadcrumItem, renderBreadcrum } from '@/components/CustomizeLayout'import { renderRoutes, getRedirectRoute } from '@/components/CustomizeLayout/dynamicRoute'import GlobalHeader from '@/components/GlobalHeader'import defaultSettings from '../config/defaultSettings'import type { ZLayoutSettings } from '../config/defaultSettings'export const dva = {  config: {    onError(e: Error) {      // e.preventDefault()      console.error(e.message)    }  }}let userInfo: anylet permissionList: any[]let authRoutes: any[]// 渲染之前获取权限信息export function render(oldRender: () => void) {  commonLogin({ appId: defaultSettings.systemId })    .then(({ permissionInfo, userInfo: user }: any) => {      const { resources = [], resourcesTree = [] } = permissionInfo      userInfo = user      permissionList = resources      authRoutes = resourcesTree    })    .catch(() => {})    .finally(() => {      oldRender()    })}// 注册路由export function patchRoutes({ routes }: any) {  const localRoutes = routes[0].routes[0].routes  const mainRoutes = renderRoutes(authRoutes, localRoutes)  // 重定向路由  const redirectRoute = getRedirectRoute(mainRoutes)  if (redirectRoute) {    mainRoutes.unshift(redirectRoute)  }  // 404路由  const notFoundRoute = { component: NotFound }  mainRoutes.push(notFoundRoute)  routes[0].routes[0].routes = mainRoutes}// 把用户和权限信息放在initialState里export async function getInitialState(): romise {  return {    userInfo,    permissionList,    authRoutes,    settings: {      ...defaultSettings,      collapsed: localStorage.getItem('ui:collapsed') === 'true'    }  }}// 运行时布局配置export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {  return {    ...initialState?.settings,    contentStyle: { margin: useSidebar ? 12 : 0 },    unAccessible: ,    onCollapse: (collapsed: boolean) => {      if (initialState) {        setInitialState({          ...initialState,          settings: { ...initialState.settings, collapsed }        })      }      localStorage.setItem('ui:collapsed', collapsed.toString())    },    menuItemRender: renderMenuItem,    breadcrumbRender: renderBreadcrum,    itemRender: renderBreadcrumItem,    rightContentRender: () => ,    headerContentRender: () =>    }}从代码中可看到,核心是获取权限树状结构数据后,通过patchRoutes修改路由配置,即可实现运行时动态路由,菜单也会同步生成。总结本文主要介绍权限Sdk如何在前端结合使用,对于Umi和非Umi项目,需要注意的就是在页面渲染前一定要先获取到当前用户权限信息。对于前端页面来讲,权限就是控制菜单、路由、按钮展示,使用公司内部Umi脚手架模板接入权限展示受控菜单则更加简易,因此也是在公司内推荐同学们升级新的脚手架模板。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 12:12 , Processed in 1.123516 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表