SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(1)权限管理
SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(2)角色管理
1、动态菜单、动态路由,即,不同的用户登录展示其相应的菜单
1.1 admin-管理员登录
1.2 aaa-普通用户登录
2、实现动态路由 router -> index.ts
import { createRouter, createWebHistory } from 'vue-router'
import menuApi from '@/api/menu'
import userApi from '@/api/user'
import { useUserInfoStore } from '@/stores/userInfo'
import { useMenuStore } from '@/stores/menu'
import { useTokenStore } from '@/stores/token'
import { ElMessage } from 'element-plus'
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
// @ts-ignore
component: () => import("@/views/login/index.vue")
},
{
path: '/home',
component: () => import("@/views/index.vue"),
children: [] as any
}
];
const router = createRouter({
// 创建路由工作模式
history: createWebHistory(),
routes
})
// 全局前置路由守卫
router.beforeEach(async(to,from,next) => {
// 登录路由放行
if(to.path==='/login'){
next();
}else{
// router总是比pinia之前创建,延迟调用pinia
const menuStore=useMenuStore();
const userInfoStore=useUserInfoStore();
// 如果没有用户信息缓存(对象判空),则请求,然后缓存
if(userInfoStore && JSON.stringify(userInfoStore.userInfo) === "{}"){
const response= await userApi.userInfo();
// 缓存用户信息
userInfoStore.set(response.data);
}
// 如果没有菜单信息缓存(数组判空),则请求,然后缓存
if(menuStore && menuStore.routeList.length===0 && menuStore.menuList.length===0){
const routeResponse= await menuApi.getMyRoute();
// 管理没分配菜单权限
if(routeResponse.data.length===0){
ElMessage.error("暂无菜单权限,请联系管理员");
await userApi.logout();
useTokenStore().remove();
useUserInfoStore().remove();
menuStore.remove();
router.push('/login');
}else{
const menuResponse= await menuApi.getMyMenu();
// 缓存菜单、路由
menuStore.setMenu(menuResponse.data);
menuStore.setRoute(routeResponse.data);
// vite中的动态引入组件的方法 import.meta.glob()
const loadView = import.meta.glob('../views/**/*.vue');
// 构造 请求的所有路由都是 /home 的子路由
const homeRoute=routes.filter((item:any)=>item.path==='/home')[0];
homeRoute.children=[];
menuStore.routeList.forEach((item:any) => {
homeRoute.children.push({
path: item.path,
name: item.menuName,
component: loadView[`../views${item.path}.vue`]
})
});
// homeRoute.redirect='/article/index';
// 动态添加路由
router.addRoute(homeRoute);
// 重新执行 router.beforeEach
next({...to});
}
}else{
next();
}
}
})
export default router
3、实现动态菜单 views -> index.vue
<template>
<el-container class="container">
<!-- 菜单栏 -->
<el-aside width="200px">
<!-- default-active="/article/index" -->
<el-menu default-active="/home/index" active-text-color="#ffd04b" background-color="black" text-color="white" router>
<div v-for="(item,index) in myMenu" :key="item.id">
<!-- 一级菜单 -->
<el-menu-item :index="item.path" v-if="item.type!=1" >
<component :is="getIconComponent(item.icon)" style="height: 15px;" />
<span class="text">{{ item.menuName }}</span>
</el-menu-item>
<!-- 二级菜单 -->
<el-sub-menu :index="index.toString()" v-else>
<template #title>
<component :is="getIconComponent(item.icon)" style="height: 15px;" />
<span class="text">{{ item.menuName }}</span>
</template>
<el-menu-item :index="child.path" v-for="child in item.children" :key="child.id">
<component :is="getIconComponent(child.icon)" style="height: 15px;" />
<span class="text">{{ child.menuName }}</span>
</el-menu-item>
</el-sub-menu>
</div>
</el-menu>
</el-aside>
<!-- 主体区域 -->
<el-container>
<el-header class="header">
<el-image style="height: 60px" src="@/assets/logo1.jpg" />
<div class="right">
<div class="username">{{ userInfoStore.userInfo.username }}</div>
<el-dropdown>
<span class="span">
<el-avatar :src="userInfoStore.userInfo.avatarUrl" fit="fill"/>
<el-icon><CaretBottom/></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="SwitchButton" @click="logout">注销</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main">
<router-view></router-view>
</el-main>
<el-footer class="footer">
@2024 Created by Dragon
</el-footer>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { SwitchButton,CaretBottom } from '@element-plus/icons-vue'
import userApi from '@/api/user'
import router from '@/router';
import { useUserInfoStore } from '@/stores/userInfo'
import { useTokenStore } from '@/stores/token'
import { ElMessageBox} from 'element-plus'
import { useMenuStore } from '@/stores/menu'
import * as Icons from '@element-plus/icons-vue';
// 缓存读取
const userInfoStore = useUserInfoStore() as any
const menuStore=useMenuStore()
const myMenu=menuStore.menuList as any
const logout= async()=>{
ElMessageBox.confirm(
'是否注销?',
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async() => {
await userApi.logout();
useTokenStore().remove();
useUserInfoStore().remove();
menuStore.remove();
router.push('/login');
})
}
// 获取图标组件
const getIconComponent = (icon: string) => {
// selectedIcon.value=icon;
return (Icons as any)[icon] || null;
};
</script>
<style scoped lang="less">
.container{
height: 100vh;
// width: 100vw; // 不要设置宽度,不然会有滚动条
.el-aside{
background-color: black;
.el-menu{
border-right: none;
}
}
.header{
background-color: white;
display: flex;
align-items: center;
justify-content: space-between;
.right{
display: flex;
align-items: center;
justify-content: center;
}
.username{
margin-right: 20px;
}
.span{
display: flex;
align-items: center;
justify-content: center;
.el-icon{
margin-left: 5px;
}
&:active,
&:focus {
outline: none;
}
}
}
.footer{
display: flex;
align-items: center;
justify-content: center;
}
.a {
text-decoration: none;
}
.router-link{
text-decoration: none;
}
}
.text{
margin-left: 10px;
}
</style>
4、Vue 3 前端代码补充
4.1 stores -> munu.ts(pinia缓存)
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useMenuStore = defineStore(
'menuStore',
() => {
const menuList = ref([]);
const routeList = ref([]);
const setMenu= (value:any)=>{
menuList.value=value;
}
const setRoute= (value:any)=>{
routeList.value=value;
}
const remove= ()=>{
menuList.value=[];
routeList.value=[];
}
return { menuList,routeList,setMenu, setRoute,remove }
},
// 动态路由不能开持久化,刷新之后动态路由丢失,才会重新加载全局前置路由守卫
// {
// persist: true
// }
)
4.2 封装 request 请求(utils -> request.ts)
import axios from 'axios'
import { ElMessage } from 'element-plus'
import {useTokenStore} from '@/stores/token'
import router from '@/router';
// 创建请求实例
let request = axios.create({
baseURL:"http://localhost:8080"
})
// 添加 request 拦截器
request.interceptors.request.use(
config=>{
if(useTokenStore().token){
config.headers['X-Token'] = useTokenStore().token;
}
return config;
},
error=>{
return Promise.reject(error);
}
)
// 添加 response 拦截器
request.interceptors.response.use(
response=>{
if(response.data.code === 200){
return response.data;
}else if(response.data.code === 401){
ElMessage.error('请先登录');
router.push('/login');
return Promise.reject(response.data);
}else{
ElMessage.error(response.data.msg || '服务异常');
return Promise.reject(response.data);
}
},
error=>{
if(error.response.status === 401){
ElMessage.error('请先登录');
}else{
ElMessage.error('服务异常');
}
return Promise.reject(error);
}
)
export default request
4.3 调用 request 请求(api -> menu.ts)
import request from "@/utils/request";
// 菜单管理 API
export default{
menuList(value:any){
return request.post('/menu/list',value);
},
saveOrUpdate(value:any){
return request.post('/menu/saveOrUpdate',value);
},
remove(ids:any){
return request.delete('/menu/remove',{ data:{ ids } });
},
getMyMenu(){
return request.get('/menu/getMyMenu');
},
getMyRoute(){
return request.get('/menu/getMyRoute');
}
}