前言
做管理系统必定绕不开权限管理这一块,本文详细介绍了对路由权限,接口权限,菜单栏权限,动态路由设置,按钮权限五个模块。
一、后端设计
设计思路:
在用户登录时,将token,用户等级以及菜单栏列表返回给前端;
token用来判别前端是否登录,用户等级决定前端动态路由,菜单栏列表决定前端菜单栏展示
1. 搭建node服务
-
在vue3项目中,新建一个server文件夹(与src同级)
-
新建一个终端,通过命令行
cd server
进入server文件夹
-
运行
npm init -y
初始化packag.json包 -
安装koa
npm i koa -s
-
新建server/index.js
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
ctx.body = "这是一个应用中间件";
await next();
});
app.listen(4000, () => {
console.log("server is listening on port 4000");
});
5. 安装nodemon npm i nodemon -g
- 修改server/package.json
"scripts": {
"dev": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
运行:npm run dev 访问:127.0.0.1:4000,可以看到页面显示这是一个应用中间件
2. 使用路由中间件
-
安装
npm i koa-router -S
-
新建server/routes/index.js
const router = require("koa-router")();
let accessToken = "init_s_token"; //定义token
let role = ""; //定义用户等级
let menus = []; //定义菜单列表
/* 5s刷新一次token */
setInterval(() => {
accessToken = "s_tk" + Math.random();
}, 5000);
/* 登录接口获取token */
router.get("/login", async (ctx) => {
const { name } = ctx.query;
switch (name) {
case "admin":
role = "admin";
menus = ["home", "about", "manage"]; //管理员能看到首页,说明页和管理页
break;
default:
role = "visitor";
menus = ["home", "about"]; //游客只能看到首页,说明页
break;
}
ctx.body = {
accessToken,
role,
menus,
};
});
/* 获取应用数据 */
router.get("/getData", async (ctx) => {
let { authorization } = ctx.headers;
if (authorization !== accessToken) {
ctx.body = {
returncode: 104,
info: "token过期,重新登录",
};
} else {
ctx.body = {
code: 200,
returncode: 0,
data: { id: Math.random() },
};
}
});
module.exports = router;
3. 修改server/index.js
//删除
app.use(async (ctx, next) => {
ctx.body = "这是一个应用中间件";
await next();
});
//新增
const index = require("./routes/index");
app.use(index.routes(), index.allowedMethods());
3. 跨域处理
-
安装
npm i koa2-cors
-
修改server/index.js
//新增
const cors = require("koa2-cors");
app.use(cors());
最终server/index.js文件
const Koa = require("koa");
const app = new Koa();
const index = require("./routes/index");
const cors = require("koa2-cors");
app.use(cors());
app.use(index.routes(), index.allowedMethods());
app.listen(4000, () => {
console.log("server is listening on port 4000");
});
目录结构:
重新运行 npm run dev,这时服务端已准备好
从截图中可以看出起了两个服务,一个前端本地服务,一个node服务
二、前端设计
技术准备:
- 定义使用到的常量
新建src/config/constant.js
//localStorage存储字段
export const ACCESS_TOKEN = "tk"; //存token
export const ROLE = 'role'; //存用户等级
export const MENUS = 'menus';//存菜单列表
//HTTP请求头字段
export const AUTH = "Authorization";
新建src/config/returnCodeMap.js
//接口状态码
export const CODE_LOGGED_OTHER = 106;// 在其它客户端被登录
export const CODE_RELOGIN = 104;// 重新登陆
新建src/config/menus.js
const menus = [
{
path: "/home",
key: "home",
name: "首页",
},
{
path: "/about",
key: "about",
name: "说明页",
},
{
path: "/manage",
key: "manage",
name: "管理页",
},
];
export default menus
2. 安装axios服务
安装 npm i axios
新建src/service/index.js
//axios服务
import axios from "axios";
const service = axios.create({
baseURL: "//127.0.0.1:4000",
timeout: 30000,
});
export default service;
新建src/service/api.js
//定义接口
import service from "./index";
const API = {};
/* 登录接口 */
API.getLogin = (params) => {
return service.get("/login", { params: params });
};
/* 获取应用数据接口 */
API.getData = () => {
return service.get("/getData");
};
export default API;
3. 调整目录结构
一个管理系统需要登录页,登录之后使用嵌套路由,layout用来布局,展示左侧菜单栏和头部用户信息,右侧用来展示页面内容,这样在路由切换时,菜单栏和头部可以保持不变
新建src/Layout.vue,src/views/Login.vue,src/views/Manage.vue(管理员才能访问的页面),还有创建项目自带的About.vue和Home.vue
- 调整路由
新建src/router/routes.js
//配置路由
const Login = () =>
import(/* webpackChunkName: "login" */ "../views/Login.vue");
const Home = () =>
import(/* webpackChunkName: "home" */ "../views/Home.vue");
const About = () =>
import(/* webpackChunkName: "about"*/ "../views/About.vue")
const Layout = () =>
import(/* webpackChunkName: "layout" */ "../Layout.vue");
const routes = [
{
path: "/",
redirect: "/home",
},
{
path: "/login",
component: Login,
},
{
path: "/layout",
name: "Layout",
component: Layout,
children: [
{
path: "/home",
name: "Home",
component: Home,
//存放按钮权限信息
meta: {
btnPermissions: ['admin', 'visitor']
},
},
{
path: "/about",
name: "About",
component: About,
meta: {
btnPermissions: ['admin']
},
},
],
},
];
export default routes;
修改src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import routes from "./routes";
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
新建src/router/dynamicRoute.js
//动态路由
const manage = {
path: "/manage",
name: "manage",
component: () =>
import(/* webpackChunkName:"manage" */ "../views/Manage.vue"),
};
export default manage
新建终端,启动前端服务。这时页面能出来了,接下来进行权限管理
1. 路由权限设置
情景:当用户没有登录,直接访问页面时,重定向到登录页登录
思路:在路由全局前置钩子中,增加鉴权功能
修改src/router/index.js
import { ACCESS_TOKEN, ROLE, MENUS } from "../config/constant";
router.beforeEach((to, from, next) => {
if (to.path === "/login") {
//在登录页清除存储信息
localStorage.removeItem(ACCESS_TOKEN);
localStorage.removeItem(ROLE);
localStorage.removeItem(MENUS);
}
let token = localStorage.getItem(ACCESS_TOKEN);
//没有token,则重定向到登录页
if (!token && to.path !== "/login") {
next({
path: "/login",
});
} else {
next();
}
});
2. 接口权限设置
情景:当token过期时,需用户重新登录
思路:在请求拦截器中,将token添加到请求头中;在响应拦截器中,判断状态码决定是否跳转到登录页
- 增加请求拦截器和响应拦截器
修改src/service/index.js
//新增
import { CODE_LOGGED_OTHER, CODE_RELOGIN } from "../config/returnCodeMap";
import { ACCESS_TOKEN, AUTH } from "../config/constant";
import router from "../router";
service.interceptors.request.use(
(config) => {
let { headers } = config;
const tk = localStorage.getItem(ACCESS_TOKEN);
tk &&
Object.assign(headers, {
[AUTH]: tk,
});
return config;
},
(error) => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
(res) => {
let { data } = res;
if (
data.returncode === CODE_RELOGIN ||
data.returncode === CODE_LOGGED_OTHER
) {
router.push("/login");
//清除动态路由缓存
location.reload();
}
return res;
},
(error) => {
return Promise.reject(error);
}
);
2. 登录页
修改src/views/Login.vue
<template>
<div style="height: 170px; margin-top: 60px; text-align: center">
XXXX管理系统
</div>
<div style="text-align: center">
姓名:<input v-model="user.name" />
<br />
密码:<input v-model="user.password" />
<br />
<button @click="sumbit">提交</button>
</div>
</template>
<script>
import { reactive } from "vue";
import { useRouter } from "vue-router";
import API from "../service/api";
import { ACCESS_TOKEN, ROLE, MENUS } from "../config/constant";
import manageRoute from "../router/dynamicRoute";
export default {
setup() {
const router = useRouter();
const user = reactive({ name: "", password: "" });
const sumbit = () => {
API.getLogin(user).then((res) => {
localStorage.setItem(ACCESS_TOKEN, res.data.accessToken);
localStorage.setItem(ROLE, res.data.role);
localStorage.setItem(MENUS, JSON.stringify(res.data.menus));
if (res.data.role === "admin") {
router.addRoute("Layout", manageRoute);
}
router.push("/home");
});
};
return { user, sumbit };
},
};
</script>
3. 首页
修改src/views/Home.vue
<template>
<div class="home">
Home页面
<button @click="sumbit">提交</button>
</div>
</template>
<script>
import { reactive } from "vue";
import API from "../service/api";
export default {
setup() {
const user = reactive({ name: "", password: "" });
const sumbit = () => {
console.log(user);
API.getData('/getData')
};
return { user, sumbit };
},
}
</script>
访问http://127.0.0.1:8080/login 进入登录页,登录之后过5s在首页点击获取数据按钮(token在服务端上设置了5s的过期时间),后端判断token是否过期,过期返回过期状态码,响应拦截器根据状态码跳转到登录页
3. 菜单栏权限设置
情景:不同级别用户看到不同菜单栏
思路:前端通过返回的菜单栏列表,去封装一个新的菜单栏数组
修改src/Layout.vue
<template>
<div id="home">
<header>
<button style="float: right" @click="exit">退出</button>
</header>
<main>
<aside>
<ul style="list-style: none">
<li v-for="(item,index) in newMenus" :key="index">
<router-link :to="item.path">{{item.name}}</router-link>
</li>
</ul>
</aside>
<article>
<router-view />
</article>
</main>
</div>
</template>
<script>
import menus from "./config/menus";
import { MENUS } from "./config/constant";
export default {
data() {
return {
newMenus:[]
};
},
created() {
const menuKeys = JSON.parse(localStorage.getItem(MENUS));
menus.forEach((item) => {
if (item.key && menuKeys.includes(item.key)) this.newMenus.push(item);
});
},
methods: {
exit() {
this.$router.push("/login");
//清除动态路由缓存
location.reload();
},
},
};
</script>
<style>
#home {
height: 100vh;
}
header {
background: #f4f4f5;
height: 70px;
}
main {
display: flex;
height: 100%;
}
aside {
width: 150px;
background: gray;
height: 100%;
}
article {
flex: 1;
}
</style>
这时通过用户名为 admin 的账户登录能看到三个菜单,其他用户只能看到两个
4. 动态路由设置
情景:管理员能访问管理页面路由,非管理员不能访问该路由
思路:通过router.addRoute添加动态路由
- 修改src/App.vue
<template>
<router-view />
</template>
<style>
body {
margin: 0;
}
</style>
2. 管理员页面
修改src/views/Manage.vue
<template>
<div>管理员才能看到的页面</div>
</template>
3. 添加动态路由
在src/views/Login.vue中新增(上面Login.vue文件已经加上了,这里单独拎出来展示)
import manageRoute from '../router/manageRoute'
//如果是管理员,添加管理员页面路由
if (res.data.role === "admin") {
router.addRoute("Layout", manageRoute);
}
提示:这里用的是vue3,对应的vue-router是4.x版本,使用addRoute添加动态路由。vue2对应的vue-router是3.x版本,使用addRoutes添加动态路由
- 解决刷新页面,动态路由丢失
原因:刷新页面,路由初始化,动态路由会丢失
思路:通过监听路由的变化,当刷新时,添加动态路由并定位到管理页面
修改src/App.vue
//新增
<script>
import { ROLE } from "./config/constant";
import manage from "./router/dynamicRoute";
export default {
watch: {
$route: {
async handler(newVal) {
console.log("newVal", newVal);
const role = localStorage.getItem(ROLE);
if (role && role === "admin") {
/* 在4.x版本中需手动调用router.replace方法重定向,
因为动态路由页面刷新时,matched的值为空;
在3.x版本中,刷新页面添加异步路由,matched有值,不需要再重定向 */
this.$router.addRoute("Layout", manage);
/* 在动态路由页面刷新时,matched数组为空 */
if (!newVal.matched.length && newVal.fullPath === "/manage") {
await this.$router.replace("/manage");
}
}
},
},
},
};
</script>
5. 按钮权限设置
情景:根据不同的用户,一些页面功能进行显示或者隐藏
思路:在路由元信息上定义权限信息,通过自定义指令删除一些DOM节点
- 定义路由元信息(上面routes.js中已经添加了)
{
path: "/about",
name: "About",
component: About,
meta: {
btnPermissions: ['admin']
},
},
2. 增加判断方法
新建src/utils/index.js
import { ROLE } from "../config/constant";
// 权限检查方法
export function has(value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = localStorage.getItem(ROLE);
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
}
3. 新建自定义指令
修改src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { has } from "./utils";
const app = createApp(App);
app.directive("has", {
mounted(el) {
// 获取页面按钮权限
const btnPermissionsArr = router.currentRoute._value.meta.btnPermissions;
if (!has(btnPermissionsArr)) {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
},
});
app.use(router).mount("#app");
4. 在about页面使用v-has指令
修改src/views/About.vue
<template>
<div class="about">
<h1>This is an about page</h1>
<button type="button" v-has>管理员按钮</button>
</div>
</template>
效果演示:
欢迎关注:之后文章会首发在云在前端公众号,未经许可禁止转载!