Spring Security + Vue2 + Element-UI 总结
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,运行
2、进入可视化界面
3、选择项目路径,新建项目
4、输入项目名,下一步
5、可以选择预设或者自定义,默认预设即可。这里选择手动,下一步
6、选择功能,加上 Router 和 Vuex
7、选择 Vue 的版本和路由的模式,这里选择 Vue2 与 不使用 History 模式
8、不保存预设
9、创建完成
10、启动项目,选择 任务 --> serve --> 运行,后续进入项目可通过命令行启动
11、启动成功
12、访问 localhost:8080,初始界面,因没有选择 History 路由模式,地址会带上 # 号
13、项目初始结构
2. Vue 项目搭建
2.1 引入 Element-UI、Axios、Mockjs、QS
2.1.1 引入 Element-UI
- 安装:
npm install element-ui -S
- 完整引入: 在 main.js 引入
// 引入 Element UI 组件库 import ElementUI from 'element-ui'; // 引入 Element UI 全部样式 import 'element-ui/lib/theme-chalk/index.css'; // 应用 Element UI Vue.use(ElementUI);
- 按需引入:
- 安装:
npm install babel-plugin-component -D
,-D 表示开发依赖 - 将 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" } ] ] }
- 在 main.js 引入需要的部分组件:
// 引入 Button、Select 和 Option,按钮、选择器和选项 import { Button, Select, Option } from 'element-ui'; // Button.name 即使用时标签的默认名字 <el-button></el-button>,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
在 src 目录下 新建 mock/index.js
在 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
在 main.js 文件中引入
// 引入 qs
import qs from 'qs'
// 配置全局 qs 属性
Vue.prototype.$qs = qs
2.2 登录页
2.2.1 新建登录组件
在 views 目录下,将原来的默认组件删掉,新建一个 Login 组件
1、使用 Element-UI 的 Layout 布局,加上表单验证
- :model 表示表单数据对象
- :rules 表示表单验证规则
- :status-icon 表示输入框中校验结果反馈图标
- :hide-required-asterisk 表示表单验证必填项的 * 号
- label 表示标签名字
- label-width 表示标签宽度
- 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、修改表单验证提示语样式
<style scoped>
::v-deep .el-form-item__error {
font-size: 14px;
}
</style>
4、修改表单标签样式
<style scoped>
::v-deep .el-form-item__label {
color: #fff;
font-weight: bold;
}
</style>
5、重置表单
this.$refs['loginForm']
表示获取表单,或者写为 this.$refs.loginForm
,resetFields()
用于对整个表单进行重置,将所有字段值重置为初始值并移除校验结果。@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 里添加 <router-view></router-view>
标签,并且将边距设为 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
2.4 整体布局
在 src 目录下创建 layout 文件夹,并且分别创建三个布局组件。使用 Element-UI 的 Container 布局容器
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>
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
})
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 文件,删除标签页时,判断首页不能删除,以及点击标签页跳转到对应路由
<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>
2.6 菜单管理
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="排序号"
>1</el-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="排序号"
>1</el-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 角色管理
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 用户管理
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 执行流程图
流程说明:
- 客户端发起一个请求,进入 Security 过滤器链
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器
- 进入认证 BasicAuthenticationFilter 进行用户认证,成功的话会把认证了的结果写入到 SecurityContextHolder 中 SecurityContext 的属性 authentication 上面。如果认证失败就会交给 AuthenticationEntryPoint 认证失败处理类,或者抛出异常被后续 ExceptionTranslationFilter 过滤器处理异常,如果是 AuthenticationException 就交给 AuthenticationEntryPoint 处理,如果是 AccessDeniedException 异常则交给 AccessDeniedHandler 处理
- 当到 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 依赖
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.4</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
<!-- jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<!-- kaptcha -->
<dependency>
<groupId>cloud.agileframework</groupId>
<artifactId>spring-boot-starter-kaptcha</artifactId>
<version>2.1.0.M19</version>
</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>