react 权限控制方案实践
权限控制是项目中,特别是后台管理项目中比较常见的功能了
结合实际的项目需求,讲讲在react中是如何实现权限控制的
背景
- 项目使用umi搭建
需求:
- 根据不同角色权限配置路由权限
- 根据不同权限控制页面显示效果
- 按钮显示隐藏
实现页面路由权限
实现效果:无权限的用户没有该页面的入口:左侧菜单无入口,以及直接进入url提示无权限
@umijs/plugin-access
src/access.ts
约定 src/access.ts
文件为权限定义文件,该文件需要默认导出一个方法,导出的方法会在项目初始化时被执行。该方法需要返回一个对象,对象的每一个值就对应定义了一条权限。具体的介绍见文档。
方案1
我的需求是根据用户角色来判断是否有该路由权限,如果按照文档的demo来,那么我需要先:
- 定义每个角色是否有pageA权限,pageB权限……
- 再在
access.ts
里输出对象{canReadPageA: true, canReadPageB: false...}
- 然后再在路由文件里定义
access: 'canReadPageA'
,access: 'canReadPageB'
……
这样虽然可以实现,但代码量太大,要对每个需要权限的页面进行定义和判断,access.ts 和 route.ts 文件都需要嵌入大量代码
所以我改变了思路,换了另一种方案,也就是方案2
方案2
在 access.ts
中,当返回的对象中,值是方法时:
- 参数是route, 即是当前路由信息
- 方法最后返回布尔值
利用这个参数,就可以在 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 配置了两个属性:
access
值为access.ts
返回对象的某个key,这里的话固定为auth
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
里定义好该页面的 access
和 roles
属性, 不需要改动到 access.ts
实现权限控制页面显示
实现效果:用户有菜单入口,进入页面后显示无权限
方案1
思路:
- 利用 umi 提供的 Access 组件来实现
根据
currentUser
对应的字段来判断是否有权限
代码如下:import { Access } from 'umi'; <Access accessible={currentUser.foo} fallback={<div>暂无权限</div>}> Foo content. </Access>;
缺点:需要在对应的页面中嵌入代码
方案2
思路:
通过高阶组件 wrappers
实现
在
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
做权限校验- 然后在
@/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
总结
以上就是最近关于权限控制的实践,做到对路由、页面和按钮层级进行鉴权,每一种都有对应的实现方案,每个方案都有自己的优缺点,旨在更优雅地编程!
如果有更好的方案,欢迎评论区留言!