react 权限控制方案实践

权限控制是项目中,特别是后台管理项目中比较常见的功能了
结合实际的项目需求,讲讲在react中是如何实现权限控制的

背景

  1. 项目使用umi搭建
  2. 需求:

    1. 根据不同角色权限配置路由权限
    2. 根据不同权限控制页面显示效果
    3. 按钮显示隐藏

实现页面路由权限

实现效果:无权限的用户没有该页面的入口:左侧菜单无入口,以及直接进入url提示无权限

@umijs/plugin-access

src/access.ts

约定 src/access.ts 文件为权限定义文件,该文件需要默认导出一个方法,导出的方法会在项目初始化时被执行。该方法需要返回一个对象,对象的每一个值就对应定义了一条权限。具体的介绍见文档。

方案1

我的需求是根据用户角色来判断是否有该路由权限,如果按照文档的demo来,那么我需要先:

  1. 定义每个角色是否有pageA权限,pageB权限……
  2. 再在 access.ts 里输出对象 {canReadPageA: true, canReadPageB: false...}
  3. 然后再在路由文件里定义 access: 'canReadPageA', access: 'canReadPageB'……

这样虽然可以实现,但代码量太大,要对每个需要权限的页面进行定义和判断,access.ts 和 route.ts 文件都需要嵌入大量代码

所以我改变了思路,换了另一种方案,也就是方案2

方案2

access.ts 中,当返回的对象中,值是方法时:

  1. 参数是route, 即是当前路由信息
  2. 方法最后返回布尔值

利用这个参数,就可以在 route 里加入我们所需要的信息

// routes.ts
[
  {
    name: 'pageA',
    path: '/pageA',
    component: './pageA',
    access: 'auth', // 权限定义返回值的某个 key
    roles: ['admin', 'user'], // role为admin或者user时可以访问pageA页面
  },
  {
    name: 'pageB',
    path: '/pageB',
    component: './pageB',
    access: 'auth',
    roles: ['admin'],// 只有role为admin时可以访问pageA页面
  },
]

我给 route 配置了两个属性:

  1. access 值为 access.ts 返回对象的某个key,这里的话固定为 auth
  2. roles 定义可以有该页面权限的角色组

access.ts 中返回key为 auth 的对象:

// access.ts
let hasAuth = (route: any, roleId?: string) => {
  //  关键:对比route.roles 和 currentUser.roleId 判断是否有权限
  return route.roles ? route.roles.includes(roleId) : true;
};
export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) {
  const { currentUser } = initialState || {};
  return {
    auth: (route: any) => hasAuth(route, currentUser?.roleId),
  };
}

拿到route里的信息和当前用户信息进行对比,判断,返回布尔值

对比方案1,方案2优点就是,之后新增页面时,只需要在 routes.ts 里定义好该页面的 accessroles 属性, 不需要改动到 access.ts

实现权限控制页面显示

实现效果:用户有菜单入口,进入页面后显示无权限

方案1

思路:

  1. 利用 umi 提供的 Access 组件来实现
  2. 根据 currentUser 对应的字段来判断是否有权限
    代码如下:

    import { Access } from 'umi';
    
    <Access accessible={currentUser.foo} fallback={<div>暂无权限</div>}>
      Foo content.
    </Access>;

    缺点:需要在对应的页面中嵌入代码

方案2

思路:

通过高阶组件 wrappers 实现

  1. routes.ts 配置 wrappers 属性

    // routes.ts
    [
      {
     name: 'pageA',
     path: '/pageA',
     component: './pageA',
     wrappers: ['@/wrappers/authA']
      },
      {
     name: 'pageB',
     path: '/pageB',
     component: './pageB',
     wrappers: ['@/wrappers/authB']
      },
    ]

    这样,访问 /pageA 时,会先通过 @/wrappers/authA 做权限校验

  2. 然后在 @/wrappers/authA 中,
// wrappers/authA
import { useModel } from 'umi';

export default (props) => {
  const { initialState } = useModel('@@initialState');
  const { currentUser } = initialState || {};
  if (currentUser.authA) {
    return <div>{ props.children }</div>;
  } else {
    return <div>无权限</div>;
  }
}

这样,根据 currentUser 来判断是否渲染组件

方案2的优点是:

无须在页面组件中嵌入相关代码,只需要在 routes.tx 中配置 wrappers ,鉴权部分由 @/wrappers/ 来处理

然而,缺点就是,如果有多个权限,如authA, authB, authC…… 那么则需要在 @/wrappers/ 新建多个鉴权文件

实现按钮权限

实现效果:
无权限的按钮不显示或者置灰

一般的做法是在组件中判断

// 不显示按钮
{currentUser.auth ?  <button>创建</button> : null}
// 置灰
{<button disabled={currentUser.auth}>创建</button>}

但如果有大量的权限按钮,那么将要写好多次这种代码,所以在这里对按钮进行一次封装

// AuthBtn
import React, { useState, useEffect, useRef } from 'react';
import { Button } from 'antd';

const AuthBtn: React.FC<{}> = (props) => {
  let { authId, children } = props;
  // btnIds 应该有后台接口返回,告诉前端用户有哪些按钮权限
  let btnIds = ['read', 'edit'];
  let hasAuth = btnIds.includes(authId);
  // 这里可以根据实际需求封装
  return <Button disabled={!hasAuth}>{children}</Button>;
};
export default AuthBtn;

// index.ts
<AuthBtn authId="read">read 只读权限</AuthBtn>
<AuthBtn authId="write">write 写入权限</AuthBtn>

传入的 authId 需要先和后台约定好, 还可以根据实际需求传入type、loading等

这样,普通按钮用 Button, 需要鉴权的使用 AuthBtn

总结

以上就是最近关于权限控制的实践,做到对路由、页面和按钮层级进行鉴权,每一种都有对应的实现方案,每个方案都有自己的优缺点,旨在更优雅地编程!
如果有更好的方案,欢迎评论区留言!

03-05 21:26