17.1 概述

在使用JSP/Serverlet、PHP、ASP等技术开发Web应用系统时,由于前端页面及代码是在后端动态生成的,权限控制相对简单,可以在生成(运行)时进行判断。但本教程讨论的前后端逻辑分离的设计方法使得前端代码已经在发布前预先生成,因此需要在在前端设计中进行更多的工作。

17.2 @umijs/plugin-access插件机制

在Ant Design Pro构建应用程序的的时候,已经启用了umijs的权限插件plugin-access,我们使用它来实现前端系统的权限定义和控制。

本节代码都是示意性质的,不是实际使用的

17.2.1 权限定义文件

@umijs/plugin-access约定 src/access.ts 为权限定义文件,该文件需要默认导出一个方法,导出的方法会在项目初始化时被执行。该方法需要返回一个对象,对象的每一个值就对应定义了一条权限。下面是文档中的示意代码

  1. // src/access.ts
  2. export default function(initialState) {
  3. const { userId, role } = initialState;
  4. return {
  5. canReadFoo: true,
  6. canUpdateFoo: role === 'admin',
  7. canDeleteFoo: foo => {
  8. return foo.ownerId === userId;
  9. },
  10. };
  11. }

其中 initialState 是通过初始化状态插件 @umijs/plugin-initial-state 提供的数据,你可以使用该数据来初始化你的用户权限。

17.2.2 扩展路由配置

完成权限定义以后,可以很简单的在路由配置中实现针对某些页面的权限控制。如下所示,只有拥有了 canReadPageA 权限,用户才可以访问该页面。否则会默认渲染内置的权限错误页面。

  1. // config/route.ts
  2. export const routes = [
  3. {
  4. path: '/pageA',
  5. component: 'PageA',
  6. access: 'canReadPageA', // 权限定义返回值的某个 key
  7. }
  8. ]

17.2.3 在组件中获取权限信息

使用一个专门的Reace Hook useAccess 能够在组件中获取权限相关信息,如下所示:

  1. import React from 'react';
  2. import { useAccess } from 'umi';
  3. const PageA = props => {
  4. const { foo } = props;
  5. const access = useAccess();
  6. if (access.canReadFoo) {
  7. // 如果可以读取 Foo,则...
  8. }
  9. return <>TODO</>;
  10. };
  11. export default PageA;

17.2.4 使用Access组件控制权限

可以在业务组件中使用插件提供的 React hook useAccess 以及组件 <Access /> 对应用进行权限控制。组件 Access 支持的属性如下:

  • accessible: boolean

是否有权限,通常通过 useAccess 获取后传入进来。

  • fallback: React.ReactNode

无权限时的显示,默认无权限不显示任何内容。

  • children: React.ReactNode

有权限时的显示。

  1. import React from 'react';
  2. import { useAccess, Access } from 'umi';
  3. const PageA = props => {
  4. const { foo } = props;
  5. const access = useAccess(); // access 的成员: canReadFoo, canUpdateFoo, canDeleteFoo
  6. if (access.canReadFoo) {
  7. // 如果可以读取 Foo,则...
  8. }
  9. return (
  10. <div>
  11. <Access
  12. accessible={access.canReadFoo}
  13. fallback={<div>Can not read foo content.</div>}
  14. >
  15. Foo content.
  16. </Access>
  17. <Access
  18. accessible={access.canUpdateFoo}
  19. fallback={<div>Can not update foo.</div>}
  20. >
  21. Update foo.
  22. </Access>
  23. <Access
  24. accessible={access.canDeleteFoo(foo)}
  25. fallback={<div>Can not delete foo.</div>}
  26. >
  27. Delete foo.
  28. </Access>
  29. </div>
  30. );
  31. };

useAccess()的返回值 access 就是第三步中定义的权限集合,可以利用它进行组件内代码执行流的控制。<Access>组件拥有 accessiblefallback 两个属性,当 accessibletrue 时会渲染子组件,当 accessiblefalse 会渲染 fallback 属性对应的 ReactNode。

17.3 模拟实现前端权限控制

本节我们给当前代码中的会员列表、查询会员以及上传附件加上权限控制来体验一下实际的控制方法(都是需要修改、录入的实际代码)。

17.3.1 权限控制目标

本节设计的权限控制目标如下:

  • admin用户对会员数据和附件文件具有读写的权限
  • user用户只有会员数据的只读权限,不具有上传附件的权限

    17.3.2 在Mock中返回权限

    修改mock/user.ts中的getUserInfo函数 ```diff function getUserInfo(req: Request, res: Response) {
  • enum ModuleRight {
  • NORIGHT = 0,
  • READONLY = 1,
  • WRITABLE = 2,
  • } +
  • let authorization = undefined
  • if(req.headers[‘token’] == adminToken) {
  • authorization = {
  • member: ModuleRight.WRITABLE,
  • upload: ModuleRight.WRITABLE,
  • }
  • } else {
  • authorization = {
  • member: ModuleRight.READONLY,
  • upload: ModuleRight.NORIGHT,
  • }
  • }
  • res.send({ name: ‘Serati Ma’,
  • authorization,

    1. <a name="vWN6O"></a>
    2. ### 17.3.3 修改权限定义文件
    3. 删除`src/access.ts`原有的内容并填入如下的我代码:
    4. ```typescript
    5. /**
    6. * @see https://umijs.org/zh-CN/plugins/plugin-access
    7. * */
    8. export default function access(initialState: any) {
    9. const result = {}
    10. const authorization = initialState?.currentUser?.authorization;
    11. if(authorization != undefined) {
    12. result['memberRaadable'] = authorization?.member > 0
    13. result['memberWritable'] = authorization?.member > 1
    14. result['uploadRaadable'] = authorization?.upload > 0
    15. result['uploadWritable'] = authorization?.upload > 1
    16. }
    17. return result;
    18. }

    17.3.4 在路由中配置权限

    给config/routes.ts做如下的修改 ```diff { name: ‘会员列表’, icon: ‘smile’, path: ‘/memberlist’, component: ‘./MemberList’,

  • access: ‘memberRaadable’, }, { name: ‘数据查询’, icon: ‘smile’, path: ‘/dataquery’, component: ‘./MemberList/components/query.tsx’,
  • access: ‘memberRaadable’, }, { path: ‘/upload’, name: ‘上传文件’, icon: ‘upload’, routes: [ {
    1. path: '/upload/upload01',
    2. name: '上传附件',
    3. component: './Upload/upload01.tsx',
  • access: ‘uploadWritable’, },
    1. <a name="IffsG"></a>
    2. ### 17.3.5 权限控制效果
    3. 此时用`admin`用户登录前端操作没有变化,改用`user`用户登录,在左侧“上传文件”的菜单中已经看不到“上传附件”的菜单项:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12725763/1620045190078-1c4c2c8f-23bc-49a1-b6fa-533054a87aa6.png#clientId=u8d8b1852-b90f-4&from=paste&height=204&id=u5f9a6e0e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=408&originWidth=456&originalType=binary&size=20689&status=done&style=none&taskId=u3824211c-d84f-4d0b-bd3c-92e33d1b6a7&width=228)<br />直接访问upload/upload01地址,将得到“无权访问”的错误提示<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12725763/1620045151078-59cb5705-ae08-417e-adff-c0aea93d277f.png#clientId=u8d8b1852-b90f-4&from=paste&height=747&id=uff5e8303&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1494&originWidth=1334&originalType=binary&size=154010&status=done&style=none&taskId=uce317293-b034-4ab9-b2d5-4657c8dfa87&width=667)
    4. <a name="DrFV3"></a>
    5. ## 17.4 更细力度地控制权限
    6. 在管理会员信息页面判断权限<br />在src/pages/MemberList/index.tsx和src/pages/MemberList/components/query.tsx中引用
    7. ```typescript
    8. import { useAccess, Access } from 'umi';
    在src/pages/MemberList/index.tsx中 ```diff const MemberList: React.FC = () => {
  • const access:any = useAccess(); 在src/pages/MemberList/components/query.tsx中diff const MemberQuery: React.FC = () => {
  • const access:any = useAccess(); 在src/pages/MemberList/index.tsx中diff
  • ,
    1. src/pages/MemberList/index.tsxsrc/pages/MemberList/components/query.tsx
    2. ```typescript
    3. <Access accessible={access.memberWritable}>
    4. <Button
    5. onClick={async () => {
    6. Modal.confirm({
    7. title: '删除提示',
    8. icon: <ExclamationCircleOutlined />,
    9. content: '你确实要删除这些选中的数据么?',
    10. okText: '是的',
    11. okType: 'danger',
    12. cancelText: '不了',
    13. onOk: async () => {
    14. await handleDeleteList(selectedRows);
    15. setSelectedRows([]);
    16. actionRef.current?.reloadAndRest?.();
    17. },
    18. onCancel() { },
    19. });
    20. }}
    21. >
    22. 批量删除
    23. </Button>
    24. </Access>
    1. <Access accessible={access.memberWritable}>
    2. <PopConfirm
    3. placement="rightTop"
    4. title={'确实要删除这一条么?'}
    5. onConfirm={async () => {
    6. await handleDeleteCurrent(record.id as number);
    7. actionRef.current?.reloadAndRest?.();
    8. setShowDetail(false);
    9. }}
    10. okText="是的"
    11. cancelText="取消"
    12. >
    13. <a
    14. key="delete"
    15. >删除</a>
    16. </PopConfirm>
    17. </Access>,
    18. <Access accessible={access.memberWritable}>
    19. <a
    20. key="update"
    21. onClick={() => {
    22. setShowDetail(false);
    23. setFormState({ showModal: true, operation: 'edit', record: record })
    24. }}
    25. >修改</a>
    26. </Access>,
    需要说明的是,这里没有彻底关掉“操作”列的原因在于实际的项目场景中,可能会存在其他读权限的操作。

    17.5 重要的说明

    不管前端做了多少的安全设计,但对于后端来说这些都是不可靠的——后端程序要默认前端是不安全到,永远不要相信前端传递的任何信息,所有的信息安全防范措施必须在后端从0开始

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。