权限
在线体验,请访问: https://condorheroblog.github.io/react-antd-admin/access/page-control
项目的权限基于 RBAC(Role-based access control) 进行设计,如果你对 RBAC 不熟悉,自行查找相关资料进行学习。
用户
项目模拟了两个用户,分别为:
角色
项目模拟了两个角色,分别为:
权限的获取方式
- 通过前端获取,前端写死路由,路由上有静态的角色,登录之后调用接口获取角色,根据角色加载对应的路由表。
- 通过后端获取,路由和角色均为动态获取,登录之后调用接口获取角色和路由表。
前端访问控制
实现原理: 在前端写死所有的路由的权限。初次进入系统只初始化通用的路由,需要权限才能访问的路由并未被加入路由表内。登录后拉取用户角色,通过角色去遍历前端路由表,获取该角色可以访问的路由表,生成路由表,再通过 router.patchRoutes
添加到路由实例,实现权限的过滤。
- 优点: 权限设计简单,适合人数不超过 10 人角色固定的小型团队。
- 缺点: 权限设计简单,如果后台改动角色,前台也需要跟着改动,无法适用大型项目。
开启前端访问控制
- 确保当前模式为前端访问控制模式
打开 src/store/preferences/index.ts
文件,在 DEFAULT_PREFERENCES 中将 enableBackendAccess
设置为 false
、enableFrontendAceess
设置为 true
。
export const DEFAULT_PREFERENCES = {
enableBackendAccess: false,
enableFrontendAceess: true,
};
- 配置路由权限
如果不配置角色权限 (roles) ,默认此路由所有用户可见,路由下的细颗粒度权限比如按钮权限需要通过权限码 (permissions) 来控制,下面是一个典型的权限格式。
{
"handle": {
"permissions": ["permission:button:get"],
"roles": ["admin"]
}
}
- 后端接口返回的角色和路由表的权限匹配
打开浏览器的控制台查看 /api/user-info
接口返回的用户信息,查看接口的 roles 字段是否和路由中的匹配。
{
"code": 200,
"result": {
"userId": 1,
"avatar": "https://avatars.githubusercontent.com/u/47056890",
"username": "Admin",
"email": "<EMAIL>",
"phoneNumber": "1234567890",
"description": "manager",
"roles": [
"admin"
]
},
"message": "ok",
"success": true
}
如果 roles 字段被后端设计为一个包含对象的数组,例如:
{
"roles": [
{
"id": "1",
"name": "admin"
}
]
}
在 src/store/user.ts
中修改 getUserInfo
方法,将 roles 字段改为包含字符串的数组即可。
后端访问控制
实现原理: 通过请求接口动态生成路由表。前端根据后端动态返回的数据结构转为前端路由表,然后通过 router.patchRoutes
添加到路由实例,实现权限的动态生成。
- 优点: 权限设计复杂,适合大型项目。
- 缺点: 权限设计复杂,不适合小型团队。
开启后端访问控制
- 确保当前模式为后端访问控制模式
打开 src/store/preferences/index.ts
文件,在 DEFAULT_PREFERENCES 中将 enableBackendAccess
设置为 true
、enableFrontendAceess
设置为 false
。
export const DEFAULT_PREFERENCES = {
enableBackendAccess: true,
enableFrontendAceess: false,
};
- 确保后端接口返回的菜单数据结构正确
后端返回的菜单数据结构可以在这个文件中查看:fake/async-routes.fake.ts
下面是一个典型的权限格式案例。
const accessRouter = {
path: "/access",
handle: {
icon: "SafetyOutlined",
title: "common.menu.access",
order: access,
},
children: [
{
path: "/access/page-control",
handle: {
icon: "FileTextOutlined",
title: "common.menu.pageControl",
},
},
{
path: "/access/button-control",
handle: {
icon: "LockOutlined",
title: "common.menu.buttonControl",
permissions: [
"permission:button:get",
"permission:button:update",
"permission:button:delete",
"permission:button:add",
],
},
},
{
path: "/access/common-visible",
handle: {
icon: "EyeOutlined",
title: "common.menu.commonVisible",
},
},
],
};
后端访问控制从用户信息接口中获取权限
默认情况下,后端访问控制会单独发一个接口,即 get-async-routes
接口,如果后端的路由数据在调用用户信息接口(user-info) 中返回,比如在 menus 字段中返回,则可以开启如下配置。
打开 src/router/routes/config.ts
文件,设置 isSendRoutingRequest 为 false,系统将会读取 user-info 接口的 menus 字段作为路由表。
权限控制方式
- 粗颗粒度权限,例如菜单权限。
- 细颗粒度权限,例如按钮级别的权限。
粗颗粒度权限控制
粗颗粒度权限控制,主要用来控制系统显示的菜单。
- 如果路由是后端控制则会调用
src/router/utils/generate-routes-from-backend.ts
函数进行路由的生成。 - 如果路由是前端控制则会调用
src/router/utils/generate-routes-from-frontend.ts
函数进行路由的生成。
生成的路由表会作为参数传递给 src/router/utils/generate-menu-items-from-routes.ts
方法,返回的值为 wholeMenus ,变量自动挂载到全局状态 access
中,最后组件自动渲染对应的权限菜单。
细颗粒度权限控制
细颗粒度权限控制,主要用来控制按钮的显示。
权限码
权限码是细颗粒度权限控制的唯一标识,例如:
{
"permissions": [
"permission:button:get",
"permission:button:update",
"permission:button:delete",
"permission:button:add"
]
}
每个路由下的 permissions 字段的值必须是唯一的,为了方便管理权限码,你必须在 src/hooks/use-access/constants.ts
文件中定义对应的权限码常量,然后在其他文件中引用。
例如:
export const accessControlCodes = {
get: "permission:button:get",
update: "permission:button:update",
delete: "permission:button:delete",
add: "permission:button:add",
};
组件方式
引入 AccessControl 组件,需要呈现的内容作为组件的 children 传入,对应的权限码作为组件的 codes 属性传入。
import { AccessControl } from "#src/components";
import { accessControlCodes } from "#src/hooks/use-access/constants";
import { Typography } from "antd";
export function AccessDemo() {
return (
<>
<AccessControl codes={accessControlCodes.get}>
<Typography.Text code>
{accessControlCodes.get}
</Typography.Text>
</AccessControl>
<AccessControl codes={accessControlCodes.update}>
<Typography.Text code>
{accessControlCodes.update}
</Typography.Text>
</AccessControl>
<AccessControl codes={accessControlCodes.delete}>
<Typography.Text code>
{accessControlCodes.delete}
</Typography.Text>
</AccessControl>
<AccessControl codes={accessControlCodes.add}>
<Typography.Text code>
{accessControlCodes.add}
</Typography.Text>
</AccessControl>
</>
);
}
函数方式
引入 useAccess 钩子,通过 hasAccessByCodes 方法来判断是否有权限。
import { AccessControl } from "#src/components";
import { useAccess } from "#src/hooks";
import { accessControlCodes } from "#src/hooks/use-access/constants";
import { Typography } from "antd";
export function AccessDemo() {
const { hasAccessByCodes } = useAccess();
return (
<>
{
hasAccessByCodes(accessControlCodes.get) && (
<Typography.Text code>
{accessControlCodes.get}
</Typography.Text>
)
}
{
hasAccessByCodes(accessControlCodes.update) && (
<Typography.Text code>
{accessControlCodes.update}
</Typography.Text>
)
}
{
hasAccessByCodes(accessControlCodes.delete) && (
<Typography.Text code>
{accessControlCodes.delete}
</Typography.Text>
)
}
{
hasAccessByCodes([accessControlCodes.add]) && (
<Typography.Text code>
{accessControlCodes.add}
</Typography.Text>
)
}
</>
);
}
角色
为了方便角色,你必须在 src/hooks/use-access/constants.ts
文件中定义对应的常量,然后在其他文件中引用。
例如:
export const AccessControlRoles = {
admin: "admin",
common: "common",
};
组件方式
引入 AccessControl 组件,需要呈现的内容作为组件的 children 传入,对应的权限码作为组件的 codes 属性传入,判断类型 type 属性设置为 "role"
。
import { AccessControl } from "#src/components";
import { useAccess } from "#src/hooks";
import { AccessControlRoles } from "#src/hooks/use-access/constants";
import { Typography } from "antd";
import { useTranslation } from "react-i18next";
export function AccessDemo() {
const { t } = useTranslation();
const { hasAccessByRoles } = useAccess();
return (
<>
<AccessControl type="role" codes={[AccessControlRoles.admin, AccessControlRoles.common]}>
<Typography.Text code>
{t("access.adminVisible.title")}
&
{t("access.commonVisible.title")}
</Typography.Text>
</AccessControl>
<AccessControl type="role" codes={AccessControlRoles.admin}>
<Typography.Text code>
{t("access.adminVisible.title")}
</Typography.Text>
</AccessControl>
<AccessControl type="role" codes={AccessControlRoles.common}>
<Typography.Text code>
{t("access.commonVisible.title")}
</Typography.Text>
</AccessControl>
</>
);
}
函数方式
引入 useAccess 钩子,通过 hasAccessByRoles 方法来判断是否有权限。
import { AccessControl } from "#src/components";
import { useAccess } from "#src/hooks";
import { AccessControlRoles } from "#src/hooks/use-access/constants";
import { Typography } from "antd";
import { useTranslation } from "react-i18next";
export function AccessDemo() {
const { t } = useTranslation();
const { hasAccessByRoles } = useAccess();
return (
<>
{
hasAccessByRoles([AccessControlRoles.admin, AccessControlRoles.common]) && (
<Typography.Text code>
{t("access.adminVisible.title")}
&
{t("access.commonVisible.title")}
</Typography.Text>
)
}
{
hasAccessByRoles([AccessControlRoles.admin]) && (
<Typography.Text code>
{t("access.adminVisible.title")}
</Typography.Text>
)
}
{
hasAccessByRoles(AccessControlRoles.common) && (
<Typography.Text code>
{t("access.commonVisible.title")}
</Typography.Text>
)
}
</>
);
}