React18管理系统权限管理

2,742 阅读6分钟

前言

做管理系统必定绕不开权限管理这一块,本文详细介绍了对路由权限,接口权限,菜单栏权限,动态路由设置,按钮权限五个模块。

一、后端设计

设计思路:

在用户登录时,将token,用户等级以及菜单栏与按钮权限映射关系返回给前端;

token用来判别前端是否登录,用户等级决定前端动态路由,菜单栏与按钮权限映射关系决定前端菜单栏展示,权限按钮展示。

1. 搭建node服务

  1. 新建server文件夹

  2. 在终端中运行

npm init -y

  1. 安装express

npm i express -s

  1. 新建app.js
const express = require("express");
const app = express();

app.get("/", (req, res) => {
  res.send("Hello World");
});

app.listen(4000, () => {
  console.log("server is running");
});
  1. 安装nodemon

npm i nodemon -g

  1. 修改server/package.json
  "scripts": {
    "dev": "nodemon app.js"
  },

运行:npm run dev 访问:127.0.0.1:4000,可以看到页面显示“Hello World”

2. 配置路由

1. 新建routes.js

const express = require("express");

const router = express.Router();

let accessToken = "init_s_token"; //定义token
let role = ""; //定义用户等级
let premissions = {}; //定义菜单列表和按钮权限

/* 5s刷新一次token */
setInterval(() => {
  accessToken = "s_tk" + Math.random();
}, 5000);

router.get("/login", function (req, res) {
  const { name } = req.query;
  switch (name) {
    case "admin":
      role = "admin";
      //管理员能看到首页,声明页和管理页
      premissions = {
        "/home": [],
        "/declare": ["admin"], //声明页的按钮只有管理员可看
        "/manage": [],
      };
      break;
    default:
      role = "visitor";
      //游客只能看到首页,声明页
      premissions = {
        "/home": [],
        "/declare": ["admin"], 
      };
      break;
  }
  res.json({
    role,
    accessToken,
    premissions,
  });
});

router.get("/getData", function (req, res) {
  let { authorization } = req.headers;
  if (authorization !== accessToken) {
    res.json({
      returncode: 104,
      info: "token过期,重新登录",
    });
  } else {
    res.json({
      code: 200,
      returncode: 0,
      data: { id: Math.random() },
    });
  }
});

module.exports = router;

2. 修改app.js

//删除
app.get("/", (req, res) => {
    res.send("Hello World");
  });
  
  //新增
  const routes = require("./routes");
  app.use("/", routes);

3. 跨域处理

  1. 安装

npm i cors

  1. 修改app.js
//新增
const cors = require('cors'); 

app.use(cors()); 

最终app.js文件

const express = require("express");
const app = express();

const routes = require("./routes");
const cors = require('cors'); 

app.use(cors()); 
app.use("/", routes); // 注入登录路由模块

app.listen(4000, () => {
  console.log("server is running");
});

二、前端设计

技术准备:

1. 定义使用到的常量

  1. 新建config/constant.ts
//localStorage存储字段
export const ACCESS_TOKEN = "tk"; //存token
export const ROLE = 'role'; //存用户等级
export const PREMISSIONS = 'premissions';//存菜单列表和按钮权限

//HTTP请求头字段
export const AUTH = "authorization";
  1. 新建config/returnCodeMap.ts
//接口状态码
export const CODE_RELOGIN = 104;// 需重新登陆
  1. 新建config/menus.ts
const menus = [
    {
      key: "/home",
      name: "首页",
    },
    {
      key: "/declare",
      name: "说明页",
    },
    {
      key: "/manage",
      name: "管理页",
    },
  ];
  
  export default menus;

2. 安装axios

  1. 安装

yarn add axios

  1. 新建utils/axios.ts
//axios服务
import axios from "axios";

const service = axios.create({
  baseURL: "//127.0.0.1:4000",
  timeout: 30000,
});

export default service;
  1. 新建apis/index.ts
//定义接口
import service from "../utils/axios";

/* 登录接口 */
export const getLogin = (params: { name: string; password: string }) => {
  return service.get("/login", { params: params });
};

/* 获取应用数据接口 */
export const getData = () => {
  return service.get("/getData");
};

3. 调整目录结构

一个管理系统需要登录页,登录之后使用嵌套路由,layout用来布局,展示左侧菜单栏和头部用户信息,右侧用来展示页面内容,这样在路由切换时,菜单栏和头部可以保持不变

clipboard.png

在src目录下,新建pages/login.tsx,pages/home.tsx,pages/declare.tsx,pages/manage.tsx(管理员才能访问的页面),layout.tsx

4. 调整路由

  1. 安装路由

yarn add react-router-dom

  1. 新建components/lazyComponent.tsx
import { Suspense, LazyExoticComponent } from "react";

export default function LazyComponent(props: { lazyChildren: LazyExoticComponent<() => JSX.Element> }) {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <props.lazyChildren />
    </Suspense>
  );
}

  1. 新建router/routes.tsx
import { lazy } from 'react';
import { Navigate } from "react-router-dom";

import LazyComponent from '../components/lazyComponent';

const routes = [
  {
    path: '/login',
    element: <LazyComponent lazyChildren={lazy(() => import('../pages/login'))} />,
  },
  {
    path: '/',
    element: <LazyComponent lazyChildren={lazy(() => import('../layout'))} />,
    children: [
      {
        index: true,
        element: <Navigate to={'/home'} replace={true} />,
      },
      {
        path: '/home',
        element: <LazyComponent lazyChildren={lazy(() => import('../pages/home'))} />,
      },
      {
        path: '/declare',
        element: <LazyComponent lazyChildren={lazy(() => import('../pages/declare'))} />,
      },
      {
        path: '/manage',
        element: <LazyComponent lazyChildren={lazy(() => import('../pages/manage'))} />,
      },
    ],
  },
];

export default routes;
  1. 新建router/index.ts
import { createBrowserRouter } from 'react-router-dom';

import routes from './routes';

const router = createBrowserRouter(routes);

export default router;
  1. 修改main.tsx
import { RouterProvider } from 'react-router-dom';

import ReactDOM from 'react-dom/client';

import router from './router';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(<RouterProvider router={router} />);

1. 路由权限设置

情景:当用户没有登录,直接访问页面时,重定向到登录页登录

思路:在布局路由loader钩子中,增加鉴权功能

新建router/loader.ts

import { redirect } from 'react-router-dom';

import { ACCESS_TOKEN } from '../config/constant';

export function protectedLoader() {
  //没有登录,跳转到登录页  
  if (!localStorage.getItem(ACCESS_TOKEN)) {
    return redirect('/login');
  }
  return null;
}

修改router/routes.tsx

import { protectedLoader } from "./loader";

const routes = [
  {
    path: "/",
    loader: protectedLoader, //新增
    element: <LazyComponent lazyChildren={lazy(() => import("../layout"))} />,
    //...
  },
];

2. 接口权限设置

情景:当token过期时,需用户重新登录

思路:在请求拦截器中,将token添加到请求头中;在响应拦截器中,判断状态码决定是否跳转到登录页

1. 增加请求拦截器和响应拦截器

修改utils/axios.ts

//axios服务
import axios from "axios";
//新增
import { CODE_RELOGIN } from '../config/returnCodeMap';
import { ACCESS_TOKEN, AUTH } from '../config/constant';

const service = axios.create({
  baseURL: "//127.0.0.1:4000",
  timeout: 30000,
});

service.interceptors.request.use(
    (config) => {
      const tk = localStorage.getItem(ACCESS_TOKEN);
      tk &&
        Object.assign(config.headers, {
          [AUTH]: tk,
        });
      return config;
    },
    (error) => {
      return Promise.reject(error);
    },
  );
  
  service.interceptors.response.use(
    (res) => {
      if (res.data.returncode === CODE_RELOGIN) {
        window.location.href = '/login'
      }
      return res;
    },
    (error) => {
      return Promise.reject(error);
    },
  );

export default service;

2. 登录页

修改pages/login.tsx

import { useState } from "react";
import { useNavigate } from 'react-router-dom';
import { getLogin } from "../apis";
import { ACCESS_TOKEN, ROLE, PREMISSIONS } from "../config/constant";

export default function Login() {
  const [user, setUser] = useState({ name: "", password: "" });
  const navigation = useNavigate();

  const sumbit = () => {
    getLogin(user).then((res) => {
      localStorage.setItem(ACCESS_TOKEN, res.data.accessToken);
      localStorage.setItem(ROLE, res.data.role);
      localStorage.setItem(PREMISSIONS, JSON.stringify(res.data.premissions));
      navigation("/home");
    });
  };

  return (
    <>
      <div style={{ height: 170, marginTop: 60, textAlign: "center" }}>XXXX管理系统</div>
      <div style={{ textAlign: "center" }}>
        姓名:
        <input onChange={(e) => setUser((prev) => ({ ...prev, name: e.target.value }))} />
        <br />
        密码:
        <input onChange={(e) => setUser((prev) => ({ ...prev, password: e.target.value }))}  />
        <br />
        <button onClick={sumbit}>提交</button>
      </div>
    </>
  );
}

3. 首页

修改pages/home.tsx

import { getData } from "../apis";

export default function Home() {
  return (
    <div>
      Home页面
      <button onClick={getData}>提交</button>
    </div>
  );
}

3. 菜单栏权限设置

情景:不同级别用户看到不同菜单栏

思路:前端通过返回的菜单栏列表,去封装一个新的菜单栏数组

1. 新建layout.css

.layout {
    height: 100vh;
  }
  
  header {
    background: #f4f4f5;
    height: 70px;
  }
  
  main {
    display: flex;
    height: 100%;
  }
  
  aside {
    width: 150px;
    background: gray;
    height: 100%;
  }
  
  aside .active {
    color: red;
  }
  
  article {
    flex: 1;
  }

2. 修改layout.tsx

import { Outlet, NavLink, useNavigate } from "react-router-dom";
import { ACCESS_TOKEN, ROLE, PREMISSIONS } from "./config/constant";
import menus from "./config/menus";
import { useMemo } from "react";
import "./layout.css";

export default function Layout() {
  const navigation = useNavigate();

  const newMenus = useMemo(() => {
    //获取菜单栏和按钮权限信息
    const premissions = JSON.parse(localStorage.getItem(PREMISSIONS) || "{}");
    //获取菜单栏权限信息
    const keys = Object.keys(premissions);
    return menus.filter((o) => keys.includes(o.key));
  }, []);

  const exit = () => {
    navigation("/login");
    //退出登陆时,清除存储信息
    localStorage.removeItem(ACCESS_TOKEN);
    localStorage.removeItem(ROLE);
    localStorage.removeItem(PREMISSIONS);
  };
  return (
    <div className="layout">
      <header>
        <button style={{ float: "right" }} onClick={exit}>
          退出
        </button>
      </header>
      <main>
        <aside>
          {newMenus.map((o) => (
            <NavLink to={o.key} key={o.key} style={{ display: "block" }}>
              {o.name}
            </NavLink>
          ))}
        </aside>
        <article>
          <Outlet />
        </article>
      </main>
    </div>
  );
}

这时通过用户名为 admin 的账户登录能看到三个菜单,其他用户只能看到两个

4. 页面权限设置

情景:管理员能访问管理页面路由,非管理员访问管理页面路由重定向到首页

思路:在页面路由loader钩子中,增加鉴权功能

1. 管理员页面

修改pages/manage.tsx

export default function Manage() {
    return <div>管理员才能看到的页面</div>;
  }

2. 无权限提示页面

新建pages/nopermission.tsx

export default function Nopermission() {
    return <div>权限不够</div>;
  }

3. 修改router/loader.ts

//...
//新增
import { ACCESS_TOKEN,ROLE } from '../config/constant';

export function manageLoader(){
  //权限不够,跳转到无权限提示页
  if(localStorage.getItem(ROLE) !=='admin'){
    return redirect('/nopermission');
  }
  return null;
}

4. 修改router/routes.tsx

import { protectedLoader, manageLoader } from "./loader";

const routes = [
  {
    path: "/",
    loader: protectedLoader,
    element: <LazyComponent lazyChildren={lazy(() => import("../layout"))} />,
    children: [
    //...
      {
        path: "/manage",
        loader: manageLoader, //新增
        element: <LazyComponent lazyChildren={lazy(() => import("../pages/manage"))} />,
      },
      {
        path: "/nopermission",//新增
        element: <LazyComponent lazyChildren={lazy(() => import("../pages/nopermission"))} />,
      }
    ],
  },
];

5. 按钮权限设置

情景:根据不同的用户,一些页面功能进行显示或者隐藏

思路:使用高阶组件检测用户权限

1. 新建components/authButton

import { memo, ReactNode } from "react";
import { useLocation } from "react-router-dom";
import { ROLE, PREMISSIONS } from "../config/constant";

export default memo(function AuthButton({ children }: { children: ReactNode }) {
  const { pathname } = useLocation();

  //获取菜单栏和按钮权限信息
  const premissions = JSON.parse(localStorage.getItem(PREMISSIONS) || "{}");

  //获取按钮权限信息
  const auth = premissions[pathname];

  if (!auth.includes(localStorage.getItem(ROLE))) return null;

  return <>{children}</>;
});

2. 修改pages/declare.tsx

import AuthButton from '../components/authButton'

export default function Declare(){
    return <div>说明页
        <AuthButton>
            <button>管理员按钮</button>
        </AuthButton>
    </div>
}

结尾

效果演示:

1.gif

如果有更好的思路,欢迎大家在评论区留言