后台管理系统 - 权限设计
2022-08-03 09:57:03
315
{{single.collect_count}}

一、前言

对于前端项目特别是中后台管理系统项目,权限设计是最复杂的点之一。

一般来说权限设计需要后端来把关,毕竟相对来说前端是无法保证安全的,前端的代码和数据请求都可以伪造。而前端的权限设计更多是为了用户体验的考虑。前端保证体验,后端保证安全。

由于前后端的开发差异和侧重点不同,在权限设计上也不一样。后端更多的是根据功能对象划分不同的权限模块,针对接口相应进行权限判断;而前端更多是针对页面路由进行模块划分,针对页面可访问进行判断。

接下来将以后台管理系统为例,分享个人对前端权限设计的见解。

(具体内容尽量做到和技术框架无关,无论是vue还是react都只是代码实现上的差异,主思路一致。不过话说vue的实现确实要比react简便很多,所以下述代码都以react为例)

二、页面级别

1、几种方式比较

先上几个常见的权限设计方式。

  • 方式一:由后端返回筛选后的路由配置,前端渲染

    • 描述:
      这种就是前端将所有路由配置数据给到后端存储,后端对根据用户权限对路由数据筛选后返回到前端,再由前端渲染。
    • 存在的问题:
      路由是前端使用的,为啥要存储到后端呢?要调整路由结构或者修改路由啥的都要找后端修改,前端开发不乐意,后端也不乐意,前后端分离的时代,这不是在倒退嘛。
  • 方式二:后端返回用户角色,前端根据角色做路由筛选

    • 描述:
      这种对方式一做了优化,方式一是后端根据权限筛选路由后返回前端,而现在是把筛选过程放在了前端,后端返回角色信息,前端遍历路由配置,根据角色筛选出有权限的路由渲染。
    • 存在的问题:
      但这种方式还有一个问题,就是角色的权限并非一成不变,一旦角色权限改了,前端的路由配置都要逐个排查修改,如果系统设计了动态修改角色权限的功能,那这种设计方式就没法用了。
  • 方式三:后端返回当前用户的所有权限id,前端根据权限id做路由筛选

    • 这也是本文推荐的方式,下面详细说明。

具体来说,就是对每一个页面路由都设置一个匹配的权限id(accessId),后端只需要把用户的所有权限id给到前端即可,不需要角色信息。

ps: 有些人可能对角色这点绕不过去,其实不管你的系统有没有角色这个概念,对于前端来说,角色只是一个对用户的一个称谓而已,在需要的时候展示这个称谓给用户界面。
一个角色可以有多个权限,然而前端不需要关心具体角色有哪些权限,前端需要的只是当前用户有哪些权限。
具体角色的权限数据只有在动态配置角色权限的页面才需要,实现上只需要遍历路由配置以一个tree树形组件展示即可,这种场景下也就是角色权限可能随时会变,前端就不应该以角色数据处理权限,而是应该以权限id来定,以不变应万变。
其实你也可以把每一个权限id都当成不同的角色理解,只不过这个角色就只有一个权限。

至于路由的权限id在哪里配置,这就看你项目的路由管理方案了,最好是对路由有一个统一管理,然后根据用户权限对路由做动态筛选,或者在路由访问时拦截判断。

2、导航菜单的处理

在这里插入图片描述

一般来说后台管理系统都会有个导航菜单,以侧边栏导航居多,对于用户来说这个也是所有页面的访问入口,所以导航菜单需要根据用户权限动态展示。

建议将所有路由配置信息存储在一个配置数组中,导航菜单就根据路由配置数组来动态生成,同时判断权限做筛选。

  • 对于 vue 来说,使用 vue-router 管理路由已经非常方便了;
  • 而 react 就有点麻烦,
    • 对于 react-router v5 及以下版本可以使用react-router-config来统一管理路由,
    • 对于 react-router v6 版本,安利一下个人封装的路由管理方案react-router-waiter传送门)。

路由配置示例:

const routes = [{path: '/index',component: Index,meta: { // meta,自定义的数据都放这里面title: '首页', // 菜单标题accessId: 10000, // 权限idhideMenu: false, // 是否在侧边栏隐藏当前路由菜单noLogin: false, // 当前路由访问是否需要登录},},{path: '/nest',meta: {title: '多级菜单',},children: [{path: 'nest1',component: Nest1,meta: {title: '二级菜单',accessId: 10001,}},]},]

导航菜单动态生成示例:

function getMenuList () {const getList = (routeList = [], prePath = '') => {let menuList = []// 遍历路由routeList.forEach(v => {v.meta = v.meta || {}// 排除不需要显示菜单的路由if (v.redirect || v.path === '*' || v.meta.hideMenu) {return}// 排除没有访问权限的路由if (!getIsCanAccess(v.meta.accessId)) {return}const currentPath = prePath + v.pathif (v.children) {// 有嵌套路由,递归添加菜单menuList.push((<SubMenu key={currentPath} title={v.meta.title}>{getList(v.children, currentPath + '/')}</SubMenu>))} else {// 无嵌套路由,菜单添加结束menuList.push((<ItemMenu key={currentPath}><Link to={currentPath}>{v.meta.title}</Link></ItemMenu>))}})return menuList} return getList(routes)}

3、路由访问控制

导航菜单动态生成一定程度上限制了用户访问无权限的路由,但还不够,用户如果跳转一个没有权限的路由,或者在地址栏手动输入没有权限的路由网址,也是能访问页面,这就需要处理。

一般用户的权限信息都是从接口异步获取,所以我们需要在用户打开项目进入页面之前先请求接口拿到权限信息,然后再做后续页面的展示,这样才能保证在用户手动输入url场景下能有效地进行权限判断和路由拦截。

两种方式:

  • 1、简单的,获取权限信息 - 筛选路由配置数据 - 渲染路由。即拿到权限信息后就对路由配置数据做个过滤,只保留有权限的路由数据,再渲染路由,让用户访问无权限的路由时展示404页面。
  • 2、复杂点,获取权限信息 - 渲染路由 - 路由拦截处理。即拿到权限信息后直接渲染完整路由数据,然后通过路由的导航守卫做判断拦截,这样可以控制用户访问无权限的路由时展示403页面及更多提示信息,自定义性更强。

渲染路由前的控制,在入口组件App.vue或App.js里来写,代码示例:

import { HashRouter } from 'react-router-dom'import RouterWaiter from 'react-router-waiter'export default function App () {const [isRender, setIsRender] = useState(false)useEffect(() => {// 解析url,获取path路由const path = getRoutePath()// 排除登录页等不需要权限的路由if (['/login'].includes(path)) {setIsRender(true)} else {// 判断是否已获取到权限信息if (!store.isGotUserInfo) {api.getUserInfo().then(res => {const data = res.data || {}// 权限信息存储到store状态管理数据中store.setUserInfo(data) // 获取完权限信息,放开路由渲染setIsRender(true)})}}}, [])return (<HashRouter>{isRender ? <RouterWaiter /> : null}</HashRouter>)}
  • vue的实现也简单,在App.vue里通过v-if绑定控制<router-view />即可。

4、路由拦截

这是对上述“路由访问控制”的方式2的补充说明。

要实现路由拦截,需要对每一个路由的访问都做前置判断。

  • 对于vue,有自带的路由全局导航守卫beforeEach,处理很方便。
  • 而react没有,只能自行封装,再次安利一下react-router-waiter,对路由拦截也做了封装处理。

拦截判断的代码示例:

meta = meta || {} // 路由配置数据的meta字段if (!meta.noLogin && store.isLogin) { // 登录判断const { accessId } = metaif (!store.isGotUserInfo) { // 是否已获取到用户(权限)信息api.getUserInfo().then(res => {const data = res.data || {}store.setUserInfo(data)// 无权限时拦截跳转403页面if (!getIsCanAccess(accessId)) {toPage403()}})} else {if (!getIsCanAccess(accessId)) {toPage403()}}} else {// 未登录时拦截跳转登录页toPageLoin()}

三、按钮级别

按钮级别,即页面中更细粒度的权限控制。

这个其实就很简单了,只需要控制相关的dom是否展示即可。

每一个需要控制的操作区域dom都给分配一个权限id,然后判断该用户是否具有该权限,控制该区域dom的显示隐藏。

后端也只需要把所有页面权限id和按钮级别的权限id都一箩筐给到前端就行。

  • vue里通过v-if绑定dom来处理就行,封装一个公共的方法来判断是否具有权限,也可以封装一个自定义指令来处理,以权限id为入参,使用更方便。
  • react里也差不多,通过jsx里if控制,同样可以封装个公共方法,也可以封装成一个公共组件处理。

代码示例:

return (<div>{getIsCanAccess('10008')? (<div>我是权限dom1</div>): null}<div>hello</div>{getIsCanAccess('10009')? (<div>我是权限dom2</div>): null}</div>)

四、其他

基于此权限设计方案,个人搭建了一个react后台管理系统react-antd-mobx-admin,里面有完整的权限设计代码,供参考。

回帖
全部回帖({{commentCount}})
{{item.user.nickname}} {{item.user.group_title}} {{item.friend_time}}
{{item.content}}
{{item.comment_content_show ? '取消' : '回复'}} 删除
回帖
{{reply.user.nickname}} {{reply.user.group_title}} {{reply.friend_time}}
{{reply.content}}
{{reply.comment_content_show ? '取消' : '回复'}} 删除
回帖
收起
没有更多啦~
{{commentLoading ? '加载中...' : '查看更多评论'}}