SpringBoot3+Vue3 前后端分离项目基于RBAC权限访问控制-实现动态路由、动态菜单

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(1)权限管理

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(2)角色管理

SpringBoot3+Vue3 前后端分离项目实现基于RBAC权限访问控制-(3)用户管理

SpringBoot3+Vue3 前后端分离项目基于RBAC权限访问控制-实现动态路由、动态菜单

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');
  }
}

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
权限管理系统是一种用于管理用户权限和角色的系统,可以根据用户的身份和角色来控制其访问系统中的各种资源。基于SpringBootVue和Redis的前后端分离模式,可以更好地实现权限管理系统的功能。 在这个系统中,SpringBoot作为后端框架,提供了强大的功能和稳定的性能,可以处理用户的请求并进行权限验证。Vue作为前端框架,提供了友好的界面和良好的用户体验,可以让用户方便地进行权限管理操作。而Redis作为缓存数据库,可以用来存储权限信息和用户的登录状态,加快系统的响应速度和提高系统的性能。 在权限管理系统中,我们可以使用RBAC(基于角色的权限控制)模型,将用户分配到不同的角色,再将角色分配到不同的权限,从而实现对用户访问资源的控制。通过这种方式,可以实现灵活的权限管理,并且可以根据实际需求动态地调整用户的权限和角色。 通过使用SpringBootVue,我们可以实现前后端分离,让前端和后端分别进行开发和维护,降低了系统的耦合度,同时也增加了系统的灵活性和可维护性。而通过使用Redis,我们可以充分利用其高速的读取和写入能力,有效地提升系统的性能和响应速度。 综上所述,基于SpringBootVue和Redis的权限管理系统,可以实现灵活、高效和安全的权限管理功能,满足用户对于权限管理的各种需求。同时,前后端分离模式也使得系统更加灵活和易于维护。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值