Spring Security + Vue2 + Element-UI 总结


title: Spring Security + Vue2 + Element-UI 总结
date: 2022-05-09 04:03:09
tags:

  • Spring
    categories:
  • Spring
    cover: https://cover.png
    feature: false

文章目录

  • 1. Vue UI 创建项目
  • 2. Vue 项目搭建
    • 2.1 引入 Element-UI、Axios、Mockjs、QS
      • 2.1.1 引入 Element-UI
      • 2.1.2 引入 Axios、[Mockjs](http://mockjs.com/?fileGuid=HXqVy6jx8qkWKPJq)
      • 2.1.3 引入 QS
    • 2.2 登录页
      • 2.2.1 新建登录组件
      • 2.2.2 添加路由
      • 2.2.3 App.vue
      • 2.2.4 访问登录页面
    • 2.3 登录校验
      • 2.3.1 图片验证码
      • 2.3.2 登录验证
      • 2.3.3 全局 Axios 拦截器
    • 2.4 整体布局
      • 2.4.1 主容器 Main.vue
      • 2.4.2 左侧菜单栏 NavAside.vue
      • 2.4.3 顶部菜单栏 NavHeader.vue
      • 2.4.4 展开/收缩侧边栏
    • 2.5 退出与动态导航路由、动态标签页绑定
      • 2.5.1 退出
      • 2.5.2 动态导航与动态路由绑定
      • 2.5.3 导航与动态标签页绑定
    • 2.6 菜单管理
    • 2.7 角色管理
    • 2.8 用户管理
  • 3. Spring Security 后端项目搭建
    • 3.1 执行流程图
    • 3.2 准备
      • 3.2.1 Security POM 依赖
      • 3.2.2 RedisConfig 配置类
      • 3.2.3 CorsConfig 跨域配置类
      • 3.2.4 MybatisPlusConfig 配置类
      • 3.2.5 RedisUtil 工具类
      • 3.2.6 Result 统一结果处理
    • 3.3 用户认证
      • 3.3.1 生成验证码
      • 3.3.2 登录成功处理器
      • 3.3.3 登录失败处理器
      • 3.3.4 图片验证码拦截器
      • 3.3.5 SecurityConfig 核心配置类
    • 3.5 异常处理
      • 3.5.1 自定义异常类
      • 3.5.2 全局异常类
      • 3.5.3 认证失败处理器
      • 3.5.4 权限失败处理器
    • 3.6 鉴权
      • 3.6.1 JWT 工具类
      • 3.6.2 JWT 过滤器
      • 3.6.3 UserDetailsServiceImpl 实现类
      • 3.6.4 UserDetails 实现类
      • 3.6.5 CustomLogoutSuccessHandler 注销成功处理器
      • 3.6.6 SecurityConfig 核心配置类完整配置
    • 3.7 权限管理
      • 3.7.1 菜单管理
      • 3.7.2 角色管理
      • 3.7.3 用户管理

Vue 基础部分见:https://blog.csdn.net/ACE_U_005A/article/details/123573568
Vue 开发部分见:https://blog.csdn.net/ACE_U_005A/article/details/123789272

1. Vue UI 创建项目

Vue UI 是 @vue/cli3.0 增加的一个可视化项目管理工具,可以运行项目、打包项目,检查等操作

1、在命令行输入 vue ui,运行
Spring Security + Vue2 + Element-UI 总结_第1张图片
2、进入可视化界面
Spring Security + Vue2 + Element-UI 总结_第2张图片
3、选择项目路径,新建项目
Spring Security + Vue2 + Element-UI 总结_第3张图片
4、输入项目名,下一步
Spring Security + Vue2 + Element-UI 总结_第4张图片
5、可以选择预设或者自定义,默认预设即可。这里选择手动,下一步
Spring Security + Vue2 + Element-UI 总结_第5张图片
6、选择功能,加上 Router 和 Vuex
Spring Security + Vue2 + Element-UI 总结_第6张图片
7、选择 Vue 的版本和路由的模式,这里选择 Vue2 与 不使用 History 模式
Spring Security + Vue2 + Element-UI 总结_第7张图片
8、不保存预设
Spring Security + Vue2 + Element-UI 总结_第8张图片
9、创建完成
Spring Security + Vue2 + Element-UI 总结_第9张图片
10、启动项目,选择 任务 --> serve --> 运行,后续进入项目可通过命令行启动
Spring Security + Vue2 + Element-UI 总结_第10张图片
11、启动成功
Spring Security + Vue2 + Element-UI 总结_第11张图片
12、访问 localhost:8080,初始界面,因没有选择 History 路由模式,地址会带上 # 号
Spring Security + Vue2 + Element-UI 总结_第12张图片
13、项目初始结构
Spring Security + Vue2 + Element-UI 总结_第13张图片

2. Vue 项目搭建

2.1 引入 Element-UI、Axios、Mockjs、QS

2.1.1 引入 Element-UI

  1. 安装:npm install element-ui -S
  2. 完整引入: 在 main.js 引入
    // 引入 Element UI 组件库
    import ElementUI from 'element-ui';
    // 引入 Element UI 全部样式
    import 'element-ui/lib/theme-chalk/index.css';
    // 应用 Element UI
    Vue.use(ElementUI);
    
  3. 按需引入:
    1. 安装:npm install babel-plugin-component -D ,-D 表示开发依赖
    2. 将 babel.config.js 修改为:
      module.exports = {
        presets: [ // 预设
          '@vue/cli-plugin-babel/preset', // vue-cli 原有的
          ['@babel/preset-env', { modules: false }] // 在原有的后面添加
        ],
        plugins: [
          [
            "component",
            {
              "libraryName": "element-ui",
              "styleLibraryName": "theme-chalk"
            }
          ]
        ]
      }
      
    3. 在 main.js 引入需要的部分组件:
      // 引入 Button、Select 和 Option,按钮、选择器和选项
      import { Button, Select, Option } from 'element-ui';
      // Button.name 即使用时标签的默认名字 ,el-xxx
      Vue.component(Button.name, Button);
      // 可以简写为:Vue.use(Button)
      Vue.component(Select.name, Select);
      // 可以自定义标签名字,则使用时标签也为自定义的标签名字而不是默认的 el-xxx
      Vue.component('fan-option', Option)
      

2.1.2 引入 Axios、Mockjs

安装 axios:npm install axios --save
安装 mockjs:npm install mockjs
Spring Security + Vue2 + Element-UI 总结_第14张图片
在 src 目录下 新建 mock/index.js
Spring Security + Vue2 + Element-UI 总结_第15张图片
在 main.js 里引入。
这里引入了 Mockjs,所有的请求都会被拦截。与后端接口对接时,需要在 main.js 中去掉 Mockjs 的引入,这样前端就可以访问后端的接口而不被 Mock 拦截

// 引入 Vue
import Vue from 'vue'
// 引入 App 组件
import App from './App.vue'
// 引入 store
import store from './store'
// 引入 VueRouter
import VueRouter from 'vue-router'
// 引入路由器
import router from './router/index.js'
// 引入 Axios
import axios from 'axios'
// 引入 Element UI 组件库
import ElementUI from 'element-ui';
// 引入 Element UI 全部样式
import 'element-ui/lib/theme-chalk/index.css';

Vue.config.productionTip = false
// 全局应用 Axios
Vue.prototype.$axios = axios
// 应用 VueRouter 插件
Vue.use(VueRouter)
// 应用 Element UI
Vue.use(ElementUI);
// 引入mock数据
require('./mock/index.js')

new Vue({
  store,
  router,
  render: h => h(App),
}).$mount('#app')

2.1.3 引入 QS

安装 qs:npm install qs
Spring Security + Vue2 + Element-UI 总结_第16张图片
在 main.js 文件中引入

// 引入 qs
import qs from 'qs'
// 配置全局 qs 属性
Vue.prototype.$qs = qs

2.2 登录页

2.2.1 新建登录组件

在 views 目录下,将原来的默认组件删掉,新建一个 Login 组件
在这里插入图片描述
1、使用 Element-UI 的 Layout 布局,加上表单验证

  1. :model 表示表单数据对象
  2. :rules 表示表单验证规则
  3. :status-icon 表示输入框中校验结果反馈图标
  4. :hide-required-asterisk 表示表单验证必填项的 * 号
  5. label 表示标签名字
  6. label-width 表示标签宽度
  7. ref 给元素注册引用信息
<template>
  <el-row>
    <el-col :span="24">

      <el-form
        :model="loginForm"
        ref="loginForm"
        :rules="rules"
        :status-icon="true"
        :hide-required-asterisk="true"
        @keyup.enter.native="login"
      >
        <el-form-item>
          <div class="el-form-login-title">系统登录div>
        el-form-item>
        <el-form-item
          prop="username"
          label="用户名"
          label-width="80px"
        >
          <el-input
            type="input"
            v-model="loginForm.username"
            autocomplete="off"
            placeholder="请输入用户名"
          >el-input>
        el-form-item>
        <el-form-item
          prop="password"
          label="密码"
          label-width="80px"
        >
          <el-input
            type="password"
            v-model="loginForm.password"
            autocomplete="off"
            placeholder="请输入密码"
          >el-input>
        el-form-item>
        <el-form-item
          prop="captcha"
          label="验证码"
          label-width="80px"
        >
          <el-input
            type="input"
            v-model="loginForm.captcha"
            autocomplete="off"
            placeholder="请输入验证码"
            style="width: 380px; float: left;"
          >el-input>
          <el-image
            :src="captchaImg"
            @click="getCaptcha"
            style="margin-left: 15px; border-radius: 5px;"
          >el-image>
        el-form-item>
        <el-form-item class="el-form-login-submit">
          <el-button
            type="primary"
            @click="login"
          >登 录el-button>
          <el-button
            type="primary"
            @click="() => this.$refs.loginForm.resetFields()"
          >重 置el-button>
        el-form-item>
      el-form>
    el-col>
  el-row>
template>

2、required 表示是否必填,message 表示提示信息,trigger 表示触发方式

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Login',
  data() {
    return {
      // 表单数据对象
      loginForm: {
        username: '',
        password: '',
        captcha: '',
        token: '',
      },
      // 表单验证规则
      rules: {
        username: [
          // required 表示是否必填,message 表示提示信息,trigger 表示触发方式
          { required: true, message: '请输入用户名', trigger: 'blur' },
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
        ],
        captcha: [
          { required: true, message: '请输入验证码', trigger: 'blur' },
        ],
      },
      captchaImg: '',
    }
  }
}
</script>

3、修改表单验证提示语样式


4、修改表单标签样式


5、重置表单
this.$refs['loginForm']表示获取表单,或者写为 this.$refs.loginFormresetFields() 用于对整个表单进行重置,将所有字段值重置为初始值并移除校验结果。@click="() => this.$refs.loginForm.resetFields()"

<el-form :model="loginForm" ref="loginForm" :rules="rules">
  <el-form-item>
        <el-button type="primary" @click="login">登录el-button>
        
        <el-button type="primary"
        @click="() => this.$refs.loginForm.resetFields()">重置el-button>
  el-form-item>
el-form>

6、表单按下回车提交
@keyup.enter.native 表示按下回车触发事件

<el-form :model="formLogin" ref="formLogin" :rules="rules"
	@keyup.enter.native="login">
el-form>

7、表单提交校验
this.$refs['loginForm']获取表单,validate()表示对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise

<script>
export default {
  methods: {
    login() {
      this.$refs['loginForm'].validate((valid) => {
        if (valid) {
          alert('submit!');
        } else {
          return false;
        }
      });
    },
  },
}
</script>

2.2.2 添加路由

在 src/router/index.js 文件里添加 /login 的路由,并且绑定到上面的 Login 组件

// 引入VueRouter
import VueRouter from 'vue-router'

// 创建 router 实例对象(路由器),去管理一组一组的路由规则,并暴露出去
export default new VueRouter({
  // 路由配置
  routes: [
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue'),
    },
  ]
})

2.2.3 App.vue

在 App.vue 里添加 标签,并且将边距设为 0

<template>
  <router-view>router-view>
template>

<script>
export default {
  name: 'App',
}
script>

<style>
body,
html {
  margin: 0;
  padding: 0;
}
style>

2.2.4 访问登录页面

访问路径 localhost:8080/#/login 即可访问登录页面

2.3 登录校验

2.3.1 图片验证码

1、Login 组件里给图片验证码加上 src 属性,图片源。在 data 里定义该图片源

<el-image
	:src="captchaImg"
	style="margin-left: 20px; border-radius: 5px;"
>el-image>

2、Login 组件里定义 getCaptcha 方法,获取验证码信息以及 token,同时组件加载时加载验证码

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Login',
  data() {
    return {
      // 验证码图片
      captchaImg: '',
    }
  },
  methods: {
    getCaptcha() {
      this.$axios.get('/hrms/api/getCaptcha').then((res) => {
        this.captchaImg = res.data.data.captchaImg;
        this.loginForm.token = res.data.data.token;
        this.loginForm.captcha = '';
      })
    },
  },
  mounted() {
    this.getCaptcha()
  },
}
</script>

3、未与后端接口对接时,可以先使用 Mockjs 模拟接口发送数据,在 src/mock/inde.js 文件中模拟发送数据(验证码和 token)。后续未对接的接口都可以在此模拟接口数据

const Mock = require('mockjs')
const Random = Mock.Random

let Result = {
  code: 200,
  msg: '成功',
  data: {}
}

Mock.mock('/hrms/api/getCaptcha', 'get', () => {
  Result.data = {
    token: Random.string(32),
    captchaImg: Random.dataImage('120x40', '4A7BF7')
  }
  return Result;
})

4、成功显示验证码

2.3.2 登录验证

1、Login 组件里定义 login 方法,发送登录请求,将登录表单的数据传过去,假如登录成功,获取并调用处理 jwt 方法,然后进行跳转

<script>
export default {
  methods: {
  	login() {
      this.$refs.loginForm.validate((valid) => {
        if (valid) {
          this.$axios.post('/hrms/login?' + this.$qs.stringify(this.loginForm)).then((res) => {
            this.$message.success('登录成功')
            // eslint-disable-next-line no-unused-vars
            const jwt = res.headers.authorization
            this.$store.commit('SET_JWT', jwt)
            this.$router.push('/home').catch(() => { })
          })
        } else {
          return false;
        }
      });
    },
  },
}
</script>

2、在 src/store/index.js 文件中接收并存储 jwt

// 引入 Vue
import Vue from 'vue'
// 引入 Vuex
import Vuex from 'vuex'
// 应用 Vuex 插件
Vue.use(Vuex)

// 准备 actions——用于响应组件中的动作
const actions = {}
// 准备 mutations——用于操作数据(state)
const mutations = {
  SET_JWT(state, jwt) {
    state.jwt = jwt;
    localStorage.setItem('jwt', jwt);
  }
}
// 准备state——用于存储数据
const state = {
  jwt: '',
}
// 准备getters——用于将state中的数据进行加工
const getters = {}

//创建并暴露 store
export default new Vuex.Store({
  //actions: actions,
  actions,
  mutations,
  state,
  getters
})

2.3.3 全局 Axios 拦截器

1、在 src 目录下新建 axios.js 文件,拦截所有请求,对于 200 状态码的请求放行,对其他状态码请求进行处理

import axios from "axios";
import ElementUI from "element-ui";
import router from "./router";

const request = axios.create({
  timeout: 5000,
  headers: {
    "Content-Type": "application/json",
  },
});

request.interceptors.request.use(config => {
  if (localStorage.getItem("token")) {
  	// 请求头带上 token
    config.headers.Authorization = localStorage.getItem("jwt");
  }
  return config;
});

request.interceptors.response.use(response => {
  let res = response.data;

  if (res.code === 200) {
    return response;
  }else {
    ElementUI.Message.error(res.message ? res.message : '系统异常');
    return Promise.reject(res.message);
  }
}, error => {
  if (error.response) {
    switch (error.response.status) {
      case 401:
        router.push("/login");
        break;
      case 403:
        ElementUI.Message.error("拒绝访问");
        break;
      case 404:
        ElementUI.Message.error("请求错误,未找到该资源");
        break;
      case 500:
        ElementUI.Message.error("服务器出错");
        break;
      default:
        ElementUI.Message.error("未知错误");
    }
  }
  return Promise.reject(error);
});

export default request

2、在 main.js 里修改 axios 的引入路径

// 引入 src/axios.js 请求拦截器
import request from './axios'
// 全局应用 axios 请求拦截器
Vue.prototype.$axios = request


Spring Security + Vue2 + Element-UI 总结_第17张图片

2.4 整体布局

在 src 目录下创建 layout 文件夹,并且分别创建三个布局组件。使用 Element-UI 的 Container 布局容器
Spring Security + Vue2 + Element-UI 总结_第18张图片

2.4.1 主容器 Main.vue

包括左侧菜单栏和顶部菜单栏,以及主体部分,后续组件都使用嵌套路由套在该容器下,继承左侧菜单栏和顶部菜单栏

<template>
  <el-container>
    <el-aside width="{asideWidth: '200px'}">
      <NavAside>NavAside>
    el-aside>
    <el-container>
      <el-header>
        <NavHeader>NavHeader>
      el-header>
      <el-main>
        <router-view style="padding: 0 20px 0 20px">router-view>
      el-main>
    el-container>
  el-container>
template>

<script>
import NavAside from '@/layout/NavAside.vue'
import NavHeader from '@/layout/NavHeader.vue'
import Tabs from '@/layout/Tabs.vue'

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Main',
  components: {
    NavAside,
    NavHeader
  },
}
script>

<style>
.el-header {
  background-color: #333;
  color: #fff;
}
.el-aside {
  background-color: #545c64;
}
.el-main {
  background-color: #f3f3f4;
  padding: 0;
}
style>

2.4.2 左侧菜单栏 NavAside.vue

collapse 属性表示是否收缩,使用 router-link 进行路由跳转。同时更改 a 链接的样式去掉 router-link 的下划线

<template>
  <el-menu
    class="el-menu-vertical-demo"
    :collapse="$store.state.isCollapse"
    background-color="#545c64"
    text-color="#fff"
    active-text-color="#ffd04b"
    :default-active="this.$store.state.menu.editableTabsValue"
  >
    <el-menu-item
      index="title"
      @click="$router.go(0)"
    >
      <span
        slot="title"
        style="margin-left: 8px"
      > <b>企业人力资源管理系统b> span>
      <i
        class="el-icon-menu"
        v-show="$store.state.isCollapse"
      >i>
    el-menu-item>

    <router-link to="/home">
      <el-menu-item index="Home">
        <i class='el-icon-s-home'>i>
        <span slot="title"> 首页 span>
      el-menu-item>
    router-link>

    <template v-for="menu in menuList">
      <el-submenu
        v-if="menu.children && menu.children.length > 0"
        :key="menu.name"
        :index="menu.name"
      >
        <template slot="title">
          <i :class=menu.icon>i>
          <span slot="title"> {{ menu.title }} span>
        template>
        <template v-for="child in menu.children">
          <router-link
            :to="child.path"
            :key="child.name"
          >
            <el-menu-item
              :index="child.name"
              @click="addTab(child)"
            >
              <i :class=child.icon>i>
              <span slot="title"> {{ child.title }} span>
            el-menu-item>
          router-link>
        template>

      el-submenu>

      <router-link
        v-else
        :to="menu.path"
        :key="menu.name"
      >
        <el-menu-item
          :index="menu.name"
          @click="addTab(menu)"
        >
          <i :class=menu.icon>i>
          <span slot="title"> {{ menu.title }} span>
        el-menu-item>
      router-link>
    template>
  el-menu>
template>

<script>
export default {
  name: 'NavAside',
  computed: {
    menuList() {
      return this.$store.state.menu.menuList
    }
  },
  methods: {
    addTab(menu) {
      this.$store.commit('ADD_TAB', menu)
    }
  }
}
script>

<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
}
.el-menu {
  height: 100vh;
  background-color: #545c64;
  color: #fff;
}
a {
  text-decoration: none;
}
style>

2.4.3 顶部菜单栏 NavHeader.vue

<template>
  <el-row>
    <el-col :span="24">
      <el-button
        size="mini"
        v-show="!$store.state.isCollapse"
        @click="handleCollapse"
        icon="el-icon-s-fold"
      >el-button>
      <el-button
        size="mini"
        v-show="$store.state.isCollapse"
        @click="handleCollapse"
        icon="el-icon-s-unfold"
      >el-button>

      <el-dropdown>
        <el-avatar
          class="el-avatar"
          shape="circle"
          :size="45"
          :src="userInfo.avatar"
        >el-avatar>
        <span style="color: #ddd; margin-left: 10px;">
          {{userInfo.empName}}<i class="el-icon-arrow-down el-icon--right">i>
        span>
        <el-dropdown-menu slot="dropdown">
          <router-link to="/userCenter">
            <el-dropdown-item>个人中心el-dropdown-item>
          router-link>
          <el-dropdown-item @click.native="logout">退出el-dropdown-item>
        el-dropdown-menu>
      el-dropdown>
    el-col>
  el-row>
template>

<script>
export default {
  name: 'NavHeader',
  data() {
    return {
      userInfo: {}
    }
  },
  methods: {
    handleCollapse() {
      this.$store.commit('HANDLE_COLLAPSE');
    },
    logout() {
      this.$axios.post('/hrms/logout').then(() => {
        this.$store.commit('LOGOUT');
        this.$router.push('/login');
      });
    },
    getUserInfo() {
      this.$axios.get("/hrms/employee/getUserInfo").then(res => {
        this.userInfo = res.data.data;
      })
    }
  },
  mounted() {
    this.getUserInfo();

    this.$bus.$on('refreshNavHeader', () => {
      this.getUserInfo();
    })
  }
}
script>

<style scoped>
.el-col {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 60px;
}
.el-button {
  margin-left: -5px;
}
.el-avatar {
  vertical-align: middle;
}
a {
  text-decoration: none;
}
style>

Spring Security + Vue2 + Element-UI 总结_第19张图片

2.4.4 展开/收缩侧边栏

在 src/store/index.js 文件中添加 isCollapse 属性来决定是否收缩,使用 HANDLE_COLLAPSE 方法来进行控制

// 引入 Vue
import Vue from 'vue'
// 引入 Vuex
import Vuex from 'vuex'
// 应用 Vuex 插件
Vue.use(Vuex)

// 准备 actions——用于响应组件中的动作
const actions = {}
// 准备 mutations——用于操作数据(state)
const mutations = {
  SET_JWT(state, jwt) {
    state.jwt = jwt;
    localStorage.setItem('jwt', jwt);
  },
  HANDLE_COLLAPSE(state) {
    state.isCollapse = !state.isCollapse
  },
}
// 准备state——用于存储数据
const state = {
  jwt: '',
  isCollapse: false,
}
// 准备getters——用于将state中的数据进行加工
const getters = {}

//创建并暴露 store
export default new Vuex.Store({
  actions,
  mutations,
  state,
  getters
})

Spring Security + Vue2 + Element-UI 总结_第20张图片

2.5 退出与动态导航路由、动态标签页绑定

2.5.1 退出

1、在顶部菜单栏,NavHeader.vue 组件中,给退出下拉框添加点击事件,一个退出的方法

<template>
  <el-row>
    <el-col :span="24">
      <el-dropdown>
        <el-avatar
          class="el-avatar"
          shape="circle"
          :size="45"
          :src="userInfo.avatar"
        >el-avatar>
        <span style="color: #ddd; margin-left: 10px;">
          {{userInfo.empName}}<i class="el-icon-arrow-down el-icon--right">i>
        span>
    
        <el-dropdown-menu slot="dropdown">
          <router-link to="/userCenter">
            <el-dropdown-item>个人中心el-dropdown-item>
          router-link>
          <el-dropdown-item @click.native="logout">退出el-dropdown-item>
        el-dropdown-menu>
      el-dropdown>
    el-col>
  el-row>
template>

2、发送退出请求,调用处理退出的方法,然后路由到登录页面

<script>
export default {
  name: 'NavHeader',
  methods: {
    handleCollapse() {
      this.$store.commit('HANDLE_COLLAPSE');
    },
    logout() {
      this.$axios.post('/hrms/logout').then(() => {
        this.$store.commit('LOGOUT');
        this.$router.push('/login');
      });
    },
  },
}
</script>

3、在 src/store/index.js 文件里处理退出,清除缓存信息

const mutations = {
  LOGOUT(state) {
    localStorage.removeItem('jwt');
    state.jwt = '';
    // localStorage.clear();
    // state.menu.menuList = [];
    // state.menu.permissions = [];
    // state.menu.hasRoute = false;
    state.menu.editableTabsValue = 'Home';
    state.menu.editableTabs = [{
      title: '首页',
      name: 'Home',
    }];
  },
}

2.5.2 动态导航与动态路由绑定

1、设置全局前置路由守卫,加载菜单信息,还可以通过判断是否登录页面,是否有 jwt 等判断条件提前判断是否能加载菜单,同时还可以通过开关 hasRoute 来动态判断是否已经加载过菜单。
将获取到的菜单动态生成路由,进行绑定,将 title、icon 等非路由属性放到 meta 路由元信息中。

// 引入VueRouter
import VueRouter from 'vue-router'
// 引入 Axios
import axios from '../axios'
// 引入 Vuex
import store from '../store'

// 创建 router 实例对象(路由器),去管理一组一组的路由规则,并暴露出去
const router = new VueRouter({
  // 路由配置
  routes: [
    {
      path: '/',
      redirect: '/home',
      name: 'Main',
      component: () => import('@/layout/Main.vue'),
      children: [
        {
          path: '/userCenter',
          name: 'UserCenter',
          component: () => import('@/views/UserCenter.vue'),
          meta: {
            title: '个人中心',
          },
        },
        {
          path: '/home',
          name: 'Home',
          component: () => import('@/views/Home.vue')
        },
      ]
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/Login.vue'),
    }
  ]
})

router.beforeEach((to, from, next) => {
  let hasRoute = store.state.menu.hasRoute;
  if (to.path === '/login') {
    next();
  } else if(!localStorage.getItem('jwt')){
    next('/login');
  } else if (!hasRoute){
    axios.get('/hrms/sys/menu/getNavMenu').then(res => {
      // 拿到 menuList 菜单列表
      store.commit('SET_MENU_LIST', res.data.data.menuList);
      // 拿到 permissionList 权限列表
      store.commit('SET_PERMISSION_LIST', res.data.data.permissionList);
  
      // 动态绑定路由
      res.data.data.menuList.forEach(menu => {
        if (menu.children) {
          menu.children.forEach(child => {
            // 转成路由
            let route = menuToRouter(child);
            // 把路由添加到路由管理器中
            if (route) {
              router.addRoute('Main', route);
            }
          })
        } else {
          let route = menuToRouter(menu);
          if (route) {
            router.addRoute('Main', route);
          }
        }
      });
      hasRoute = true;
      store.commit('CHANGE_ROUTE_STATUS', hasRoute);
      next(to.path);
    })
  }else {
    next();
  }
})

const menuToRouter = (menu) => {
  if(!menu.component){
    return null;
  } else {
    return {
      path: menu.path,
      name: menu.name,
      component: () => import('@/views/' + menu.component +'.vue'),
      meta: {
        title: menu.title,
        icon: menu.icon
      }
    }
  }
}

export default router

2、在 src/store 目录中新建 menu.js,用来存储菜单数据
在这里插入图片描述

// 引入 Vue
import Vue from 'vue'
// 引入 Vuex
import Vuex from 'vuex'
// 应用 Vuex 插件
Vue.use(Vuex)

export default {
  mutations: {
    SET_MENU_LIST(state, menuList) {
      state.menuList = menuList;
    },
    SET_PERMISSION_LIST(state, permissionList) {
      state.permissionList = permissionList;
    },
    CHANGE_ROUTE_STATUS(state, hasRoute) {
      state.hasRoute = hasRoute;
    },
  },
  state: {
    menuList: [],
    permissionList: [],
    hasRoute: false,
  },
}

3、在 src/store/index.js 文件中引入 menu.js,然后添加到 modules

import menu from './menu'

//创建并暴露 store
export default new Vuex.Store({
  actions,
  mutations,
  state,
  getters,
  modules: {
    menu,
  },
})

4、在左侧菜单栏 NavAside.vue 中直接获取 store 中的 menuList 数据,显示菜单

<script>
export default {
  name: 'NavAside',
  computed: {
    menuList() {
      return this.$store.state.menu.menuList
    }
  }
}
script>

2.5.3 导航与动态标签页绑定

1、在 src/layout 目录下新建 Tabs.vue 文件,删除标签页时,判断首页不能删除,以及点击标签页跳转到对应路由
Spring Security + Vue2 + Element-UI 总结_第21张图片

<template>
  <el-tabs
    v-model="editableTabsValue"
    type="card"
    closable
    @tab-remove="removeTab"
    @tab-click="clickTab"
  >
    <el-tab-pane
      v-for="tab in editableTabs"
      :key="tab.name"
      :label="tab.title"
      :name="tab.name"
    >
    el-tab-pane>
  el-tabs>
template>

<script>
export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Tabs',
  computed: {
    editableTabsValue: {
      get() {
        return this.$store.state.menu.editableTabsValue
      },
      set(val) {
        this.$store.state.menu.editableTabsValue = val
      }
    },
    editableTabs: {
      get() {
        return this.$store.state.menu.editableTabs
      },
      set(val) {
        this.$store.state.menu.editableTabs = val
      }
    }
  },
  methods: {
    removeTab(targetName) {
      let tabs = this.editableTabs;
      let activeName = this.editableTabsValue;

      if (targetName === 'Home') {
        return false;
      }
      if (activeName === targetName) {
        tabs.forEach((tab, index) => {
          if (tab.name === targetName) {
            let nextTab = tabs[index + 1] || tabs[index - 1];
            if (nextTab) {
              activeName = nextTab.name;
            }
          }
        });
      }

      this.editableTabsValue = activeName;
      this.editableTabs = tabs.filter(tab => tab.name !== targetName);
      this.$router.push({ name: activeName })
    },
    clickTab(tab) {
      this.$router.push({ name: tab.name })
    }
  }
}
script>

2、在主容器 Main.vue 中的主体部分加上标签页

<template>
  <el-container>
    <el-aside width="{asideWidth: '200px'}">
      <NavAside>NavAside>
    el-aside>
    <el-container>
      <el-header>
        <NavHeader>NavHeader>
      el-header>
      <el-main>
        <Tabs />
        <router-view>router-view>
      el-main>
    el-container>
  el-container>
template>

<script>
import NavAside from '@/layout/NavAside.vue'
import NavHeader from '@/layout/NavHeader.vue'
import Tabs from '@/layout/Tabs.vue'

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Main',
  components: {
    NavAside,
    NavHeader,
    Tabs
  },
}
script>

3、在 src/store/menu.js 文件里添加点击路由添加对应标签页的方法,并激活对应标签页。只有标签页不存在,才进行添加

export default {
  mutations: {
    ADD_TAB(state, tab) {
      let index = state.editableTabs.findIndex(item => item.name === tab.name);
      if (index === -1) {
        state.editableTabs.push({
          title: tab.title,
          name: tab.name,
        });
      }
      state.editableTabsValue = tab.name;
    },
  },
  state: {
    editableTabsValue: 'Home',
    editableTabs: [{
      title: '首页',
      name: 'Home',
    }],
  },
}

4、在左侧菜单栏 NavAside.vue 文件中,给菜单项加上点击事件,调用上面的添加标签页方法

<template>
	<router-link
	  v-else
	  :to="menu.path"
	  :key="menu.name"
	>
	  <el-menu-item
		:index="menu.name"
		@click="addTab(menu)"
	  >
          <i :class="'el-icon-' + menu.icon">i>
          <span slot="title"> {{ menu.title }} span>
	  el-menu-item>
	router-link>
template>

<script>
export default {
  name: 'NavAside',
  methods: {
    addTab(menu) {
      this.$store.commit('ADD_TAB', menu)
    }
  }
}
script>

5、在 App.vue 中监视刷新浏览器后回显之前激活的标签页

<template>
  <router-view>router-view>
template>

<script>
export default {
  name: 'App',
  watch: {
    $route(to) {
      if (to.path !== '/login') {
        let obj = {
          name: to.name,
          title: to.meta.title,
        }
        this.$store.commit('ADD_TAB', obj)
      }
    }
  },
}
script>

Spring Security + Vue2 + Element-UI 总结_第22张图片

2.6 菜单管理

Spring Security + Vue2 + Element-UI 总结_第23张图片
Spring Security + Vue2 + Element-UI 总结_第24张图片

1、新建菜单组件 Menu.vue

<template>
  <div>
    <div
      class="mainHeader"
      style="height: 630px;"
    >
      <el-row
        type="flex"
        justify="space-between"
        class="mainMessage"
      >
        <el-col class="mainMessageLeft">
          <div><b>菜单管理b>div>
        el-col>
        <el-col
          :span="4"
          class="mainMessageRight"
          style="margin-right: 7px;"
        >
          <div>
            <el-button
              type="primary"
              size="small"
              @click="$bus.$emit('menuAdd', menuList)"
              v-if="hasAuth('sys:menu:add')"
            >新增el-button>
          div>
        el-col>
      el-row>
      
      <el-table
        :data="menuList"
        id="out-table"
        class="mainTable"
        :header-cell-style="{background:'#ddd'}"
        max-height="520"
        border
        :fit="true"
        row-key="menuId"
        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
      >
        <el-table-column
          prop="menuName"
          label="名称"
          align="center"
          width="150"
        >
        el-table-column>
        <el-table-column
          prop="permission"
          label="权限编码"
          align="center"
          width="120"
        >
        el-table-column>
        <el-table-column
          prop="icon"
          label="图标"
          align="center"
          width="180"
        >
        el-table-column>
        <el-table-column
          prop="type"
          label="类型"
          align="center"
        >
          <template slot-scope="scope">
            <el-tag
              v-if="scope.row.type === 0"
              size="small"
            >目录el-tag>
            <el-tag
              v-else-if="scope.row.type === 1"
              size="small"
              type="success"
            >菜单el-tag>
            <el-tag
              v-else-if="scope.row.type === 2"
              size="small"
              type="info"
            >按钮el-tag>
          template>
        el-table-column>
        <el-table-column
          prop="path"
          label="菜单URL"
          align="center"
          width="120"
        >
        el-table-column>
        <el-table-column
          prop="component"
          label="菜单组件"
          align="center"
          width="200"
        >
        el-table-column>
        <el-table-column
          prop="orderNum"
          label="排序号"
          align="center"
        >
        el-table-column>
        <el-table-column
          prop="valiFlag"
          label="状态"
          align="center"
        >
          <template slot-scope="scope">
            <el-tag
              v-if="scope.row.valiFlag === 0"
              size="small"
              type="danger"
            >禁用el-tag>
            <el-tag
              v-else-if="scope.row.valiFlag === 1"
              size="small"
              type="success"
            >正常el-tag>
          template>
        el-table-column>
        <el-table-column
          label="操作"
          align="center"
          width="200"
          fixed="right"
        >
          <template slot-scope="scope">
            <el-button
              type="primary"
              size="small"
              @click="$bus.$emit('menuEdit', menuList, scope.row)"
              v-if="hasAuth('sys:menu:update')"
            >编辑el-button>
            <el-button
              type="danger"
              size="small"
              slot="reference"
              @click="menuDel(scope.row.menuId)"
              v-if="hasAuth('sys:menu:delete')"
            >删除el-button>
          template>
        el-table-column>
      el-table>
      <MenuAdd />
      <MenuEdit />
    div>
  div>
template>
<script>
import MenuAdd from './MenuAdd'
import MenuEdit from './MenuEdit'
import '../../../assets/css/mainStyle.css'

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: 'Menu',
  data() {
    return {
      menuList: [],
      dialogFormVisible: false,
    }
  },
  methods: {
    getMenuTree() {
      this.$axios.get('/hrms/sys/menu/getMenuList?valiFlag=').then(res => {
        this.menuList = res.data.data;
      })
    },
    menuDel(menuId) {
      this.$confirm('确定删除吗', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$axios.delete("/hrms/sys/menu/deleteMenu/" + menuId).then(() => {
          this.$message.success("删除成功")
          this.getMenuTree()
        })
      }).catch(() => {
        this.$message.success('已取消删除');
      })
    },
  },
  mounted() {
    this.$bus.$on('refreshMenuList', () => {
      this.getMenuTree();
    });
  },
  created() {
    this.getMenuTree();
  },
  components: {
    MenuAdd,
    MenuEdit,
  }
}
script>

2、新增菜单对话框 MenuAdd.vue

<template>
  <el-dialog
    title="菜单信息"
    :visible.sync="dialogFormVisible"
    :close-on-click-modal="false"
    class="el-dialog-menu"
  >
    <el-form
      :model="addForm"
      :rules="addFormRules"
      ref="addForm"
    >
      <el-form-item
        label="上级菜单"
        prop="parentId"
        label-width="100px"
      >
        
        <el-select
          v-model="addForm.parentId"
          placeholder="请选择上级菜单"
        >
          <template v-for="item in menuList">
            <el-option
              :key="item.menuId"
              :label="item.menuName"
              :value="item.menuId"
            >el-option>
            <template v-for="child in item.children">
              <el-option
                :key="child.menuId"
                :label="child.menuName"
                :value="child.menuId"
              >
                <span>{{ '- ' + child.menuName }}span>
              el-option>
            template>
          template>
        el-select>
      el-form-item>
      <el-form-item
        label="菜单名称"
        prop="menuName"
        label-width="100px"
      >
        <el-input
          v-model="addForm.menuName"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="权限编码"
        prop="permission"
        label-width="100px"
      >
        <el-input
          v-model="addForm.permission"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="图标"
        prop="icon"
        label-width="100px"
      >
        <el-input
          v-model="addForm.icon"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="菜单URL"
        prop="path"
        label-width="100px"
      >
        <el-input
          v-model="addForm.path"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="菜单组件"
        prop="component"
        label-width="100px"
      >
        <el-input
          v-model="addForm.component"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="类型"
        prop="type"
        label-width="100px"
      >
        <el-radio-group v-model="addForm.type">
          <el-radio :label=0>目录el-radio>
          <el-radio :label=1>菜单el-radio>
          <el-radio :label=2>按钮el-radio>
        el-radio-group>
      el-form-item>
      <el-form-item
        label="状态"
        prop="valiFlag"
        label-width="100px"
      >
        <el-radio-group v-model="addForm.valiFlag">
          <el-radio :label=0>禁用el-radio>
          <el-radio :label=1>正常el-radio>
        el-radio-group>
      el-form-item>
      <el-form-item
        label="排序号"
        prop="orderNum"
        label-width="100px"
      >
        <el-input-number
          v-model="addForm.orderNum"
          :min="1"
          label="排序号"
        >1el-input-number>
      el-form-item>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="resetForm('addForm')">取 消el-button>
      <el-button
        type="primary"
        @click="submitAddForm('addForm')"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'MenuAdd',
  data() {
    return {
      dialogFormVisible: false,
      menuList: [],
      addForm: {},
      addFormRules: {
        menuName: [
          { required: true, message: '请输入名称', trigger: 'blur' }
        ],
        permission: [
          { required: true, message: '请输入权限编码', trigger: 'blur' }
        ],
        type: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ],
        orderNum: [
          { required: true, message: '请填入排序号', trigger: 'blur' }
        ],
        valiFlag: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    submitAddForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios.post('/hrms/sys/menu/addMenu', this.addForm).then(() => {
            this.resetForm(formName)
            this.$message.success('添加成功')
            this.$bus.$emit('refreshMenuList')
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
      this.addForm = {}
      this.dialogFormVisible = false
    }
  },
  mounted() {
    this.$bus.$on('menuAdd', (menuList) => {
      this.dialogFormVisible = true
      this.menuList = menuList
    })
  }
}
script>

<style>
.el-dialog-menu {
  width: 100%;
  margin-top: -90px;
  overflow: hidden;
}
style>

3、编辑菜单对话框 MenuEdit.vue

<template>
  <el-dialog
    title="菜单信息"
    :visible.sync="dialogFormVisible"
    :close-on-click-modal="false"
    class="el-dialog-menu"
  >
    <el-form
      :model="editForm"
      :rules="editFormRules"
      ref="editForm"
    >
      <el-form-item
        label="上级菜单"
        prop="parentId"
        label-width="100px"
      >
        
        <el-select
          v-model="editForm.parentId"
          placeholder="请选择上级菜单"
        >
          <template v-for="item in menuList">
            <el-option
              :key="item.menuId"
              :label="item.menuName"
              :value="item.menuId"
            >el-option>
            <template v-for="child in item.children">
              <el-option
                :key="child.menuId"
                :label="child.menuName"
                :value="child.menuId"
              >
                <span>{{ '- ' + child.menuName }}span>
              el-option>
            template>
          template>
        el-select>
      el-form-item>
      <el-form-item
        label="菜单名称"
        prop="menuName"
        label-width="100px"
      >
        <el-input
          v-model="editForm.menuName"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="权限编码"
        prop="permission"
        label-width="100px"
      >
        <el-input
          v-model="editForm.permission"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="图标"
        prop="icon"
        label-width="100px"
      >
        <el-input
          v-model="editForm.icon"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="菜单URL"
        prop="path"
        label-width="100px"
      >
        <el-input
          v-model="editForm.path"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="菜单组件"
        prop="component"
        label-width="100px"
      >
        <el-input
          v-model="editForm.component"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="类型"
        prop="type"
        label-width="100px"
      >
        <el-radio-group v-model="editForm.type">
          <el-radio :label=0>目录el-radio>
          <el-radio :label=1>菜单el-radio>
          <el-radio :label=2>按钮el-radio>
        el-radio-group>
      el-form-item>
      <el-form-item
        label="状态"
        prop="valiFlag"
        label-width="100px"
      >
        <el-radio-group v-model="editForm.valiFlag">
          <el-radio :label=0>禁用el-radio>
          <el-radio :label=1>正常el-radio>
        el-radio-group>
      el-form-item>
      <el-form-item
        label="排序号"
        prop="orderNum"
        label-width="100px"
      >
        <el-input-number
          v-model="editForm.orderNum"
          :min="1"
          label="排序号"
        >1el-input-number>
      el-form-item>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="resetForm('editForm')">取 消el-button>
      <el-button
        type="primary"
        @click="submitEditForm('editForm')"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'MenuEdit',
  data() {
    return {
      dialogFormVisible: false,
      menuList: [],
      editForm: {},
      editFormRules: {
        menuName: [
          { required: true, message: '请输入名称', trigger: 'blur' }
        ],
        permission: [
          { required: true, message: '请输入权限编码', trigger: 'blur' }
        ],
        type: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ],
        orderNum: [
          { required: true, message: '请填入排序号', trigger: 'blur' }
        ],
        valiFlag: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ]
      }
    }
  },
  methods: {
    submitEditForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios.put('/hrms/sys/menu/updateMenu', this.editForm).then(() => {
            this.dialogFormVisible = false;
            this.$message.success('修改成功');
            this.$bus.$emit('refreshMenuList')
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      })
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
      this.$bus.$emit('refreshMenuList')
      this.dialogFormVisible = false
    }
  },
  mounted() {
    this.$bus.$on('menuEdit', (menuList, menu) => {
      this.dialogFormVisible = true
      this.menuList = menuList
      this.editForm = menu
    })
  }
}
script>

<style>
.el-dialog-menu {
  width: 100%;
  margin-top: -90px;
  overflow: hidden;
}
style>

2.7 角色管理

Spring Security + Vue2 + Element-UI 总结_第25张图片
1、新建角色组件 Role.vue

<template>
  <div>
    <div class="mainHeader">
      <el-form
        :inline="true"
        class="demo-form-inline"
        size="small"
        :model="searchForm"
        ref="searchForm"
        @submit.native.prevent="getRoleList"
      >
        <el-form-item
          label="角色名称"
          prop="roleName"
        >
          <el-input
            v-model="searchForm.roleName"
            placeholder="请输入角色名称"
            clearable
          >
          el-input>
        el-form-item>
        <el-form-item>
          <el-button
            size="small"
            type="primary"
            icon="el-icon-search"
            @click="getRoleList()"
          >查询el-button>
          <el-button
            @click="() => this.$refs['searchForm'].resetFields()"
            icon="el-icon-refresh-right"
          >重置el-button>
        el-form-item>
      el-form>
    div>

    <div class="mainBody">
      <el-row
        type="flex"
        justify="space-between"
        class="mainMessage"
      >
        <el-col class="mainMessageLeft">
          <div><b>查询结果b>div>
        el-col>
        <el-col
          :span="4"
          class="mainMessageRight"
        >
          <div>
            <el-button
              size="small"
              type="primary"
              @click="roleAdd"
              v-if="hasAuth('sys:role:add')"
            >
              新增
            el-button>
            <el-button
              size="small"
              type="danger"
              :disabled="delBatchBtn"
              @click="delRole(null)"
              v-if="hasAuth('sys:role:delete')"
            >删除选中el-button>
          div>
        el-col>
      el-row>
      <RoleAdd />

      <template>
        <el-table
          ref="multipleTable"
          class="mainTable"
          border
          :fit="true"
          :data="roleList"
          max-height="420"
          :header-cell-style="{background:'#ddd'}"
          @selection-change="handleSelectionChange"
          :default-sort="{prop: 'roleName', order: 'ascending'}"
        >
          <el-table-column type="selection">
          el-table-column>
          <el-table-column
            prop="roleName"
            align="center"
            sortable
            label="名称"
          >
          el-table-column>
          <el-table-column
            prop="code"
            align="center"
            label="唯一编码"
            width="120"
          >
          el-table-column>
          <el-table-column
            prop="remark"
            align="center"
            label="描述"
            width="500"
          >
          el-table-column>
          <el-table-column
            prop="valiFlag"
            label="状态"
            align="center"
          >
            <template slot-scope="scope">
              <el-tag
                v-if="scope.row.valiFlag === 0"
                size="small"
                type="danger"
              >禁用el-tag>
              <el-tag
                v-else-if="scope.row.valiFlag === 1"
                size="small"
                type="success"
              >正常el-tag>
            template>
          el-table-column>
          <el-table-column
            label="操作"
            align="center"
            width="320"
          >
            <template slot-scope="scope">
              <el-button
                size="small"
                type="primary"
                @click="rolePermission(scope.row)"
                v-if="hasAuth('sys:role:permission')"
              >分配权限el-button>
              <el-button
                size="small"
                type="success"
                @click="roleEdit(scope.row)"
                v-if="hasAuth('sys:role:update')"
              >编辑el-button>
              <el-button
                size="small"
                type="danger"
                @click="delRole(scope.row.roleId)"
                v-if="hasAuth('sys:role:delete')"
              >删除el-button>
            template>
          el-table-column>
        el-table>
        <RolePermission />
        <RoleEdit />
      template>

      <el-pagination
        class="mainPagination"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="current"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      >
      el-pagination>
    div>
  div>
template>
<script>
import '../../../assets/css/mainStyle.css';
import RoleAdd from './RoleAdd.vue';
import RolePermission from './RolePermission.vue';
import RoleEdit from './RoleEdit.vue';

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "Role",
  data() {
    return {
      searchForm: {
        roleName: ''
      },
      roleList: [],
      multipleSelection: [],
      delBatchBtn: true,
      current: 1,
      size: 10,
      total: 0,
    }
  },
  methods: {
    roleAdd() {
      this.$bus.$emit('roleAdd')
    },
    rolePermission(row) {
      this.$bus.$emit('RolePermission', row)
    },
    roleEdit(row) {
      this.$bus.$emit('RoleEdit', row)
    },
    handleSelectionChange(val) {
      this.multipleSelection = val;
      this.delBatchBtn = val.length == 0
    },
    getRoleList() {
      this.$axios.get('/hrms/sys/role/getRoleList', {
        params: {
          roleName: this.searchForm.roleName,
          valiFlag: '',
          current: this.current,
          size: this.size
        }
      }).then(res => {
        this.roleList = res.data.data.records;
        this.current = res.data.data.current;
        this.size = res.data.data.size;
        this.total = res.data.data.total;
      });
    },
    handleSizeChange(val) {
      this.size = val
      this.getRoleList()
    },
    handleCurrentChange(val) {
      this.current = val
      this.getRoleList()
    },
    delRole(roleId) {
      this.$confirm('是否确定删除?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        var roleIds = []
        roleId ? roleIds.push(roleId) : this.multipleSelection.forEach(row => {
          roleIds.push(row.roleId)
        })
        this.$axios.delete("/hrms/sys/role/deleteRole", {
          data: {
            roleIds: roleIds
          }
        }).then(() => {
          this.$message.success('删除角色成功')
          this.getRoleList()
        })
      }).catch(() => {
        this.$message({
          type: 'success',
          message: '已取消删除'
        });
      });
    },
  },
  created() {
    this.getRoleList()
  },
  mounted() {
    this.$bus.$on('refreshRoleList', () => {
      this.getRoleList()
    })
  },
  components: {
    RoleAdd,
    RolePermission,
    RoleEdit
  }
}
script>

2、新增角色对话框 RoleAdd.vue

<template>
  <el-dialog
    title="角色信息"
    :visible.sync="dialogFormVisible"
    width="600px"
    @close="resetForm('addForm')"
    :close-on-click-modal="false"
  >
    <el-form
      :model="addForm"
      :rules="addFormRules"
      ref="addForm"
    >
      <el-form-item
        label="角色名称"
        prop="roleName"
        label-width="100px"
      >
        <el-input
          v-model="addForm.roleName"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="唯一编码"
        prop="code"
        label-width="100px"
      >
        <el-input
          v-model="addForm.code"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="描述"
        prop="remark"
        label-width="100px"
      >
        <el-input
          v-model="addForm.remark"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="状态"
        prop="valiFlag"
        label-width="100px"
      >
        <el-radio-group v-model="addForm.valiFlag">
          <el-radio :label="0">禁用el-radio>
          <el-radio :label="1">正常el-radio>
        el-radio-group>
      el-form-item>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="dialogFormVisible = false">取 消el-button>
      <el-button
        type="primary"
        @click="submitAddForm('addForm')"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'RoleAdd',
  data() {
    return {
      dialogFormVisible: false,
      addForm: {},
      addFormRules: {
        roleName: [
          { required: true, message: '请输入名称', trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入唯一编码', trigger: 'blur' }
        ],
        valiFlag: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ]
      },
    }
  },
  methods: {
    submitAddForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios.post('/hrms/sys/role/addRole', this.addForm).then(() => {
            this.$message.success('添加角色成功')
            this.dialogFormVisible = false;
            this.$bus.$emit('refreshRoleList')
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
  },
  mounted() {
    this.$bus.$on('roleAdd', () => {
      this.dialogFormVisible = true
    })
  },
}
script>

3、编辑角色对话框 RoleEdit.vue

<template>
  <el-dialog
    title="角色信息"
    :visible.sync="dialogFormVisible"
    width="600px"
    @close="resetForm"
    :close-on-click-modal="false"
  >
    <el-form
      :model="editForm"
      :rules="editFormRules"
      ref="editForm"
    >
      <el-form-item
        label="角色名称"
        prop="roleName"
        label-width="100px"
      >
        <el-input
          v-model="editForm.roleName"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="唯一编码"
        prop="code"
        label-width="100px"
      >
        <el-input
          v-model="editForm.code"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="描述"
        prop="remark"
        label-width="100px"
      >
        <el-input
          v-model="editForm.remark"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="状态"
        prop="valiFlag"
        label-width="100px"
      >
        <el-radio-group v-model="editForm.valiFlag">
          <el-radio :label="0">禁用el-radio>
          <el-radio :label="1">正常el-radio>
        el-radio-group>
      el-form-item>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="dialogFormVisible = false">取 消el-button>
      <el-button
        type="primary"
        @click="updateRole('editForm')"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'RoleEdit',
  data() {
    return {
      dialogFormVisible: false,
      editForm: {},
      editFormRules: {
        roleName: [
          { required: true, message: '请输入名称', trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入唯一编码', trigger: 'blur' }
        ],
        valiFlag: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ]
      },
    }
  },
  methods: {
    updateRole(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios.put('/hrms/sys/role/updateRole', this.editForm).then(() => {
            this.$message.success('修改角色成功')
            this.dialogFormVisible = false;
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm() {
      this.editForm = {};
      this.$bus.$emit('refreshRoleList')
    },
  },
  mounted() {
    this.$bus.$on('RoleEdit', row => {
      this.dialogFormVisible = true
      this.editForm = row
    })
  },
}
script>

4、分配权限对话框 RolePermission.vue

<template>
  <el-dialog
    title="分配权限"
    class="el-dialog-role"
    :visible.sync="dialogFormVisible"
    width="600px"
    :close-on-click-modal="false"
    @closed="resetForm('permissionForm')"
  >
    <el-form
      :model="permissionForm"
      ref="permissionForm"
    >
      <el-tree
        :data="permissionTree"
        show-checkbox
        ref="permissionTree"
        :check-strictly="checkStrictly"
        node-key="menuId"
        :default-expand-all=true
        :props="defaultProps"
      >
      el-tree>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="dialogFormVisible = false">取 消el-button>
      <el-button
        type="primary"
        @click="rolePermission"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'RolePermission',
  data() {
    return {
      dialogFormVisible: false,
      permissionTree: [],
      permissionForm: {},
      defaultProps: {
        children: 'children',
        label: 'menuName',
      },
      checkStrictly: true,
    }
  },
  methods: {
    rolePermission() {
      var menuIds = this.$refs.permissionTree.getCheckedKeys();
      // menuIds = menuIds.concat(this.$refs.permTree.getHalfCheckedKeys()) // 半选中状态的父节点
      this.$axios.post("/hrms/sys/role/assignPermissions/" + this.permissionForm.roleId, menuIds).then(() => {
        this.$message.success("分配权限成功");
        this.$bus.$emit("refreshRoleList");
        this.$store.state.menu.hasRoute = false;
        this.dialogFormVisible = false
      })
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
  },
  mounted() {
    this.$bus.$on('RolePermission', row => {
      this.dialogFormVisible = true;
      this.permissionForm = row;
      this.$axios.get("/hrms/sys/menu/getMenuList?valiFlag=1").then(res => {
        this.permissionTree = res.data.data;
        this.$refs.permissionTree.setCheckedKeys(row.menuIds);
      })
    })
  }
}
script>

<style scoped>
.el-dialog-role {
  width: 100%;
  margin-top: -90px;
}
style>

2.8 用户管理

Spring Security + Vue2 + Element-UI 总结_第26张图片
1、新建用户组件 User.vue

<template>
  <div>
    <div class="mainHeader">
      <el-form
        :inline="true"
        class="demo-form-inline"
        size="small"
        :model="searchForm"
        ref="searchForm"
        @submit.native.prevent="getEmployeeList"
      >
        <el-form-item
          label="姓名"
          prop="empName"
        >
          <el-input
            v-model="searchForm.empName"
            placeholder="请输入用户姓名"
            clearable
          >
          el-input>
        el-form-item>
        <el-form-item>
          <el-button
            size="small"
            type="primary"
            icon="el-icon-search"
            @click="getEmployeeList"
          >查询el-button>
          <el-button
            @click="() => this.$refs['searchForm'].resetFields()"
            icon="el-icon-refresh-right"
          >重置el-button>
        el-form-item>
      el-form>
    div>

    <div class="mainBody">
      <el-row
        type="flex"
        justify="space-between"
        class="mainMessage"
      >
        <el-col class="mainMessageLeft">
          <div><b>查询结果b>div>
        el-col>
      el-row>
      <el-table
        class="mainTable"
        ref="multipleTable"
        border
        :fit="true"
        :header-cell-style="{background:'#ddd'}"
        max-height="420"
        :data="employeeList"
        :default-sort="{prop: 'empName', order: 'ascending'}"
      >
        <el-table-column
          label="头像"
          align="center"
        >
          <template slot-scope="scope">
            <el-avatar
              size="small"
              :src="scope.row.avatar"
            >el-avatar>
          template>
        el-table-column>
        <el-table-column
          prop="empName"
          label="用户名"
          align="center"
          sortable
        >
        el-table-column>
        <el-table-column
          label="角色名称"
          align="center"
          width="200"
        >
          <template slot-scope="scope">
            <el-tag
              style="margin-right: 5px;"
              size="small"
              type="info"
              v-for="item in scope.row.sysRoleDOS"
              :key="item.empId"
            >{{item.roleName}}el-tag>
          template>
        el-table-column>
        <el-table-column
          prop="empCode"
          label="工号"
          sortable
          align="center"
        >
        el-table-column>
        <el-table-column
          prop="idcardNo"
          label="身份证号"
          width="180"
          align="center"
        >
        el-table-column>
        <el-table-column
          label="状态"
          align="center"
        >
          <template slot-scope="scope">
            <el-tag
              v-if="scope.row.valiFlag === 0"
              size="small"
              type="danger"
            >禁用el-tag>
            <el-tag
              v-else-if="scope.row.valiFlag === 1"
              size="small"
              type="success"
            >正常el-tag>
          template>
        el-table-column>
        <el-table-column
          prop="createTime"
          label="创建时间"
          sortable
          align="center"
        >
        el-table-column>
        <el-table-column
          align="center"
          label="操作"
          fixed="right"
          width="300"
        >
          <template slot-scope="scope">
            <el-button
              size="small"
              type="primary"
              @click="userRole(scope.row)"
              v-if="hasAuth('employee:role')"
            >分配角色el-button>
            <el-button
              size="small"
              type="danger"
              @click="rePassword(scope.row)"
              v-if="hasAuth('employee:resetPassword')"
            >重置密码el-button>
            <el-button
              size="small"
              type="success"
              @click="userEdit(scope.row)"
              v-if="hasAuth('employee:update')"
            >编辑el-button>
          template>
        el-table-column>
      el-table>
      <el-pagination
        class="mainPagination"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="current"
        :page-sizes="[10, 20, 50, 100]"
        :page-size="size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      >
      el-pagination>
    div>
    <UserEdit />
    <UserRole />
  div>
template>

<script>
import '../../../assets/css/mainStyle.css'
import UserEdit from './UserEdit'
import UserRole from './UserRole'

export default {
  // eslint-disable-next-line vue/multi-word-component-names
  name: "User",
  data() {
    return {
      searchForm: {
        empName: ''
      },
      current: 1,
      total: 0,
      size: 10,
      dialogFormVisible: false,
      employeeList: [],
    }
  },
  methods: {
    getEmployeeList() {
      this.$axios.get('/hrms/employee/getEmployeeList', {
        params: {
          empName: this.searchForm.empName,
          pageNum: this.current,
          pageSize: this.size,
          valiFlag: 1
        }
      }).then(res => {
        this.employeeList = res.data.data.records
        this.current = res.data.data.current
        this.size = res.data.data.size
        this.total = res.data.data.total
      })
    },
    handleSizeChange(val) {
      this.size = val
      this.getEmployeeList()
    },
    handleCurrentChange(val) {
      this.current = val
      this.getEmployeeList()
    },
    userEdit(row) {
      this.$bus.$emit('UserEdit', row)
    },
    userRole(row) {
      this.$bus.$emit('UserRole', row)
    },
    rePassword(row) {
      this.$confirm('将重置用户【' + row.empName + '】的密码, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$axios.post("/hrms/employee/resetPassword", row.empId).then(() => {
          this.$message.success('重置密码成功')
        })
      }).catch(() => {
        this.$message.success('已取消重置密码')
      })
    }
  },
  created() {
    this.getEmployeeList()
  },
  mounted() {
    this.$bus.$on('refreshEmployeeList', () => this.getEmployeeList())
  },
  components: {
    UserEdit,
    UserRole
  }
}
script>

2、编辑用户对话框 UserEdit.vue

<template>
  <el-dialog
    title="用户信息"
    :visible.sync="dialogFormVisible"
    width="600px"
    @closed="resetForm('editForm')"
    :close-on-click-modal="false"
  >
    <el-form
      :model="editForm"
      :rules="editFormRules"
      ref="editForm"
    >
      <el-form-item
        label="用户名"
        prop="empName"
        label-width="100px"
      >
        <el-input
          v-model="editForm.empName"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="工号"
        prop="empCode"
        label-width="100px"
      >
        <el-input
          v-model="editForm.empCode"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="身份证号"
        prop="idcardNo"
        label-width="100px"
      >
        <el-input
          v-model="editForm.idcardNo"
          autocomplete="off"
        >el-input>
      el-form-item>
      <el-form-item
        label="状态"
        prop="valiFlag"
        label-width="100px"
      >
        <el-radio-group v-model="editForm.valiFlag">
          <el-radio :label="0">禁用el-radio>
          <el-radio :label="1">正常el-radio>
        el-radio-group>
      el-form-item>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="dialogFormVisible = false">取 消el-button>
      <el-button
        type="primary"
        @click="updateEmployee('editForm')"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'UserEdit',
  data() {
    return {
      editForm: {},
      dialogFormVisible: false,
      editFormRules: {
        empName: [
          { required: true, message: '请输入用户名称', trigger: 'blur' }
        ],
        empCode: [
          { required: true, message: '请输入工号', trigger: 'blur' }
        ],
        valiFlag: [
          { required: true, message: '请选择状态', trigger: 'blur' }
        ]
      },
    }
  },
  mounted() {
    this.$bus.$on('UserEdit', row => {
      this.dialogFormVisible = true;
      this.editForm = row
    })
  },
  methods: {
    updateEmployee(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$axios.put('/hrms/employee/updateEmployee', this.editForm).then(() => {
            this.$message.success('修改成功')
            this.dialogFormVisible = false;
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm() {
      this.editForm = {};
      this.$bus.$emit('refreshEmployeeList')
    },
  },
}
script>

3、分配角色对话框 UserRole.vue

<template>
  <el-dialog
    title="分配角色"
    :visible.sync="dialogFormVisible"
    width="600px"
    @closed="resetForm('userRoleForm')"
    :close-on-click-modal="false"
  >
    <el-form
      :model="userRoleForm"
      ref="userRoleForm"
    >
      <el-tree
        :data="roleTree"
        show-checkbox
        ref="roleTree"
        node-key="roleId"
        :check-strictly="checkStrictly"
        :default-expand-all=true
        :props="defaultProps"
      >
      el-tree>
    el-form>
    <div
      slot="footer"
      class="dialog-footer"
    >
      <el-button @click="dialogFormVisible = false">取 消el-button>
      <el-button
        type="primary"
        @click="userRole"
      >确 定el-button>
    div>
  el-dialog>
template>

<script>
export default {
  name: 'UserRole',
  data() {
    return {
      dialogFormVisible: false,
      userRoleForm: {},
      roleTree: [],
      defaultProps: {
        children: 'children',
        label: 'roleName'
      },
      checkStrictly: true,
    }
  },
  mounted() {
    this.$bus.$on('UserRole', row => {
      this.dialogFormVisible = true;
      this.userRoleForm = row;

      this.$axios.get('/hrms/sys/role/getRoleList', {
        params: {
          roleName: '',
          valiFlag: '1',
          current: 1,
          size: 10
        }
      }).then(res => {
        this.roleTree = res.data.data.records
        this.$refs.roleTree.setCheckedKeys(row.roleIds);
      })
    })
  },
  methods: {
    userRole() {
      var roleIds = this.$refs.roleTree.getCheckedKeys()
      this.$axios.post("/hrms/employee/assignRoles/" + this.userRoleForm.empId, roleIds).then(() => {
        this.$message.success('分配角色成功')
        this.$store.state.menu.hasRoute = false;
        this.$bus.$emit('refreshEmployeeList')
        this.dialogFormVisible = false
      })
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
      this.roleTree = {}
    },
  },
}
script>

3. Spring Security 后端项目搭建

3.1 执行流程图

Spring Security + Vue2 + Element-UI 总结_第27张图片
流程说明:

  1. 客户端发起一个请求,进入 Security 过滤器链
  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器
  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器
  4. 进入认证 BasicAuthenticationFilter 进行用户认证,成功的话会把认证了的结果写入到 SecurityContextHolder 中 SecurityContext 的属性 authentication 上面。如果认证失败就会交给 AuthenticationEntryPoint 认证失败处理类,或者抛出异常被后续 ExceptionTranslationFilter 过滤器处理异常,如果是 AuthenticationException 就交给 AuthenticationEntryPoint 处理,如果是 AccessDeniedException 异常则交给 AccessDeniedHandler 处理
  5. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理

Spring Security 详细可参考:https://blog.csdn.net/ACE_U_005A/article/details/123482893

项目中用到的部分组件:

  • LogoutFilter - 登出过滤器
  • logoutSuccessHandler - 登出成功之后的处理器
  • UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
  • AuthenticationFailureHandler - 登录失败处理器
  • AuthenticationSuccessHandler - 登录成功处理器
  • BasicAuthenticationFilter - Basic身份认证过滤器
  • SecurityContextHolder - 安全上下文静态工具类
  • AuthenticationEntryPoint - 认证失败入口
  • ExceptionTranslationFilter - 异常处理过滤器
  • AccessDeniedHandler - 权限不足操作类
  • FilterSecurityInterceptor - 权限判断拦截器、出口

3.2 准备

3.2.1 Security POM 依赖


<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-securityartifactId>
	<version>2.6.4version>
dependency>

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-data-redisartifactId>
	<version>2.6.4version>
dependency>

<dependency>
	<groupId>io.jsonwebtokengroupId>
	<artifactId>jjwtartifactId>
	<version>${jjwt.version}version>
dependency>

<dependency>
	<groupId>cloud.agileframeworkgroupId>
	<artifactId>spring-boot-starter-kaptchaartifactId>
	<version>2.1.0.M19version>
dependency>

3.2.2 RedisConfig 配置类

重新定义 Redis 的序列化规则。将 RedisTemplate 的 Key 的序列化规则设为 StringRedisSerializer,Value 的序列化规则设为 Jackson2JsonRedisSerializer

关于 Redis 序列化规则参考:https://blog.csdn.net/ACE_U_005A/article/details/124565124

@Configuration
public class RedisConfig {

    @Bean
    RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    
        return redisTemplate;
    }
}

3.2.3 CorsConfig 跨域配置类

浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起 HTTP 请求时必须遵守同源策略,否则就是跨域的 HTTP 请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题,需要进行处理让前端能进行跨域请求

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 允许所有来源
        corsConfiguration.addAllowedHeader("*"); // 允许所有请求头
        corsConfiguration.addAllowedMethod("*"); // 允许所有方法
        corsConfiguration.addExposedHeader("Authorization");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许 cookie
//                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的 header 属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

3.2.4 MybatisPlusConfig 配置类

@Configuration
@MapperScan("fan.**.dao")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 添加全表更新删除插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}

3.2.5 RedisUtil 工具类

@Component
public class RedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    // 指定缓存失效时间
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // hash 存储数据
    public boolean hashSet(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // hash 存储数据带过期时间
    public boolean hashSet(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // hash 获取数据
    public Object hashGet(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    // hash 删除值
    public void hashDel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    // String 存储数据
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // String 存储数据带过期时间
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // String 获取数据
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    // 判断key是否存在
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            return false;
        }
    }

    // 删除缓存
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
}

3.2.6 Result 统一结果处理

@Data
@Builder
public class Result implements Serializable {

    private static final long serialVersionUID = -1L;
    private Integer code;
    private String message;
    private Object data;

    public static Result success(Object data) {
        return Result.builder().code(200).message("操作成功").data(data).build();
    }

    public static Result success(String message, Object data) {
        return Result.builder().code(200).message(message).data(data).build();
    }

    public static Result success(int code, String message, Object data) {
        return Result.builder().code(code).message(message).data(data).build();
    }

    public static Result fail(String message) {
        return Result.builder().code(400).message(message).build();
    }

    public static Result fail(int code, String message) {
        return Result.builder().code(code).message(message).build();
    }

    public static Result fail(int code, String message, Object data) {
        return Result.builder().code(code).message(message).data(data).build();
    }
}

3.3 用户认证

用户认证问题,分为首次登陆和二次认证

  • 首次登录认证:用户名、密码和验证码完成登录
  • 二次 token 认证:请求头携带 Jwt 进行身份认证

3.3.1 生成验证码

1、在前面导入了 Google 的 Kaptcha 依赖包,可以用这个来生成图片验证码,首先新建一个图片验证码配置类,配置图片验证码的参数

@Configuration
public class KaptchaConfig {
    @Bean
    public DefaultKaptcha producer() {
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "4");
        properties.put("kaptcha.image.height", "40");
        properties.put("kaptcha.image.width", "120");
        properties.put("kaptcha.textproducer.font.size", "30");

        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);

        return defaultKaptcha;
    }
}

2、然后在 Controller 中生成图片验证码并进行映射
把验证码存入 Redis,使用一个随机字符串作为 Key,并传给前端,前端再把 Key 和用户输入的验证码传回来,这样就可以通过 Key 获取到保存的验证码和用户的验证码进行比较了是否一致
因为是图片验证码的方式,所以进行了 encode,把图片进行了 base64 编码,这样前端就可以显示图片

@RestController
@RequestMapping("/api")
public class AuthController {

    @Resource
    private Producer producer;

    @Resource
    private RedisUtil redisUtil;

    @GetMapping("/api/getCaptcha")
    public Result getCaptcha() throws IOException {

        String token = UUID.randomUUID().toString();
        String captcha = producer.createText(); // 生成验证码字符串

        BufferedImage image = producer.createImage(captcha);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ImageIO.write(image, "jpg", byteArrayOutputStream); // 生成图片字节数组

        Base64Encoder base64Encoder = new Base64Encoder();
        // 转换为base64,生成图片验证码
        String captchaImg = "data:image/jpg;base64," + base64Encoder.encode(byteArrayOutputStream.toByteArray());

        redisUtil.hashSet(Const.CAPTCHA_KEY, token, captcha, 120); // 将验证码存入redis

        return Result.success(MapUtil.builder().put("token", token).put("captchaImg", captchaImg).build());
    }
}

3.3.2 登录成功处理器

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private JwtUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream outputStream = response.getOutputStream();

        // 生成 jwt,并放到响应头中
        String jwt = jwtUtil.generateJwt(authentication.getName());
        response.setHeader(jwtUtil.getHeader(), jwt);

        Result success = Result.success("登录成功");
        outputStream.write(JSONUtil.toJsonStr(success).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();
    }
}

3.3.3 登录失败处理器

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream outputStream = response.getOutputStream();

        Result fail = Result.fail(exception.getMessage().equals("Bad credentials") ? "用户名或密码错误" : exception.getMessage());
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();
    }
}

3.3.4 图片验证码拦截器

Spring Security 的所有过滤器都是没有图片验证码的,如果依然想沿用自带的 UsernamePasswordAuthenticationFilter,可以在这个过滤器之前添加一个图片验证码过滤器。或者自定义过滤器继承 UsernamePasswordAuthenticationFilter,然后在原有的认证逻辑上加上验证码验证逻辑。

这里我们在 UsernamePasswordAuthenticationFilter 之前自定义一个图片过滤器 CaptchaFilter

@Component
public class CaptchaFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getRequestURI().equals("/hrms/login") && request.getMethod().equals("POST")) {
            try {
                // 校验验证码
                validate(request);
                filterChain.doFilter(request, response);
            } catch (CustomException e) {
                // 交给认证失败处理器处理
                loginFailureHandler.onAuthenticationFailure(request, response, e);
            }
        } else {
            filterChain.doFilter(request, response);
        }

    }

    // 校验验证码
    private void validate(HttpServletRequest request) {
        String captcha = request.getParameter("captcha");
        String token = request.getParameter("token");

        if (StringUtils.isBlank(captcha) || StringUtils.isBlank(token)) {
            throw new CustomException("验证码不能为空");
        }

        if (!captcha.equals(redisUtil.hashGet(Const.CAPTCHA_KEY, token))) {
            throw new CustomException("验证码错误");
        }

		redisUtil.hashDel(Const.CAPTCHA_KEY, token);
    }
}

3.3.5 SecurityConfig 核心配置类

该配置类用于 Security 的核心配置。这里进行跨域和请求路径配置,放开登录登出以及验证码的请求,对其他请求进行拦截。并添加登录成功和失败处理器,将图片验证码过滤器添加到 UsernamePasswordAuthenticationFilter 之前

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private LoginFailureHandler loginFailureHandler;

    @Resource
    private LoginSuccessHandler loginSuccessHandler;
   
    @Resource
    private CaptchaFilter captchaFilter;

    public static final String[] AUTH_WHITELIST = {
            "/login",
            "/logout",
            "/api/**",
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域访问,关闭csrf防护
        http.csrf().disable().cors();
        // 拦截规则
        http.authorizeRequests()
                .antMatchers(AUTH_WHITELIST).permitAll()
                .anyRequest().authenticated();
        // 登录配置
        http.formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler);
        // 添加验证码过滤器在登录之前,添加jwt过滤器
        http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

3.5 异常处理

3.5.1 自定义异常类

public class CustomException extends AuthenticationException {

    public CustomException(String msg) {
        super(msg);
    }
}

3.5.2 全局异常类

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) {
        e.printStackTrace();
        return Result.fail(e.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }
}

3.5.3 认证失败处理器

@Component
public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401,未认证
        ServletOutputStream outputStream = response.getOutputStream();

        Result fail = Result.fail(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage().equals("JWT异常") ? authException.getMessage() : "请先登录");
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();
    }
}

3.5.4 权限失败处理器

@Component
public class UnAccessDeniedHandler implements AccessDeniedHandler{

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403,未授权,禁止访问
        ServletOutputStream outputStream = response.getOutputStream();

        Result fail = Result.fail(HttpServletResponse.SC_FORBIDDEN, "没有权限访问");
        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();
    }
}

3.6 鉴权

3.6.1 JWT 工具类

JWT 相关知识:https://blog.csdn.net/ACE_U_005A/article/details/123531422

@Data
@Component
public class JwtUtil {

    @Value("${fan.jwt.expire}")
    private String expire;
    @Value("${fan.jwt.header}")
    private String header;
    private final static RSA rsa = new RSA();

    // 生成 JWT
    public String generateJwt(String username) {

        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) rsa.getPrivateKey();

        String jwt = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + Long.parseLong(expire))) // 设置过期时间
                .signWith(SignatureAlgorithm.RS256, rsaPrivateKey)
                .compact();
        System.out.println(jwt + "  生成的jwt");
        return jwt;
    }

    // 解析 JWT
    public Jws<Claims> parseJwt(String jwt) {

        RSAPublicKey rsaPublicKey = (RSAPublicKey) rsa.getPublicKey();
        try {
            return Jwts.parser().setSigningKey(rsaPublicKey).parseClaimsJws(jwt);
        } catch (Exception e) {
            return null;
        }
    }
}

3.6.2 JWT 过滤器

过滤器会去获取请求头中的 JWT,对 JWT 进行解析取出其中的 username。使用 username 去 Redis 中获取对应的权限列表。然后封装 Authentication 对象存入 SecurityContextHolder

在 Spring Security 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。在项目中只需要把当前登录用户的权限信息也存入 Authentication。然后设置资源所需要的权限即可

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Resource
    private JwtUtil jwtUtil;

    @Resource
    private EmployeeService employeeService;

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Resource
    private UnAuthenticationEntryPoint unAuthenticationEntryPoint;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String jwt = request.getHeader(jwtUtil.getHeader());
        if (StringUtils.isBlank(jwt)) {
            chain.doFilter(request, response);
            return;
        }

        Jws<Claims> claimsJws = jwtUtil.parseJwt(jwt);
        if (claimsJws == null) {
            CustomException customException = new CustomException("JWT异常");
            unAuthenticationEntryPoint.commence(request, response, customException);
            throw customException;
        }
        String username = claimsJws.getBody().getSubject();
        EmployeeDTO employeeDTO = employeeService.getEmpByCode(username);

        List<GrantedAuthority> authorities;
        if (redisUtil.hasKey("GrantedAuthority:" + employeeDTO.getEmpName())) {
            authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) redisUtil.get("GrantedAuthority:" + employeeDTO.getEmpName()));
        } else {
            authorities = userDetailsService.getAuthorities(employeeDTO);
        }
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(username, claimsJws, authorities);

        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        chain.doFilter(request, response);
    }
}

3.6.3 UserDetailsServiceImpl 实现类

实现 UserDetailsService 接口,根据用户名从数据库查出用户信息和用户权限信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private EmployeeService employeeService;

    @Resource
    private RedisUtil redisUtil;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        EmployeeDTO employeeDTO = employeeService.getEmpByCode(username);
        if (employeeDTO == null) {
            throw new CustomException("用户名不存在");
        }

        List<GrantedAuthority> authorities;
        if (redisUtil.hasKey("GrantedAuthority:" + employeeDTO.getEmpName())) {
            authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) redisUtil.get("GrantedAuthority:" + employeeDTO.getEmpName()));
        } else {
            System.out.println("UserDetailsServiceImpl从数据库中获取权限");
            authorities = getAuthorities(employeeDTO);
        }
        return new SecurityUser(employeeDTO, authorities);
    }

    public List<GrantedAuthority> getAuthorities(EmployeeDTO employeeDTO) {

        String authority = employeeService.getAuthority(employeeDTO.getEmpId());
        redisUtil.set("GrantedAuthority:" + employeeDTO.getEmpName(), authority);

        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
        return authorities;
    }
}

3.6.4 UserDetails 实现类

保存用户信息

@Data
public class SecurityUser implements UserDetails {

    private transient EmployeeDTO employeeDTO;
    private Collection<? extends GrantedAuthority> authorities;

    public SecurityUser(EmployeeDTO employeeDTO, List<GrantedAuthority> authorities) {
        this.employeeDTO = employeeDTO;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return employeeDTO.getPassword();
    }

    @Override
    public String getUsername() {
        return employeeDTO.getEmpCode();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3.6.5 CustomLogoutSuccessHandler 注销成功处理器

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Resource
    private JwtUtil jwtUtil;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出成功过滤器");

        // 手动退出
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }

        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream outputStream = response.getOutputStream();

        response.setHeader(jwtUtil.getHeader(), "");
        Result success = Result.success("登出成功");
        outputStream.write(JSONUtil.toJsonStr(success).getBytes(StandardCharsets.UTF_8));

        outputStream.flush();
        outputStream.close();
    }
}

3.6.6 SecurityConfig 核心配置类完整配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private LoginFailureHandler loginFailureHandler;

    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    @Resource
    private CaptchaFilter captchaFilter;

    @Resource
    private UnAccessDeniedHandler unAccessDeniedHandler;

    @Resource
    private UnAuthenticationEntryPoint unAuthenticationEntryPoint;

    @Resource
    private UserDetailsServiceImpl userDetailsServiceImpl;

    @Resource
    private CustomLogoutSuccessHandler customLogoutSuccessHandler;

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager());
    }

    public static final String[] AUTH_WHITELIST = {
            "/login",
            "/logout",
            "/api/**",
    };

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域访问,关闭csrf防护
        http.csrf().disable().cors();
        // 拦截规则
        http.authorizeRequests()
                .antMatchers(AUTH_WHITELIST).permitAll()
                .anyRequest().authenticated();
        // 登录配置
        http.formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler);
        // 添加验证码过滤器在登录之前,添加jwt过滤器
        http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定义异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(unAuthenticationEntryPoint)
                .accessDeniedHandler(unAccessDeniedHandler);
        // 添加自定义注销处理器
        http.logout().logoutSuccessHandler(customLogoutSuccessHandler);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3.7 权限管理

代码零散有点乱。仅供参考

3.7.1 菜单管理

1. Controller

@RestController
@RequestMapping("/sys/menu")
public class SysMenuController {

    @Resource
    private EmployeeService employeeService;

    @Resource
    private SysMenuService sysMenuService;

    @Resource
    private SysRoleMenuService sysRoleMenuService;

    @GetMapping("/getNavMenu")
    public Result getNavMenu(Principal principal) {
        // 获取权限信息
        EmployeeDTO employeeDTO = employeeService.getEmpByCode(principal.getName());
        String authority = employeeService.getAuthority(employeeDTO.getEmpId());
        String[] permissionList = StringUtils.tokenizeToStringArray(authority, ",");

        // 获取导航栏菜单
        List<SysMenuDTO> menuList = sysMenuService.getNavMenu(employeeService.getEmpByCode(principal.getName()));
        return Result.success("获取导航栏菜单成功", MapUtil.builder().put("menuList", menuList).put("permissionList", permissionList).build());
    }

    @GetMapping("/getMenuList")
    @PreAuthorize("hasAuthority('sys:menu:list')")
    public Result getMenuList(String valiFlag) {
        return Result.success("获取菜单列表成功", sysMenuService.getMenuList(valiFlag));
    }

    @PostMapping("/addMenu")
    @PreAuthorize("hasAuthority('sys:menu:add')")
    public Result addMenu(@Validated @RequestBody SysMenuDO sysMenuDO) {
        if (org.apache.commons.lang3.StringUtils.isBlank(sysMenuDO.getParentId())) {
            sysMenuDO.setParentId("0");
        }
        sysMenuDO.setMenuId(UUID.randomUUID().toString());
        sysMenuDO.setCreateTime(LocalDateTime.now());
        sysMenuDO.setUpdateTime(LocalDateTime.now());
        return Result.success("添加菜单成功", sysMenuService.save(sysMenuDO));
    }

    @PutMapping("/updateMenu")
    @PreAuthorize("hasAuthority('sys:menu:update')")
    public Result updateMenu(@Validated @RequestBody SysMenuDO sysMenuDO) {

        sysMenuDO.setUpdateTime(LocalDateTime.now());
        sysMenuService.updateById(sysMenuDO);
        // 清除所有与菜单相关的缓存
        employeeService.clearUserAuthorityByMenuId(sysMenuDO.getMenuId());
        return Result.success("更新菜单成功");
    }

    @DeleteMapping("/deleteMenu/{menuId}")
    @PreAuthorize("hasAuthority('sys:menu:delete')")
    public Result deleteMenu(@PathVariable("menuId") String menuId) {
        long parent_id = sysMenuService.count(new QueryWrapper<SysMenuDO>().eq("parent_id", menuId));
        if (parent_id > 0) {
            return Result.fail("该菜单下存在子菜单,不能删除");
        }

        sysMenuService.removeById(menuId);
        // 清除所有与菜单相关的缓存
        employeeService.clearUserAuthorityByMenuId(menuId);

        // 同步删除中间关联表
        sysRoleMenuService.remove(new QueryWrapper<SysRoleMenuDO>().eq("menu_id", menuId));
        return Result.success("删除菜单成功");
    }
}

2. ServiceImpl 实现类。接口实现 IService 接口

@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuDAO, SysMenuDO> implements SysMenuService {

    @Resource
    private SysMenuDAO sysMenuDAO;

    @Resource
    private SysMenuDTOConvert sysMenuDTOConvert;

	// 根据员工id获取导航菜单列表id
    @Override
    public List<String> getNavMenuIds(String empId) {
        return sysMenuDAO.getNavMenuIds(empId);
    }

	// 获取用户导航菜单列表
    @Override
    public List<SysMenuDTO> getNavMenu(EmployeeDTO employeeDTO) {
        List<String> navMenuIds = getNavMenuIds(employeeDTO.getEmpId());
        List<SysMenuDO> sysMenuDOS = this.listByIds(navMenuIds);

        // 转树状结构
        List<SysMenuDO> sysMenuDOSTree = navBuildTree(sysMenuDOS);
        // 转DTO
        List<SysMenuDTO> sysMenuDTOS = convert(sysMenuDOSTree);
        return sysMenuDTOS;
    }

	// 获取所有菜单列表
    @Override
    public List<SysMenuDO> getMenuList(String valiFlag) {

        List<SysMenuDO> sysMenuDOS = this.list(new QueryWrapper<SysMenuDO>().eq(StringUtils.isNotBlank(valiFlag),"vali_flag", valiFlag)
                .orderByAsc("order_num"));

        return buildTree(sysMenuDOS);
    }

	// 将菜单列表转为树状结构
    private List<SysMenuDO> buildTree(List<SysMenuDO> sysMenuDOS){
        List<SysMenuDO> finalMenu = new ArrayList<>();

        for (SysMenuDO sysMenuDO : sysMenuDOS) {

            for (SysMenuDO menuDO : sysMenuDOS) {
                if (sysMenuDO.getMenuId().equals(menuDO.getParentId())) {
                    sysMenuDO.getChildren().add(menuDO);
                }
            }

            if (sysMenuDO.getParentId().equals("0")){
                finalMenu.add(sysMenuDO);
            }
        }
        return finalMenu;
    }

    private List<SysMenuDO> navBuildTree(List<SysMenuDO> sysMenuDOS){
        List<SysMenuDO> finalMenu = new ArrayList<>();

        for (SysMenuDO sysMenuDO : sysMenuDOS) {

            for (SysMenuDO menuDO : sysMenuDOS) {
                if (sysMenuDO.getMenuId().equals(menuDO.getParentId()) && menuDO.getType() == 1) {
                    sysMenuDO.getChildren().add(menuDO);
                }
            }

            if (sysMenuDO.getParentId().equals("0")){
                finalMenu.add(sysMenuDO);
            }
        }
        return finalMenu;
    }

	// 将树状菜单结构转为DTO
    private List<SysMenuDTO> convert(List<SysMenuDO> sysMenuDOSTree) {
        List<SysMenuDTO> sysMenuDTOS = new ArrayList<>();

        sysMenuDOSTree.forEach(sysMenuDO -> {
            SysMenuDTO sysMenuDTO = sysMenuDTOConvert.convertToSysMenuDTO(sysMenuDO);

            if (sysMenuDO.getChildren().size() > 0){
                sysMenuDTO.setChildren(convert(sysMenuDO.getChildren()));
            }

            sysMenuDTOS.add(sysMenuDTO);
        });

        return sysMenuDTOS;
    }
}

3. Mapper

<mapper namespace="fan.security.dao.SysMenuDAO">

    <select id="getNavMenuIds" resultType="java.lang.String">
        SELECT DISTINCT sys_role_menu.menu_id
        FROM sys_employee_role
                 LEFT JOIN sys_role_menu ON sys_employee_role.role_id = sys_role_menu.role_id
        WHERE sys_employee_role.emp_id = #{empId}
    select>

mapper>

3.7.2 角色管理

1. Controller

@RestController
@RequestMapping("/sys/role")
public class SysRoleController extends BaseController {

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private EmployeeService employeeService;

    @Resource
    private SysRoleMenuService sysRoleMenuService;

    @Resource
    private SysEmployeeRoleService sysEmployeeRoleService;

    @GetMapping("/getRoleList")
    @PreAuthorize("hasAnyAuthority('sys:role:list')")
    public Result getRoleList(String roleName, String valiFlag) {

        Page<SysRoleDO> sysRoleDOPage = sysRoleService.page(getPage(), new QueryWrapper<SysRoleDO>()
                .eq(StringUtils.isNotBlank(valiFlag), "vali_flag", valiFlag)
                .like(StringUtils.isNotBlank(roleName), "role_name", roleName));

        sysRoleDOPage.getRecords().forEach(sysRoleDO -> {
            List<SysRoleMenuDO> sysRoleMenuDOS = sysRoleMenuService.list(new QueryWrapper<SysRoleMenuDO>().eq("role_id", sysRoleDO.getRoleId()));
            List<String> menuIds = sysRoleMenuDOS.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
            sysRoleDO.setMenuIds(menuIds);
        });
        return Result.success(sysRoleDOPage);
    }

    @PostMapping("/addRole")
    @PreAuthorize("hasAuthority('sys:role:add')")
    public Result addRole(@Validated @RequestBody SysRoleDO sysRoleDO) {
        sysRoleDO.setCreateTime(LocalDateTime.now());
        sysRoleDO.setUpdateTime(LocalDateTime.now());
        sysRoleDO.setValiFlag(Const.STATUS_ON);
        return Result.success("添加角色成功", sysRoleService.save(sysRoleDO));
    }

    @PutMapping("/updateRole")
    @PreAuthorize("hasAuthority('sys:role:update')")
    public Result updateRole(@Validated @RequestBody SysRoleDO sysRoleDO) {
        sysRoleDO.setUpdateTime(LocalDateTime.now());
        sysRoleService.updateById(sysRoleDO);
        employeeService.clearUserAuthorityByRoleId(sysRoleDO.getRoleId());
        return Result.success("修改角色成功");
    }

    @DeleteMapping("/deleteRole")
    @PreAuthorize("hasAuthority('sys:role:delete')")
    @Transactional
    public Result deleteRole(@RequestBody RoleConditionDTO roleConditionDTO) {

        sysRoleService.removeByIds(roleConditionDTO.getRoleIds());

        // 删除中间表
        sysEmployeeRoleService.remove(new QueryWrapper<SysEmployeeRoleDO>().in("role_id", roleConditionDTO.getRoleIds()));
        sysRoleMenuService.remove(new QueryWrapper<SysRoleMenuDO>().in("role_id", roleConditionDTO.getRoleIds()));

        // 清除缓存
        roleConditionDTO.getRoleIds().forEach(roleId -> employeeService.clearUserAuthorityByRoleId(roleId));

        return Result.success("删除角色成功");
    }

    @PostMapping("/assignPermissions/{roleId}")
    @PreAuthorize("hasAuthority('sys:role:permission')")
    @Transactional
    public Result assignPermissions(@PathVariable("roleId") String roleId, @RequestBody String[] menuIds) {
        ArrayList<SysRoleMenuDO> sysRoleMenuDOS = new ArrayList<>();

        Arrays.stream(menuIds).forEach(menuId -> {
            SysRoleMenuDO sysRoleMenuDO = new SysRoleMenuDO();
            sysRoleMenuDO.setRoleId(roleId);
            sysRoleMenuDO.setMenuId(menuId);

            sysRoleMenuDOS.add(sysRoleMenuDO);
        });

        // 先删除原来的记录,再添加新的记录
        sysRoleMenuService.remove(new QueryWrapper<SysRoleMenuDO>().eq("role_id", roleId));
        sysRoleMenuService.saveBatch(sysRoleMenuDOS);

        // 清除缓存
        employeeService.clearUserAuthorityByRoleId(roleId);
        return Result.success("分配权限成功");
    }
}

2. ServiceImpl 实现类。接口实现 IService 接口

@Service
public class SysRoleServiceImpl extends ServiceImpl<SysRoleDAO, SysRoleDO> implements SysRoleService {

    @Resource
    private SysRoleDAO sysRoleDAO;

    @Override
    public List<String> getRoleIdsByEmpId(String empId) {
        List<String> roleIds = sysRoleDAO.getRoleIds(empId);

        return roleIds;
    }

    @Override
    public List<SysRoleDO> getRoleListByRoleIds(List<String> roleIds) {
        return roleIds.isEmpty() ? null : sysRoleDAO.selectBatchIds(roleIds);
    }
  
}

3. Mapper

<mapper namespace="fan.security.dao.SysRoleDAO">

    <select id="getRoleIds" resultType="java.lang.String">
        SELECT role_id
        FROM sys_employee_role
        WHERE emp_id = #{empId}
    select>
  
mapper>

3.7.3 用户管理

1. Controller

@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Resource
    private EmployeeService employeeService;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private SysEmployeeRoleService sysEmployeeRoleService;

    @GetMapping("/getEmployeeList")
    @ApiOperation(value = "查询员工信息")
    @PreAuthorize("hasAuthority('employee:list')")
    public Result getEmployeeList(EmployeeConditionDTO conditionDTO){
        Page<EmployeeDTO> employeeDTOS = employeeService.getEmployeeList(conditionDTO);

        employeeDTOS.getRecords().forEach(employeeDTO -> {
            List<String> roleIds = sysRoleService.getRoleIdsByEmpId(employeeDTO.getEmpId());
            employeeDTO.setRoleIds(roleIds);
            employeeDTO.setSysRoleDOS(sysRoleService.getRoleListByRoleIds(roleIds));
        });
        return Result.success("查询员工信息成功", employeeDTOS);
    }

    @PutMapping("/updateEmployee")
    @ApiOperation(value = "修改员工信息")
    @PreAuthorize("hasAuthority('employee:update')")
    public Result updateEmployee(@RequestBody EmployeeDTO employeeDTO){
        return Result.success("修改员工成功", employeeService.updateEmployee(employeeDTO));
    }

    @PostMapping("/assignRoles/{empId}")
    @PreAuthorize("hasAuthority('employee:role')")
    public  Result assignRoles(@PathVariable("empId") String empId, @RequestBody String[] roleIds) {
        ArrayList<SysEmployeeRoleDO> sysEmployeeRoleDOS = new ArrayList<>();

        Arrays.stream(roleIds).forEach(roleId -> {
            SysEmployeeRoleDO sysEmployeeRoleDO = new SysEmployeeRoleDO();
            sysEmployeeRoleDO.setEmpId(empId);
            sysEmployeeRoleDO.setRoleId(roleId);

            sysEmployeeRoleDOS.add(sysEmployeeRoleDO);
        });

        sysEmployeeRoleService.remove(new QueryWrapper<SysEmployeeRoleDO>().eq("emp_id", empId));
        sysEmployeeRoleService.saveBatch(sysEmployeeRoleDOS);
        // 清除缓存
        EmployeeDO employeeDO = employeeService.getEmpById(empId);
        employeeService.clearUserAuthority(employeeDO.getEmpName());

        return Result.success("分配角色成功");
    }

    @PostMapping("/resetPassword")
    @PreAuthorize("hasAuthority('employee:resetPassword')")
    public Result resetPassword(@RequestBody String empId) {
        EmployeeDO employeeDO = employeeService.getEmpById(empId);
        employeeDO.setPassword(passwordEncoder.encode(
                employeeDO.getIdcardNo().substring(employeeDO.getIdcardNo().length() - 6)));

        return Result.success("重置密码成功", employeeService.resetPassword(employeeDO));
    }

    @GetMapping("/getEmpByName")
    public Result getEmpByName(String empName) {

        return Result.success("通过用户名查询员工成功", employeeService.getEmpByName(empName));
    }
}

2. ServiceImpl 实现类。接口实现 IService 接口

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Resource
    private EmployeeDAO employeeDAO;

    @Resource
    private EmployeeDTOConvert employeeDTOConvert;

    @Resource
    private SysMenuService sysMenuService;

    @Resource
    private RedisUtil redisUtil;

    @Override
    public Page<EmployeeDTO> getEmployeeList(EmployeeConditionDTO conditionDTO) {

        QueryWrapper<EmployeeDO> employeeDOQueryWrapper = new QueryWrapper<>();

        if (!StringUtils.isBlank(conditionDTO.getEmpName())) {
            employeeDOQueryWrapper.like("emp_name", conditionDTO.getEmpName());
        }
        if (!StringUtils.isBlank(conditionDTO.getEmpCode())) {
            employeeDOQueryWrapper.eq("emp_code", conditionDTO.getEmpCode());
        }
        if (conditionDTO.getValiFlag() != null) {
            employeeDOQueryWrapper.eq("vali_flag", conditionDTO.getValiFlag());
        }

        Page<EmployeeDO> page = new Page<>(conditionDTO.getPageNum(), conditionDTO.getPageSize());
        Page<EmployeeDO> employeeDOPage = employeeDAO.selectPage(page, employeeDOQueryWrapper);

        Page<EmployeeDTO> employeeDTOPage = new Page<>();
        List<EmployeeDTO> employeeDTOS = new ArrayList<>();
        if (!ObjectUtils.isEmpty(employeeDOPage.getRecords())) {
            employeeDOPage.getRecords().stream().forEach(employeeDO -> employeeDTOS.add(employeeDTOConvert.convertToEmployeeDTO(employeeDO)));
        }

        BeanUtils.copyProperties(employeeDOPage, employeeDTOPage);
        employeeDTOPage.setRecords(employeeDTOS);
        return employeeDTOPage;
    }

    @Override
    public Integer updateEmployee(EmployeeDTO employeeDTO) {
        EmployeeDO employeeDO = employeeDTOConvert.convertToEmployeeDO(employeeDTO);
        employeeDO.setUpdateTime(LocalDateTime.now());

        return employeeDAO.updateById(employeeDO);
    }

    @Override
    public Integer resetPassword(EmployeeDO employeeDO) {
        employeeDO.setUpdateTime(LocalDateTime.now());

        return employeeDAO.updateById(employeeDO);
    }

    @Override
    public EmployeeDO getEmpByName(String empName) {
        return employeeDAO.selectOne(new QueryWrapper<EmployeeDO>().eq("emp_name", empName));
    }

    @Override
    public EmployeeDO getEmpById(String empId) {
        return employeeDAO.selectById(empId);
    }

    @Override
    public EmployeeDTO getEmpByCode(String username) {
        QueryWrapper<EmployeeDO> queryWrapper = new QueryWrapper<EmployeeDO>().eq("emp_code", username);
        EmployeeDO employeeDO = employeeDAO.selectOne(queryWrapper);
        if (employeeDO == null) {
            return null;
        } else {
            EmployeeDTO employeeDTO = employeeDTOConvert.convertToEmployeeDTO(employeeDO);
            return employeeDTO;
        }
    }

    @Override
    public String getAuthority(String empId) {

        String authority = "";

        // 获取角色列表
        List<SysRoleDO> sysRoleDOS = employeeDAO.getAuthority(empId);
        if (sysRoleDOS.size() > 0) {
            String roleCodes = sysRoleDOS.stream().map(sysRoleDO -> "ROLE_" + sysRoleDO.getCode()).collect(Collectors.joining(","));
            authority = roleCodes.concat(",");
        }

        // 获取菜单权限列表
        List<String> navMenuIds = sysMenuService.getNavMenuIds(empId);
        if (navMenuIds.size() > 0) {
            List<SysMenuDO> sysMenuDOS = sysMenuService.list(new QueryWrapper<SysMenuDO>().eq("vali_flag", 1).in("menu_id", navMenuIds));
            String permissions = sysMenuDOS.stream().map(sysMenuDO -> sysMenuDO.getPermission()).collect(Collectors.joining(","));
            authority = authority.concat(permissions);
        }
        return authority;
    }

    @Override
    public void clearUserAuthority(String username) {
        redisUtil.del("GrantedAuthority:" + username);
    }

    @Override
    public void clearUserAuthorityByRoleId(String roleId) {
        List<String> empIds = employeeDAO.getEmpIdsByRoleId(roleId);
        if (empIds.size() > 0) {
            employeeDAO.selectBatchIds(empIds).forEach(employeeDO -> {
                System.out.println("根据角色Id清除用户权限缓存,姓名为:" + employeeDO.getEmpName());
                clearUserAuthority(employeeDO.getEmpName());
            });
        }
    }

    @Override
    public void clearUserAuthorityByMenuId(String menuId) {
        employeeDAO.getEmpsByMenuId(menuId).forEach(employeeDO -> {
            if (employeeDO != null) {
                System.out.println("根据菜单Id清除用户权限缓存,姓名为:" + employeeDO.getEmpName());
                clearUserAuthority(employeeDO.getEmpName());
            }
        });
    }
}

3. Mapper

<mapper namespace="fan.employee.dao.EmployeeDAO">
    <update id="deleteEmployee">
        UPDATE employee
        SET vali_flag = '0'
        WHERE emp_id in
        <foreach item="item" collection="list" index="index" open="(" separator="," close=")">
            #{item}
        foreach>
    update>
    <select id="getAuthority" resultType="fan.security.entity.SysRoleDO">
        SELECT *
        FROM sys_role
        WHERE role_id in (
            SELECT role_id
            FROM sys_employee_role
            WHERE emp_id = #{empId}
        ) and vali_flag = '1'
    select>
    <select id="getEmpIdsByRoleId" resultType="java.lang.String">
        SELECT emp_id
        FROM sys_employee_role
        WHERE role_id = #{roleId}
    select>
    <select id="getEmpsByMenuId" resultType="fan.employee.entity.EmployeeDO">
        SELECT DISTINCT employee.*
        FROM sys_employee_role
                 LEFT JOIN `sys_role_menu` ON sys_role_menu.role_id = sys_employee_role.role_id
                 LEFT JOIN `employee` ON employee.emp_id = sys_employee_role.emp_id
        WHERE sys_role_menu.menu_id = #{menuId}
    select>
mapper>

你可能感兴趣的:(Spring,spring,ui,vue.js)