写点什么

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

  • 2022 年 6 月 17 日
  • 本文字数:4957 字

    阅读完需:约 16 分钟

转转权限系统之前端实现

一、权限前端 SDK 设计

本次新版设计,Ehr 系统会向权限系统同步用户数据,不用再提供用户注册能力。在保证对外接口不变的情况下简化 sdk 逻辑,对外提供用户信息和用户权限数据。


目前 sdk 提供以下接口,利用logingetUserPermssion获取用户信息和权限数据,并保存在全局变量中,并提供一个特殊接口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
复制代码


安装完成后,需要修改相应代码


在 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 中。


创建 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/CheckAuthimport React from 'react'import { useAccess, Access } from 'umi'
const CheckAuth = ({ children, permissionCode }) => { const access = useAccess()
const accessible = access[permissionCode]
return <Access accessible={accessible}>{children}</Access>}
export default CheckAuth
复制代码


使用 CheckAuth 组件对按钮进行包裹来进行渲染,当前用户没有权限就不显示按钮


import React from 'react'import { Button } from 'antd'import CheckAuth from '@/components/CheckAuth'
const AuthButton = () => ( <CheckAuth permissionCode="test"> <Button type="primary">测试权限控制</Button> </CheckAuth>)
复制代码

三、动态权限菜单实现接入

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 { ProBreadcrumb } 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(): Promise<{ userInfo: any permissionList: any[] authRoutes: any[] settings: ZLayoutSettings}> { 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: <UnAccessiblePage />, onCollapse: (collapsed: boolean) => { if (initialState) { setInitialState({ ...initialState, settings: { ...initialState.settings, collapsed } }) } localStorage.setItem('ui:collapsed', collapsed.toString()) }, menuItemRender: renderMenuItem, breadcrumbRender: renderBreadcrum, itemRender: renderBreadcrumItem, rightContentRender: () => <GlobalHeader />, headerContentRender: () => <ProBreadcrumb /> }}
复制代码


从代码中可看到,核心是获取权限树状结构数据后,通过patchRoutes修改路由配置,即可实现运行时动态路由,菜单也会同步生成。

四、总结

本文主要介绍权限 Sdk 如何在前端结合使用,对于 Umi 和非 Umi 项目,需要注意的就是在页面渲染前一定要先获取到当前用户权限信息。对于前端页面来讲,权限就是控制菜单、路由、按钮展示,使用公司内部 Umi 脚手架模板接入权限展示受控菜单则更加简易,因此也是在公司内推荐同学们升级新的脚手架模板。


转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。关注公众号「转转技术」,各种干货实践,欢迎交流分享~

用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
转转统一权限系统的设计与实现(前端实现篇)_前端开发_转转技术团队_InfoQ写作社区