1.RBAC #

RBAC(Role-Based Access Control)是一种广泛使用的访问控制机制,其核心思想是根据用户的角色来分配系统访问权限。 在RBAC模型中,权限不是直接分配给个人,而是分配给角色,然后用户通过成为角色的成员来获得这些权限。这种模型简化了权限管理,并提高了灵活性和可维护性。

RBAC模型通常包含以下基本组件:

  1. 用户(Users): 系统的操作者。一个用户可以是一个人,也可以是一个服务账户,代表自动化的系统组件。

  2. 角色(Roles): 一个角色代表了一组权限的集合,通常与组织中的工作职责相对应。例如,“管理员”、“编辑”、“访客”等都是可能的角色。

  3. 权限(Permissions): 权限是对系统资源的访问控制,它描述了可以执行的操作,如读取、写入、编辑或删除。

实施RBAC的步骤通常包括:

  1. 识别角色: 确定组织中的不同角色以及它们的职责。

  2. 定义权限: 明确每个角色需要哪些权限才能履行其职责。

  3. 创建角色和分配权限: 在系统中创建角色,并给它们分配相应的权限。

  4. 分配角色给用户: 根据用户的工作职责将一个或多个角色分配给用户。

2.数据库设计 #

CREATE TABLE `api`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

INSERT INTO `api` VALUES (1, 'DELETE', '/api/user/\\w+');
INSERT INTO `api` VALUES (2, 'GET', '/api/user');
INSERT INTO `api` VALUES (3, 'GET', '/api/menus');

CREATE TABLE `menu`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;


INSERT INTO `menu` VALUES (1, '用户管理', '/user', './user/index', 'UserOutlined');
INSERT INTO `menu` VALUES (2, '角色管理', '/role', './role/index', 'UserOutlined');
INSERT INTO `menu` VALUES (3, '菜单管理', '/menu', './menu/index', 'UserOutlined');

CREATE TABLE `role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = COMPACT;

INSERT INTO `role` VALUES (1, 'root', '超级管理员');
INSERT INTO `role` VALUES (2, 'admin', '普通管理员');
INSERT INTO `role` VALUES (3, 'member', '普通用户');

CREATE TABLE `role_api`  (
  `role_id` int(11) NOT NULL,
  `api_id` int(255) NOT NULL,
  PRIMARY KEY (`role_id`, `api_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = COMPACT;

INSERT INTO `role_api` VALUES (1, 1);
INSERT INTO `role_api` VALUES (1, 2);
INSERT INTO `role_api` VALUES (1, 3);

CREATE TABLE `role_menu`  (
  `role_id` int(11) NOT NULL,
  `menu_id` int(255) NOT NULL,
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = COMPACT;

INSERT INTO `role_menu` VALUES (1, 1);
INSERT INTO `role_menu` VALUES (1, 2);
INSERT INTO `role_menu` VALUES (1, 3);
INSERT INTO `role_menu` VALUES (2, 1);

CREATE TABLE `role_user`  (
  `role_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = COMPACT;

INSERT INTO `role_user` VALUES (1, 1);
INSERT INTO `role_user` VALUES (2, 2);
INSERT INTO `role_user` VALUES (3, 3);

CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = COMPACT;

INSERT INTO `user` VALUES (1, 'root', 'root', 'https://static.zhufengpeixun.com/root_1699509490373.png');
INSERT INTO `user` VALUES (2, 'admin', 'admin', 'https://static.zhufengpeixun.com/admin_1699509526348.jpg');
INSERT INTO `user` VALUES (3, 'member', 'member', 'https://static.zhufengpeixun.com/yong_hu_1699509499656.jpg');

3.接口文档 #

4.创建项目 #

4.1 初始化项目 #

mkdir umi4-auth
cd umi4-auth
npm init -y 
npm install @ant-design/pro-components ahooks jsonwebtoken @umijs/max

4.2 生成首页 #

mkdir src
max g 
√ Pick generator type » Create Pages -- Create a umi page by page name
√ What is the name of page? ... home
√ How dou you want page files to be created? » home\index.{tsx,less}
Write: src\pages\home\index.less
Write: src\pages\home\index.styled-components.tsx
删除 src\pages\home\index.styled-components.tsx

4.3 支持typescript #

max g
√ Pick generator type » Enable Typescript -- Setup tsconfig.json
info  - Update package.json for devDependencies
info  - Write tsconfig.json
info  - Write typings.d.ts

tsconfig.json

{
  "extends": "./src/.umi/tsconfig.json",
+ "compilerOptions": {
+   "noImplicitAny": false
+ }
}

4.4 .gitignore #

/node_modules
/src/.umi
/dist

4.4 添加命令 #

{
  "scripts": {
    "dev": "max dev",
    "build": "max build"
  },
}

http://localhost:8000/home

5.配置登录页 #

5.1 生成登录页 #

>max g
√ Pick generator type » Create Pages -- Create a umi page by page name
√ What is the name of page? ... signin
√ How dou you want page files to be created? » signin\index.{tsx,less}
Write: src\pages\signin\index.less
Write: src\pages\signin\index.styled-components.tsx
Write: src\pages\signin\index.tsx

5.2 config.ts #

config\config.ts

import { defineConfig } from '@umijs/max';
import routes from "./routes";
export default defineConfig({
  npmClient: 'npm',
  routes,
  styledComponents:{}
});

5.3 routes.ts #

config\routes.ts

export default [
  { path: "/", redirect: "/home" },
  {
    icon: "HomeOutlined",
    name: "首页",
    path: "/home",
    component: "./home/index",
  },
  {
    name: "登录",
    path: "/signin",
    component: "./signin/index" 
   },
];

5.绘制登录页 #

5.1 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
+ antd:{}
});

5.2 signin\index.tsx #

src\pages\signin\index.tsx

import { Form, Input, Button, Card, Row, Col } from "antd";
export default function () {
  const onFinish = (values: any) => {
    console.log(values);
  };
  const onFinishFailed = (errorInfo: any) => {
    console.log("Failed:", errorInfo);
  };
  return (
    <Row style={{marginTop:'20%'}}>
      <Col offset={8} span={8}>
        <Card title="请登录">
          <Form
            labelCol={{ span: 4 }}
            wrapperCol={{ span: 20 }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
          >
            <Form.Item
              label="用户名"
              name="username"
              rules={[{ required: true, message: "请输入用户名" }]}
            >
              <Input />
            </Form.Item>
            <Form.Item
              label="密码"
              name="password"
              rules={[{ required: true, message: "请输入密码" }]}
            >
              <Input.Password />
            </Form.Item>
            <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
              <Button type="primary" htmlType="submit">
                提交
              </Button>
            </Form.Item>
          </Form>
        </Card>
      </Col>
    </Row>
  );
}

5.发送请求 #

5.1 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
  antd: {},
+ request: {},
+ proxy: {
+   "/api/": {
+     target: "http://127.0.0.1:7001/",
+     changeOrigin: true,
+   },
+ },
});

5.2 user.ts #

src\services\user.ts

import { request } from "@umijs/max";
export async function signin(body) {
  return request("/api/signin", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    data: body,
  });
}

5.3 signin\index.tsx #

src\pages\signin\index.tsx

+import { Form, Input, Button, Card, Row, Col,Spin } from "antd";
+import { useRequest } from "ahooks";
+import { signin } from "@/services/user";
export default function () {
+ const { loading, run } = useRequest(signin, {
+   manual: true,
+   onSuccess(result) {
+     console.log(result);
+   },
+ });
+ const onFinish = (values: any) => {
+   run(values);
+ };
  const onFinishFailed = (errorInfo: any) => {
    console.log("Failed:", errorInfo);
  };
  return (
    <Row style={{ marginTop: "20%" }}>
      <Col offset={8} span={8}>
        <Card title="请登录">
+         <Spin spinning={loading}>
            <Form
              labelCol={{ span: 4 }}
              wrapperCol={{ span: 20 }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
            >
              <Form.Item
                label="用户名"
                name="username"
                rules={[{ required: true, message: "请输入用户名" }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="密码"
                name="password"
                rules={[{ required: true, message: "请输入密码" }]}
              >
                <Input.Password />
              </Form.Item>
              <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                <Button type="primary" htmlType="submit">
                  提交
                </Button>
              </Form.Item>
            </Form>
+         </Spin>
        </Card>
      </Col>
    </Row>
  );
}

6.发送登录请求 #

6.1 app.tsx #

src\app.tsx

import { decode } from "jsonwebtoken";
export async function getInitialState() {
  let initialState = {
    currentUser: null,
  };
  const tokens = localStorage.getItem("tokens");
  if (tokens) {
    const { access_token } = JSON.parse(tokens);
    const { currentUser } = decode(access_token);
    initialState.currentUser = currentUser;
  }
  return initialState;
}

6.2 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
  antd: {},
  request: {},
  proxy: {
    "/api/": {
      target: "http://127.0.0.1:7001/",
      changeOrigin: true,
    },
  },
+ model: {},
+ initialState: {}
});

6.3 signin\index.tsx #

src\pages\signin\index.tsx

import { Form, Input, Button, Card, Row, Col, Spin } from "antd";
import { useRequest } from "ahooks";
import { signin } from "@/services/user";
+import { useModel, history } from "@umijs/max";
+import { decode } from "jsonwebtoken";
+import { useEffect } from "react";
export default function () {
+ const { initialState, setInitialState } = useModel("@@initialState");
  const { loading, run } = useRequest(signin, {
    manual: true,
    onSuccess(result) {
+     const tokens = result.data;
+     localStorage.setItem("tokens", JSON.stringify(tokens));
+     const {currentUser} = decode(tokens.access_token);
+     setInitialState({ currentUser });
    },
  });
+ useEffect(() => {
+   if (initialState?.currentUser) 
+   history.push("/");
+ }, [initialState]);
  const onFinish = (values: any) => {
    run(values);
  };
  const onFinishFailed = (errorInfo: any) => {
    console.log("Failed:", errorInfo);
  };
  return (
    <Row style={{ marginTop: "20%" }}>
      <Col offset={8} span={8}>
        <Card title="请登录">
          <Spin spinning={loading}>
            <Form
              labelCol={{ span: 4 }}
              wrapperCol={{ span: 20 }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
            >
              <Form.Item
                label="用户名"
                name="username"
                rules={[{ required: true, message: "请输入用户名" }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="密码"
                name="password"
                rules={[{ required: true, message: "请输入密码" }]}
              >
                <Input.Password />
              </Form.Item>
              <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                <Button type="primary" htmlType="submit">
                  提交
                </Button>
              </Form.Item>
            </Form>
          </Spin>
        </Card>
      </Col>
    </Row>
  );
}

7.配置请求 #

7.1 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
  antd: {},
+ request: {dataField: 'data'},
  proxy: {
    "/api/": {
      target: "http://127.0.0.1:7001/",
      changeOrigin: true,
    },
  },
  model: {},
  initialState: {}
});

7.2 app.tsx #

src\app.tsx

import { decode } from "jsonwebtoken";
+import { notification, message } from "antd";
+import { history, request as requestMethod } from "@umijs/max";
+enum ErrorShowType {
+  SILENT = 0,
+  WARN_MESSAGE = 1,
+  ERROR_MESSAGE = 2,
+  NOTIFICATION = 3,
+}
+let refreshTokenPromise: any = null;
+const errorHandler = (error: any) => {
+  if (error.name === "BizError") {
+    const errorInfo = error.info;
+    if (errorInfo) {
+      const { errorMessage, errorCode } = errorInfo;
+      switch (errorInfo.showType) {
+        case ErrorShowType.SILENT:
+          break;
+        case ErrorShowType.WARN_MESSAGE:
+          message.warning(errorMessage);
+          break;
+        case ErrorShowType.ERROR_MESSAGE:
+          message.error(errorMessage);
+          break;
+        case ErrorShowType.NOTIFICATION:
+          notification.open({
+            description: errorMessage,
+            message: errorCode,
+          });
+          break;
+        default:
+          message.error(errorMessage);
+      }
+    }
+  } else if (error.response) {
+    const { status } = error.response;
+    if (status === 401) {
+      if (!refreshTokenPromise) {
+        const tokens = localStorage.getItem("tokens");
+        if (tokens) {
+          const { refresh_token } = JSON.parse(tokens);
+          refreshTokenPromise = requestMethod("/refresh-token", {
+            method: "POST",
+            data: { refresh_token },
+          })
+            .then((response) => {
+              localStorage.setItem("tokens", JSON.stringify(response));
+              return requestMethod(error.config);
+            })
+            .catch(() => {
+              localStorage.removeItem("tokens");
+              history.push("/signin");
+            })
+            .finally(() => {
+              refreshTokenPromise = null;
+            });
+        } else {
+          history.push("/signin");
+        }
+      }
+      return refreshTokenPromise;
+    } else if (status === 400) {
+      notification.error({ message: "请求参数错误" });
+    } else if (status === 403) {
+      notification.error({ message: "没有权限,请联系管理员" });
+    } else if (status === 404) {
+      notification.error({ message: "请求资源不存在" });
+    } else if (status >= 500) {
+      notification.error({ message: "服务端错误,请联系管理员" });
+    }
+  } else if (error.request) {
+    message.error("没有收到响应");
+  } else {
+    message.error("请求发送失败");
+  }
+};
+const errorThrower = (res: any) => {
+  const { success, data, errorCode, errorMessage } = res;
+  if (!success) {
+    const error: any = new Error(errorMessage);
+    error.name = "BizError";
+    error.info = { errorCode, errorMessage, data };
+    throw error;
+  }
+};
+export let request = {
+  timeout: 3000,
+  headers: {
+    ["Content-Type"]: "application/json",
+    ["Accept"]: "application/json",
+    credentials: "include",
+  },
+  errorConfig: {
+    errorThrower,
+    errorHandler,
+  },
+  requestInterceptors: [
+    (url, options) => {
+      const tokens = localStorage.getItem("tokens");
+      if (tokens) {
+        const { access_token } = JSON.parse(tokens);
+        options.headers.authorization = `Bearer ${access_token}`;
+      }
+      return { url, options };
+    },
+  ],
+  responseInterceptors: [
+    (response) => {
+      return response.data;
+    },
+  ],
+};
export async function getInitialState() {
  let initialState = {
    currentUser: null,
  };
  try {
    const tokens = localStorage.getItem("tokens");
    if (tokens) {
      const { access_token } = JSON.parse(tokens);
       const { currentUser } = decode(access_token);
      initialState.currentUser = currentUser;
    }
  } catch (error) {
    console.error("Error decoding access token:", error);
  }
  return initialState;
}

7.3 signin\index.tsx #

src\pages\signin\index.tsx

import { Form, Input, Button, Card, Row, Col, Spin } from "antd";
import { useRequest } from "ahooks";
import { signin } from "@/services/user";
import { useModel, history } from "@umijs/max";
import { decode } from "jsonwebtoken";
import { useEffect } from "react";
export default function () {
  const { initialState, setInitialState } = useModel("@@initialState");
  const { loading, run } = useRequest(signin, {
    manual: true,
+   onSuccess(response) {
+     localStorage.setItem("tokens", JSON.stringify(response));
+     const {currentUser} = decode(response.access_token);
      setInitialState({ currentUser });
    },
  });
  useEffect(() => {
    if (initialState?.currentUser) 
    history.push("/");
  }, [initialState]);
  const onFinish = (values: any) => {
    run(values);
  };
  const onFinishFailed = (errorInfo: any) => {
    console.log("Failed:", errorInfo);
  };
  return (
    <Row style={{ marginTop: "20%" }}>
      <Col offset={8} span={8}>
        <Card title="请登录">
          <Spin spinning={loading}>
            <Form
              labelCol={{ span: 4 }}
              wrapperCol={{ span: 20 }}
              onFinish={onFinish}
              onFinishFailed={onFinishFailed}
            >
              <Form.Item
                label="用户名"
                name="username"
                rules={[{ required: true, message: "请输入用户名" }]}
              >
                <Input />
              </Form.Item>
              <Form.Item
                label="密码"
                name="password"
                rules={[{ required: true, message: "请输入密码" }]}
              >
                <Input.Password />
              </Form.Item>
              <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                <Button type="primary" htmlType="submit">
                  提交
                </Button>
              </Form.Item>
            </Form>
          </Spin>
        </Card>
      </Col>
    </Row>
  );
}

8.后台布局和退出 #

8.1 app.tsx #

src\app.tsx

import { decode } from "jsonwebtoken";
import { notification, message, Dropdown, Avatar, Menu, Space } from "antd";
import { history, request as requestMethod } from "@umijs/max";
enum ErrorShowType {
  SILENT = 0,
  WARN_MESSAGE = 1,
  ERROR_MESSAGE = 2,
  NOTIFICATION = 3,
}
let refreshTokenPromise: any = null;
const errorHandler = (error: any) => {
  if (error.name === "BizError") {
    const errorInfo = error.info;
    if (errorInfo) {
      const { errorMessage, errorCode } = errorInfo;
      switch (errorInfo.showType) {
        case ErrorShowType.SILENT:
          break;
        case ErrorShowType.WARN_MESSAGE:
          message.warning(errorMessage);
          break;
        case ErrorShowType.ERROR_MESSAGE:
          message.error(errorMessage);
          break;
        case ErrorShowType.NOTIFICATION:
          notification.open({
            description: errorMessage,
            message: errorCode,
          });
          break;
        default:
          message.error(errorMessage);
      }
    }
  } else if (error.response) {
    const { status } = error.response;
    if (status === 401) {
      if (!refreshTokenPromise) {
        const tokens = localStorage.getItem("tokens");
        if (tokens) {
          const { refresh_token } = JSON.parse(tokens);
          refreshTokenPromise = requestMethod("/refresh-token", {
            method: "POST",
            data: { refresh_token },
          })
            .then((response) => {
              localStorage.setItem("tokens", JSON.stringify(response));
              return requestMethod(error.config);
            })
            .catch(() => {
              localStorage.removeItem("tokens");
              history.push("/signin");
            })
            .finally(() => {
              refreshTokenPromise = null;
            });
        } else {
          history.push("/signin");
        }
      }
      return refreshTokenPromise;
    } else if (status === 400) {
      notification.error({ message: "请求参数错误" });
    } else if (status === 403) {
      notification.error({ message: "没有权限,请联系管理员" });
    } else if (status === 404) {
      notification.error({ message: "请求资源不存在" });
    } else if (status >= 500) {
      notification.error({ message: "服务端错误,请联系管理员" });
    }
  } else if (error.request) {
    message.error("没有收到响应");
  } else {
    message.error("请求发送失败");
  }
};
const errorThrower = (res: any) => {
  const { success, data, errorCode, errorMessage } = res;
  if (!success) {
    const error: any = new Error(errorMessage);
    error.name = "BizError";
    error.info = { errorCode, errorMessage, data };
    throw error;
  }
};
export let request = {
  timeout: 3000,
  headers: {
    ["Content-Type"]: "application/json",
    ["Accept"]: "application/json",
    credentials: "include",
  },
  errorConfig: {
    errorThrower,
    errorHandler,
  },
  requestInterceptors: [
    (url, options) => {
      const tokens = localStorage.getItem("tokens");
      if (tokens) {
        const { access_token } = JSON.parse(tokens);
        options.headers.authorization = `Bearer ${access_token}`;
      }
      return { url, options };
    },
  ],
  responseInterceptors: [
    (response) => {
      return response.data;
    },
  ],
};
export async function getInitialState() {
  let initialState = {
    currentUser: null,
  };
  try {
    const tokens = localStorage.getItem("tokens");
    if (tokens) {
      const { access_token } = JSON.parse(tokens);
      const { currentUser } = decode(access_token);
      initialState.currentUser = currentUser;
    }
  } catch (error) {
    console.error("Error decoding access token:", error);
  }
  return initialState;
}
+export const layout = ({ initialState, setInitialState }) => {
+  return {
+    title: "UMI4",
+    onPageChange(location) {
+      const { currentUser } = initialState;
+      if (!currentUser && location.pathname !== "/signin") {
+        history.push("/signin");
+      }
+    },
+    actionsRender: () => {
+      const { currentUser } = initialState;
+      if (!currentUser) return null;
+      const items = [
+        {
+          key: "logout",
+          label: (
+            <a
+              onClick={() => {
+                setInitialState({ currentUser: null });
+                localStorage.removeItem("tokens");
+                history.push("/signin");
+              }}
+            >
+              退出登录
+            </a>
+          ),
+        },
+      ];
+      return [
+        (
+          <Dropdown menu={{ items }}>
+            <Space>
+              <Avatar size={32} src={currentUser?.avatar} />
+              {currentUser?.username}
+            </Space>
+          </Dropdown>
+        )
+      ];
+    },
+  };
+};

8.2 routes.ts #

config\routes.ts

export default [
  { path: "/", redirect: "/home" },
  {
    icon: "HomeOutlined",
    name: "首页",
    path: "/home",
    component: "./home/index",
  },
  {
    name: "登录",
    path: "/signin",
    component: "./signin/index",
+   hideInMenu: true,
+   layout: false
   },
];

8.3 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
  antd: {},
  request: {dataField: 'data'},
  proxy: {
    "/api/": {
      target: "http://127.0.0.1:7001/",
      changeOrigin: true,
    },
  },
  model: {},
  initialState: {},
+ layout: {
+   name: "UMI4",
+   locale: true
+ },
});

9.菜单权限 #

9.1 routes.ts #

config\routes.ts

export default [
  { path: "/", redirect: "/home" },
  {
    icon: "HomeOutlined",
    name: "首页",
    path: "/home",
    component: "./home/index",
  },
  {
    name: "登录",
    path: "/signin",
    component: "./signin/index",
    hideInMenu: true,
    layout: false
   },
+  {
+   name: "用户管理",
+   path: "/user",
+   component: "./user/index",
+   hideInMenu: true,
+  },
+  {
+   name: "角色管理",
+   path: "/role",
+   component: "./role/index",
+   hideInMenu: true,
+  },
+  {
+   name: "菜单管理",
+   path: "/menu",
+   component: "./menu/index",
+   hideInMenu: true,
+  },
];

9.2 src\app.tsx #

src\app.tsx

import { decode } from "jsonwebtoken";
import { notification, message, Dropdown, Avatar, Menu, Space } from "antd";
import { history, request as requestMethod } from "@umijs/max";
+import Icon from "@ant-design/icons";
+import * as icons from "@ant-design/icons";
enum ErrorShowType {
  SILENT = 0,
  WARN_MESSAGE = 1,
  ERROR_MESSAGE = 2,
  NOTIFICATION = 3,
}
let refreshTokenPromise: any = null;
const errorHandler = (error: any) => {
  if (error.name === "BizError") {
    const errorInfo = error.info;
    if (errorInfo) {
      const { errorMessage, errorCode } = errorInfo;
      switch (errorInfo.showType) {
        case ErrorShowType.SILENT:
          break;
        case ErrorShowType.WARN_MESSAGE:
          message.warning(errorMessage);
          break;
        case ErrorShowType.ERROR_MESSAGE:
          message.error(errorMessage);
          break;
        case ErrorShowType.NOTIFICATION:
          notification.open({
            description: errorMessage,
            message: errorCode,
          });
          break;
        default:
          message.error(errorMessage);
      }
    }
  } else if (error.response) {
    const { status } = error.response;
    if (status === 401) {
      if (refreshTokenPromise) {
        refreshTokenPromise.then(() => {
          requestMethod(error.config);
        });
      }else{
        const tokens = localStorage.getItem("tokens");
        if (tokens) {
          const { refresh_token } = JSON.parse(tokens);
          refreshTokenPromise = requestMethod("/api/refresh-token", {
            method: "POST",
            data: { refresh_token },
          })
            .then((response) => {
              localStorage.setItem("tokens", JSON.stringify(response));
              return requestMethod(error.config);
            })
            .catch(() => {
              localStorage.removeItem("tokens");
              history.push("/signin");
            })
            .finally(() => {
              refreshTokenPromise = null;
            });
        } else {
          history.push("/signin");
        }
      }
      notification.error({ message: error.message });
    } else if (status === 400) {
      notification.error({ message: "请求参数错误" });
    } else if (status === 403) {
      notification.error({ message: "没有权限,请联系管理员" });
    } else if (status === 404) {
      notification.error({ message: "请求资源不存在" });
    } else if (status >= 500) {
      notification.error({ message: "服务端错误,请联系管理员" });
    }
  } else if (error.request) {
    message.error("没有收到响应");
  } else {
    message.error("请求发送失败");
  }
};
const errorThrower = (res: any) => {
  const { success, data, errorCode, errorMessage } = res;
  if (!success) {
    const error: any = new Error(errorMessage);
    error.name = "BizError";
    error.info = { errorCode, errorMessage, data };
    throw error;
  }
};
export let request = {
  timeout: 3000,
  headers: {
    ["Content-Type"]: "application/json",
    ["Accept"]: "application/json",
    credentials: "include",
  },
  errorConfig: {
    errorThrower,
    errorHandler,
  },
  requestInterceptors: [
    (url, options) => {
      const tokens = localStorage.getItem("tokens");
      if (tokens) {
        const { access_token } = JSON.parse(tokens);
        options.headers.authorization = `Bearer ${access_token}`;
      }
      return { url, options };
    },
  ],
  responseInterceptors: [
    (response) => {
      return response.data;
    },
  ],
};
export async function getInitialState() {
  let initialState = {
    currentUser: null,
  };
  try {
    const tokens = localStorage.getItem("tokens");
    if (tokens) {
      const { access_token } = JSON.parse(tokens);
      const { currentUser } = decode(access_token);
      initialState.currentUser = currentUser;
    }
  } catch (error) {
    console.error("Error decoding access token:", error);
  }
  return initialState;
}
+const formatMenuItem = (menus) =>(
+  menus.map(({ icon, routes, ...item }) => ({
+    ...item,
+    icon: icon && <Icon component={icons[icon]} />,
+    routes: routes && formatMenuItem(routes),
+  }))
+)

export const layout = ({ initialState, setInitialState }) => {
  return {
    title: "UMI4",
    onPageChange(location) {
      const { currentUser } = initialState;
      if (!currentUser && location.pathname !== "/signin") {
        history.push("/signin");
      }
    },
    actionsRender: () => {
      const { currentUser } = initialState;
      if (!currentUser) return null;
      const items = [
        {
          key: "logout",
          label: (
            <a
              onClick={() => {
                setInitialState({ currentUser: null });
                localStorage.removeItem("tokens");
                history.push("/signin");
              }}
            >
              退出登录
            </a>
          ),
        },
      ];
      return [
        <Dropdown menu={{ items }}>
          <Space>
            <Avatar size={32} src={currentUser?.avatar} />
            {currentUser?.username}
          </Space>
        </Dropdown>,
      ];
    },
+   menu: {
+     locale: false,
+     request: async (_, defaultMenuData) => {
+       const response = await requestMethod("/api/menus");
+       const dynamicMenus = formatMenuItem(response.menus);
+       return [...defaultMenuData, ...dynamicMenus];
+     },
+   },
  };
};

10.路由权限 #

10.1 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "npm",
  routes,
  styledComponents: {},
  antd: {},
  request: {dataField: 'data'},
  proxy: {
    "/api/": {
      target: "http://127.0.0.1:7001/",
      changeOrigin: true,
    },
  },
  model: {},
  initialState: {},
  layout: {
    name: "UMI4",
    locale: true
  },
+ access: {}
});

10.2 routes.ts #

config\routes.ts

export default [
  { path: "/", redirect: "/home" },
  {
    icon: "HomeOutlined",
    name: "首页",
    path: "/home",
    component: "./home/index",
  },
  {
    name: "登录",
    path: "/signin",
    component: "./signin/index",
    hideInMenu: true,
    layout: false
   },
   {
    name: "角色管理",
    path: "/role",
    component: "./role/index",
    hideInMenu: true,
+   access: 'canReadRole'
   },
   {
    name: "用户管理",
    path: "/user",
    component: "./user/index",
    hideInMenu: true,
+   access: 'canReadUser'
   },
   {
    name: "菜单管理",
    path: "/menu",
    component: "./menu/index",
    hideInMenu: true,
+   access: 'canReadMenu'
   },
];

10.3 access.ts #

src\access.ts

const ROLE_ROOT = 'root';
const ROLE_ADMIN = 'admin';
const ROLE_MEMBER = 'member';
export default function (initialState) {
    const roles = initialState?.currentUser?.roles;
    console.log('roles',roles);
    return {
        canReadRole: roles?.includes(ROLE_ROOT),
        canReadUser: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN),
        canReadMenu: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN)|| roles?.includes(ROLE_MEMBER)
    };
}

11.字段权限 #

11.1 user\index.tsx #

src\pages\user\index.tsx

import { useState, useEffect } from "react";
import { Table, message } from "antd";
import { useAccess } from "@umijs/max";
import { getUser } from "@/services/user";
const Users = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const access = useAccess();
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const response = await getUser();
      setData(response.list);
      setLoading(false);
    } catch (error) {
      setLoading(false);
      message.error("获取用户列表失败");
    }
  };
  useEffect(() => {
    fetchUsers();
  }, []);
  const columns = [
    {
      title: "用户名",
      dataIndex: "username",
    },
  ];
  if (access.canReadUserPassword) {
    columns.push({
      title: "密码",
      dataIndex: "password",
    });
  }
  return (
    <div>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        loading={loading}
      />
    </div>
  );
};
export default Users;

11.2 user.ts #

src\services\user.ts

import { request } from "@umijs/max";
export async function signin(body) {
  return request("/api/signin", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    data: body,
  });
}

+export async function getUser() {
+  return request('/api/user', {
+      method: 'GET'
+  });
+}
+
+export async function addUser(user) {
+  return request('/api/user', {
+      method: 'POST',
+      data: user,
+  });
+}
+
+export async function updateUser(id, user) {
+  return request(`/api/user/${id}`, {
+      method: 'PUT',
+      data: user,
+  });
+}
+
+export async function deleteUser(ids) {
+  return request(`/api/user/${ids[0]}`, {
+      method: 'DELETE',
+      data:ids
+  });
+}

11.3 access.ts #

src\access.ts

const ROLE_ROOT = 'root';
const ROLE_ADMIN = 'admin';
const ROLE_MEMBER = 'member';
export default function (initialState) {
    const roles = initialState?.currentUser?.roles;
    return {
        canReadRole: roles?.includes(ROLE_ROOT),
        canReadUser: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN),
        canReadMenu: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN)|| roles?.includes(ROLE_MEMBER),
+       canReadUserPassword: roles?.includes(ROLE_ROOT),
    };
}

12.按钮权限 #

12.1 src\access.ts #

src\access.ts

const ROLE_ROOT = 'root';
const ROLE_ADMIN = 'admin';
const ROLE_MEMBER = 'member';
export default function (initialState) {
    const roles = initialState?.currentUser?.roles;
    console.log('roles',roles);
    return {
        canReadRole: roles?.includes(ROLE_ROOT),
        canReadUser: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN),
        canReadMenu: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN)|| roles?.includes(ROLE_MEMBER),
        canReadUserPassword: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN),
+       canDeleteUser: roles?.includes(ROLE_ROOT) || roles?.includes(ROLE_ADMIN),
    };
}

12.2 user\index.tsx #

src\pages\user\index.tsx

import { useState, useEffect } from "react";
+import { Table, message,Popconfirm } from "antd";
import { useAccess } from "@umijs/max";
+import { getUser, deleteUser } from "@/services/user";
const Users = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const access = useAccess();
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const response = await getUser();
      setData(response.list);
      setLoading(false);
    } catch (error) {
      setLoading(false);
      message.error("获取用户列表失败");
    }
  };
  useEffect(() => {
    fetchUsers();
  }, []);
+ const handleDelete = async (id) => {
+   try {
+     await deleteUser([id]);
+     message.success("删除成功");
+   } catch (error) {
+     message.error("删除失败");
+   }
+   fetchUsers();
+ };
  const columns:any = [
    {
      title: "用户名",
      dataIndex: "username",
    },
  ];
  if (access.canReadUserPassword) {
    columns.push({
      title: "密码",
      dataIndex: "password",
    });
  }
+ if (access.canDeleteUser) {
+   columns.push({
+     title: "操作",
+     render: (_, record) => {
+       return (
+         <Popconfirm
+           title="确定要删除吗?"
+           onConfirm={() => handleDelete(record.id)}
+         >
+           <a>删除</a>
+         </Popconfirm>
+       );
+     },
+   });
+ }
  return (
    <div>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        loading={loading}
      />
    </div>
  );
};
export default Users;

13.接口权限 #

13.1 src\app.tsx #

src\app.tsx

import { decode } from "jsonwebtoken";
import { notification, message, Dropdown, Avatar, Menu, Space } from "antd";
import { history, request as requestMethod } from "@umijs/max";
import Icon from "@ant-design/icons";
import * as icons from "@ant-design/icons";
enum ErrorShowType {
  SILENT = 0,
  WARN_MESSAGE = 1,
  ERROR_MESSAGE = 2,
  NOTIFICATION = 3,
}
let refreshTokenPromise: any = null;
const errorHandler = (error: any) => {
  if (error.name === "BizError") {
    const errorInfo = error.info;
    if (errorInfo) {
      const { errorMessage, errorCode } = errorInfo;
      switch (errorInfo.showType) {
        case ErrorShowType.SILENT:
          break;
        case ErrorShowType.WARN_MESSAGE:
          message.warning(errorMessage);
          break;
        case ErrorShowType.ERROR_MESSAGE:
          message.error(errorMessage);
          break;
        case ErrorShowType.NOTIFICATION:
          notification.open({
            description: errorMessage,
            message: errorCode,
          });
          break;
        default:
          message.error(errorMessage);
      }
    }
  } else if (error.response) {
    const { status } = error.response;
    if (status === 401) {
      if (refreshTokenPromise) {
        refreshTokenPromise.then(() => {
          requestMethod(error.config);
        });
      } else {
        const tokens = localStorage.getItem("tokens");
        if (tokens) {
          const { refresh_token } = JSON.parse(tokens);
          refreshTokenPromise = requestMethod("/api/refresh-token", {
            method: "POST",
            data: { refresh_token },
          })
            .then((response) => {
              console.log("response", response);
              try {
                const tokens = JSON.stringify(response);
                localStorage.setItem("tokens", tokens);
              } catch (error) {
                console.error("Error parsing tokens:", error);
                localStorage.removeItem("tokens");
              }
              return requestMethod(error.config);
            })
            .catch(() => {
              localStorage.removeItem("tokens");
              history.push("/signin");
            })
            .finally(() => {
              refreshTokenPromise = null;
            });
        } else {
          history.push("/signin");
        }
      }
      notification.error({ message: error.message });
    } else if (status === 400) {
      notification.error({ message: "请求参数错误" });
    } else if (status === 403) {
      notification.error({ message: "没有权限,请联系管理员" });
    } else if (status === 404) {
      notification.error({ message: "请求资源不存在" });
    } else if (status >= 500) {
      notification.error({ message: "服务端错误,请联系管理员" });
    }
  } else if (error.request) {
    message.error("没有收到响应");
  } else {
    message.error("请求发送失败");
  }
};
const errorThrower = (res: any) => {
  const { success, data, errorCode, errorMessage } = res;
  if (!success) {
    const error: any = new Error(errorMessage);
    error.name = "BizError";
    error.info = { errorCode, errorMessage, data };
    throw error;
  }
};
const WHITE_LIST = ["/api/signin"];
export let request = {
  timeout: 3000,
  headers: {
    ["Content-Type"]: "application/json",
    ["Accept"]: "application/json",
    credentials: "include",
  },
  errorConfig: {
    errorThrower,
    errorHandler,
  },
  requestInterceptors: [
    (url, options) => {
+     try {
+       const tokens = localStorage.getItem("tokens");
+       if (tokens) {
+         const { access_token } = JSON.parse(tokens);
+         options.headers.authorization = `Bearer ${access_token}`;
+         const { currentUser } = decode(access_token);
+         const apis = currentUser?.apis || [];
+         let hasPermission =
+           WHITE_LIST.includes(url) ||
+           apis.some(({ method, path }) => {
+             return (
+               method.toLowerCase() === options.method.toLowerCase() &&
+               new RegExp(path).test(url)
+             );
+           });
+         if (!hasPermission) {
+           return;
+         }
+       }
+     } catch (error) {
+       console.error("Error decoding access token:", error);
+       localStorage.removeItem("tokens");
+     }
+     return { url, options };
+   },
  ],
  responseInterceptors: [
    (response) => {
      return response.data;
    },
  ],
};
export async function getInitialState() {
  let initialState = {
    currentUser: null,
  };
  try {
    const tokens = localStorage.getItem("tokens");
    if (tokens) {
      const { access_token } = JSON.parse(tokens);
      const { currentUser } = decode(access_token);
      initialState.currentUser = currentUser;
    }
  } catch (error) {
    console.error("Error decoding access token:", error);
  }
  return initialState;
}
const formatMenuItem = (menus) =>
  menus.map(({ icon, routes, ...item }) => ({
    ...item,
    icon: icon && <Icon component={icons[icon]} />,
    routes: routes && formatMenuItem(routes),
  }));

export const layout = ({ initialState, setInitialState }) => {
  return {
    title: "UMI4",
    onPageChange(location) {
      const { currentUser } = initialState;
      if (!currentUser && location.pathname !== "/signin") {
        history.push("/signin");
      }
    },
    actionsRender: () => {
      const { currentUser } = initialState;
      if (!currentUser) return null;
      const items = [
        {
          key: "logout",
          label: (
            <a
              onClick={() => {
                setInitialState({ currentUser: null });
                localStorage.removeItem("tokens");
                history.push("/signin");
              }}
            >
              退出登录
            </a>
          ),
        },
      ];
      return [
        <Dropdown menu={{ items }}>
          <Space>
            <Avatar size={32} src={currentUser?.avatar} />
            {currentUser?.username}
          </Space>
        </Dropdown>,
      ];
    },
    menu: {
      locale: false,
      request: async (_, defaultMenuData) => {
        const response = await requestMethod("/api/menus");
        const dynamicMenus = formatMenuItem(response.menus);
        return [...defaultMenuData, ...dynamicMenus];
      },
    },
  };
};