RBAC(Role-Based Access Control)是一种广泛使用的访问控制机制,其核心思想是根据用户的角色来分配系统访问权限。 在RBAC模型中,权限不是直接分配给个人,而是分配给角色,然后用户通过成为角色的成员来获得这些权限。这种模型简化了权限管理,并提高了灵活性和可维护性。
RBAC模型通常包含以下基本组件:
用户(Users): 系统的操作者。一个用户可以是一个人,也可以是一个服务账户,代表自动化的系统组件。
角色(Roles): 一个角色代表了一组权限的集合,通常与组织中的工作职责相对应。例如,“管理员”、“编辑”、“访客”等都是可能的角色。
权限(Permissions): 权限是对系统资源的访问控制,它描述了可以执行的操作,如读取、写入、编辑或删除。
实施RBAC的步骤通常包括:
识别角色: 确定组织中的不同角色以及它们的职责。
定义权限: 明确每个角色需要哪些权限才能履行其职责。
创建角色和分配权限: 在系统中创建角色,并给它们分配相应的权限。
分配角色给用户: 根据用户的工作职责将一个或多个角色分配给用户。
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');
mkdir umi4-auth
cd umi4-auth
npm init -y
npm install @ant-design/pro-components ahooks jsonwebtoken @umijs/max
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
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
+ }
}
/node_modules
/src/.umi
/dist
{
"scripts": {
"dev": "max dev",
"build": "max build"
},
}
>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
config\config.ts
import { defineConfig } from '@umijs/max';
import routes from "./routes";
export default defineConfig({
npmClient: 'npm',
routes,
styledComponents:{}
});
config\routes.ts
export default [
{ path: "/", redirect: "/home" },
{
icon: "HomeOutlined",
name: "首页",
path: "/home",
component: "./home/index",
},
{
name: "登录",
path: "/signin",
component: "./signin/index"
},
];
config\config.ts
import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
npmClient: "npm",
routes,
styledComponents: {},
+ antd:{}
});
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>
);
}
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,
+ },
+ },
});
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,
});
}
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>
);
}
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;
}
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: {}
});
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>
);
}
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: {}
});
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;
}
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>
);
}
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>
+ )
+ ];
+ },
+ };
+};
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
},
];
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
+ },
});
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,
+ },
];
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];
+ },
+ },
};
};
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: {}
});
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'
},
];
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)
};
}
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;
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
+ });
+}
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),
};
}
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),
};
}
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;
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];
},
},
};
};