学习记录 前后端分离项目
学习视频的来源(链接)
B站的 博主 ps (讲的很不错)
前端的学习笔记记录
安装前端的Vue 环境 ,新建 Vue 项目
安装node.js
官网 地址
安装后傻瓜操作就可以
检查是否安装完成 (命令)
下面是我的版本
出现上面的版本信息说明安装成功
安装vue 的环境
命令
# 安装淘宝的 npm 也可以不安装,不过安装了国内的镜像会快
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue - cli 安装依赖包
cnpm install --g vue-cli
# 打开 vue 的可视化 管理界面 这个可以用可视化创建 vue 的项目,也可以不用 ,我用的命令行创建 vue 的项目
vue ui
创建 vue 的环境
剩下的一直下一步就可以了
在项目的目录下 运行就可以看到
设置在IDEA 里面输入命令访问
把项目导入到 IDEA
我的另一篇安装Vue 的博客
Vue 笔记
安装 element-ui,axios、qs、mockjs
# 安装 element-ui
cnpm install element-ui --save
# 安装 axios
cnpm install axios --save
# 安装 cnpm install qs --save
cnpm install qs --save
# 安装 mockjs
cnpm install mockjs --save-dev
- axios:一个基于 promise 的 HTTP 库,ajax
- qs:查询参数序列化和解析库
- mockjs:为我们生成随机数据的工具
**Mockjs **
在 src 目录下创建 mock.js 文件,用来编写随机的api 现在还没有和后端交互,所以先用假的数据
同时在main.js 中引入这个文件
main.js 文件
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
//引入mock数据,关闭则注释该行
require("./mock")
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
引入 element-ui ,axios
然后我们打开项目src目录下的main.js,引入element-ui依赖 引入axios。
import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
import axios from 'axios'
Vue.use(Element)
页面路由
登录和首页的创建
Router
WebApp 的链路路径管理系统,就是建立起url 和页面之间的映射关系
主要在 src\router\index.js 就是来配置路由的
创建我们的登录页面和首页页面
在路由中心配置 url 与Vue 页面的映射关系 可以参考原来默认的写法 ./src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "../views/Login";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: () => import('../views/Home.vue'),
},
{
path: "/login",
name: "Login",
component: Login
},
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
});
export default router;
修改完 index.js 页面后 我们 启动vue 的项目
npm run serve
启动完成后访问 localhost:8081/login 页面发现页面如下所示 ,出现了 Home | About
原因:
新建的vue 项目没有其他配置,默认就是一个单页面应用,就是说这个应用是由一个外壳页面,和多个页面,组成在跳转的时候没有离开外壳的页面,这个的外壳压面就是App.vue 登录的页面就是一个片段而已,我们应该修改我们的 App.vue 页面
修改 后的App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
<style lang="less">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
**修改完成后在次查看 **
登录开发
登录的开发流程
需要去element-ui 上找到 表单的组件,简单的登录页面,但是登录页面的验证码需要与后台进行交互
主要登录与后台交互的有两个
1,获取登录的验证码
2,提交登录表单完成登录
由于还没有写后端的代码,所以先在我们的mock.js 里写数据,完成交互。开发api
交互的过程
1,打开登录的界面
2,动态加载登录验证码,前后端分离的项目,我们不在使用session 进行交互,所以后端警用session 后端可以随机生成一个验证码的同时生成一个随机码,把随机码作为 key,验证码为value 保存到redis 中,然后把随机码和验证码图片的Base64 字符串码发送到前端
3,前端提交用户名,密码,验证码,还有随机码
4,后台验证是否正确
大概的流程图
登录的页面
<template>
<el-row type="flex" class="row-bg" justify="center">
<el-col :xl="6" :lg="7">
<h2>欢迎来到fjj 管理系统</h2>
<el-image :src="require('@/assets/img.png')" style="height: 180px; width: 180px;"></el-image>
<p>扫码二维码,添加个人微信</p>
</el-col>
<el-col :span="1">
<el-divider direction="vertical"></el-divider>
</el-col>
<el-col :span="6" :lg="7">
<el-form :model="loginForm" :rules="rules" ref="loginForm" label-width="80px">
<el-form-item label="用户名" prop="username" style="width: 380px;">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" style="width: 380px;">
<el-input v-model="loginForm.password" type="password"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code" style="width: 380px;">
<el-input v-model="loginForm.code" style="width: 172px; float: left" maxlength="5"></el-input>
<el-image :src="captchaImg" class="captchaImg" @click="getCaptcha"></el-image>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">立即创建</el-button>
<el-button @click="resetForm('loginForm')">重置</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script>
export default {
name: "Login",
data () {
return {
loginForm: {
username: '',
password: '',
code: '',
token: ''
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 5, max: 5, message: '长度为 5 个字符', trigger: 'blur' }
]
},
captchaImg: null
}
},
methods: {
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$axios.post('/login?' ,this.loginForm).then(res => {
})
} else {
console.log('错误的提交')
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
}
}
}
</script>
<style scoped>
.el-row {
/*background-color: #fafafa;*/
height: 100%;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
margin-top: 10%;
}
.el-divider {
height: 200px;
}
.captchaImg {
float: left;
margin-left: 8px;
border-radius: 4px;
}
</style>
效果
我们的验证码还没有显示出来效果
这个图片二维码是在这里引入的
验证码
此时我们的验证码还没有显示出来,没有与后台交互先用mock 做验证码
先在data 里面设置为 null
** getCaptcha () 的方法。调用创建图片的方法**
getCaptcha () {
this.$axios.post('/captcha').then(res => {
this.loginForm.token = res.data.data.token
this.captchaImg = res.data.data.captchaImg
})
}
创建完成之后不要忘记使用这个方法
在下面调用这个方法
**代码 ,只放了export default 的内容 **
<script>
export default {
name: "Login",
data () {
return {
loginForm: {
username: '',
password: '',
code: '',
token: ''
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
{ min: 5, max: 5, message: '长度为 5 个字符', trigger: 'blur' }
]
},
captchaImg: null
}
},
methods: {
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
// eslint-disable-next-line no-unused-vars
this.$axios.post('/login?' ,this.loginForm).then(res => {
})
} else {
console.log('错误的提交')
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
},
getCaptcha () {
this.$axios.post('/captcha').then(res => {
// res 为结果 结果后的data 里面取到我们的 token 值
// 其实就是我们 后台返回的一个结果,如果正确的话里面会有一个data 的值在
this.loginForm.token = res.data.data.token
// 同理拿出来我们的图片
this.captchaImg = res.data.data.captchaImg
})
}
},
created() {
this.getCaptcha()
}
}
</script>
<style scoped>
.el-row {
/*background-color: #fafafa;*/
height: 100%;
display: flex;
align-items: center;
text-align: center;
justify-content: center;
margin-top: 10%;
}
.el-divider {
height: 200px;
}
.captchaImg {
float: left;
margin-left: 8px;
border-radius: 4px;
}
</style>
在我们的mock.js 里面写数据
// 创建放回的对象
const Mock = require('mockjs')
// 获取随机的 Random
const Random = Mock.Random
// 设置返回结果
let Result = {
code: 200,
msg: '操作成功',
data: null
}
// 图片的请求
Mock.mock('/captcha','post',()=> {
Result.data = {
token: Random.string(32),
captchaImg: Random.dataImage('120x40','jikof')
}
return Result
})
此时我们的效果,现在的验证码是mock 随机生成 的 与后端交互的时候给成后端的api 接口就可以了
登录的请求
**由于我们登录的时候要求把我们的后台的数据放在 localStorage 里面要在 ./src/store/index.js 里面写 这样就可以存到localStorage **
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
token: ''
},
mutations: {
SET_TOKEN: (state,token) => {
state.token = token
localStorage.setItem("token",token)
}
},
actions: {},
modules: {},
});
在我们的 Login.vue 里,把我们的jwt 存到
设置登录成功后跳到首页
一样还是写我们的mock 的请求 测试数据 暂时没有办法把 jwt 给放入,所以测试的时候没有jwt 身份放入,先直接放入,等与后台交互的时候在放入里面
登录后跳转到首页
存到了 只是暂时没有与后台交互,所以 暂时是 undefined
小小的错误记录总结 引入 axios 的错误
这里出现了一个小小的错误记录一下
可是在 用 axios 请求的时候 报错
解决方法
在Login.vue 里面也更换一下
请求就可以发送出去了
设置登录时候的全局 axios
这里要设置全局的原因是因为登录失败,我们是需要弹窗显示错误的,比如验证码错误,用户名或者密码不正确等,不仅仅是这个登录接口,所有的接口调用都会有这个情况,所有我们想做个拦截器,对返回的结果进行分析,如果是异常就直接弹出错误,这样我们就省下来每个接口都写一遍
在src目录下创建一个文件axios.js (与main.js 同级)
import axios from 'axios'
import router from './router'
import Element from 'element-ui'
axios.defaults.baseURL = 'http://localhost:8081'
const request = axios.create({
timeout: 5000,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
})
request.interceptors.request.use(config => {
config.headers.Authorization = localStorage.getItem('token')
return config
})
request.interceptors.response.use(
response => {
console.log('response ->' + response)
const res = response.data
if (res.code === 200) {
return response
} else {
Element.Message.error(!res.msg ? '系统异常' : res.msg)
return Promise.reject(response.data.msg)
}
},
error => {
console.log(error)
if (error.response.data) {
error.massage = error.response.data.msg
}
if (error.response.status === 401) {
router.push('/login')
}
Element.Message.error(error.massage, {duration: 3000})
return Promise.reject(error)
}
)
export default request
前置拦截,可以统一为所有权限的请求装配上header 的 token 的信息,后置拦截中,判断status.code 和 error.response.status 如果是401 未登录没权限的就调用登录页面,其他的就直接弹窗显示错误。
我们需要在原来的main.js 更改成我们自己的axios 的js
测试检查一下,我们先在mock .js 里面输入错误的转态吗
效果
后台界面开发
这里我选择的是
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>Main</el-main>
</el-container>
</el-container>
创建一个Index 的页面 这里我们需要 先放在这里,一会可以把公共的抽离出来 记得复制样式
添加到路由里面
效果
添加一个样式设置一下高度
头部导航栏的设置
Index.vue 页面
<template>
<el-container>
<el-aside width="200px">
<div>菜单栏</div>
</el-aside>
<el-container>
<el-header style="height: 55px;"><Strong>ManHub后台管理系统</Strong>
<div class="header-avatar block">
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
<el-dropdown><span class="el-dropdown-link">
fjj<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-link href="http://markerhub.com">网站</el-link>
</div>
</el-header>
<el-main>
<div style="margin: 0 15px;">
<router-view></router-view>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
name: "Index"
}
</script>
<style scoped>
/*下拉框的css*/
.el-dropdown-link {
cursor: pointer;
color: black;
}
.el-icon-arrow-down {
font-size: 12px;
}
/*设置头部导航的样式*/
.header-avatar {
float: right;
width: 210px;
display: flex;
justify-content: space-around;
align-items: center;
text-align: center;
}
/*导航栏的css*/
.el-container {
padding: 0vh;
margin: 0;
height: 100vh;
}
.el-header {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
color: #333;
padding-left: 20px;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
/*设置 链接滑上去变成小手*/
.el-dropdown-link {
cursor: pointer;
color: black;
}
/*设置侧边栏的高度*/
.el-menu-vertical-demo {
height: 100vh;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
</style>
效果
侧边导航栏的设置
**差不多感觉是对的,在 element-ui 上找到菜单栏的组件,添加到Home .vue 里面,但是要做成动态的菜单,所以可以单独的拿出来,新建一个SideMenu.vue **
<template>
<el-menu class="el-menu-vertical-demo" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<router-link to="/index">
<el-menu-item index="Index">
<template slot="title"><i class="el-icon-s-home"></i> <span slot="title">首页</span></template>
</el-menu-item>
</router-link>
<el-submenu index="1">
<template slot="title"><i class="el-icon-s-operation"></i> <span>系统管理</span></template>
<el-menu-item index="1-1">
<template slot="title"><i class="el-icon-s-custom"></i> <span slot="title">用户管理</span></template>
</el-menu-item>
<el-menu-item index="1-2">
<template slot="title"><i class="el-icon-rank"></i> <span slot="title">角色管理</span></template>
</el-menu-item>
<el-menu-item index="1-3">
<template slot="title"><i class="el-icon-menu"></i> <span slot="title">菜单管理</span></template>
</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-s-tools"></i> <span>系统工具</span></template>
<el-menu-item index="2-2">
<template slot="title"><i class="el-icon-s-order"></i> <span slot="title">数字字典</span></template>
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "SideMenu"
}
</script>
<style scoped>
/*设置侧边栏的高度*/
.el-menu-vertical-demo {
height: 100vh;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
</style>
此时SideMenu.vue 作为一个组件添加到Home.vue 中,我们需要导入,声明组件,才能使用标签,所以应该在Index.vue 中 声明就可以使用了
效果
把整体的框架移除到Home.vue 里面,在Index 里面只留下中间的 内容
移除完之后的Home.vue
<template>
<el-container>
<el-aside width="200px">
<SideMenu></SideMenu>
</el-aside>
<el-container>
<el-header style="height: 55px;"><Strong>ManHub后台管理系统</Strong>
<div class="header-avatar block">
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
<el-dropdown><span class="el-dropdown-link">
fjj<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-link href="http://markerhub.com">网站</el-link>
</div>
</el-header>
<el-main>
<div style="margin: 0 15px;">
<router-view></router-view>
</div>
</el-main>
</el-container>
</el-container>
</template>
<script>
import SideMenu from "./inc/SideMenu";
export default {
name: "Home",
components: {SideMenu}
}
</script>
<style scoped>
/*下拉框的css*/
.el-dropdown-link {
cursor: pointer;
color: black;
}
.el-icon-arrow-down {
font-size: 12px;
}
/*设置头部导航的样式*/
.header-avatar {
float: right;
width: 210px;
display: flex;
justify-content: space-around;
align-items: center;
text-align: center;
}
/*导航栏的css*/
.el-container {
padding: 0vh;
margin: 0;
height: 100vh;
}
.el-header {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 60px;
}
.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}
.el-main {
color: #333;
padding-left: 20px;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
/*设置 链接滑上去变成小手*/
.el-dropdown-link {
cursor: pointer;
color: black;
}
/*设置侧边栏的高度*/
.el-menu-vertical-demo {
height: 100vh;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
</style>
移除完之后的index.vue
<template>
<div>中间部分</div>
</template>
<script>
export default {
name: "Index"
}
</script>
<style scoped>
</style>
这个时候当我们访问Index的时候只有中间的部分没有 整个框架是不行的
应该把index.vue 作为 Home 的子路由 ,这样当我们访问index 的时候就会显示父级的路由了
第一步 修改路由
第二步,修改 Home.vue
加上这句话,这个时候,我们在看效果的时候
侧边导航栏的路由
新建几个页面,先在views 下新建文件夹,然后再新建vue 页面。
添加路由到index.js 中
这个时候我们点击左边的用户管理都还不会点击到页面的链接修改Home 的页面
效果
用户登录信息展示,以及退出登录清除 浏览器缓存
管理界面的右上角是被写死的,我们现在登录成功,所以可以通过接口去请求获取到当前的用户信息,这样就可以动态显示用户信息了
Home.vue
在 mock.js 里面写请求以及测试数据
// 个人中心的测试数据
Mock.mock('/sys/userInfo','get',() =>{
Result.data = {
id: '1',
username :'冯娇娇',
avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
}
return Result
})
效果
退出登录的
mock.js 数据
// 退出登录的
Mock.mock('/logout', 'post', () => {
return Result
})
个人中心的界面
创建个人中心的vue
<template>
<div style="text-align: center;">
<h2>你好!{{ userInfo.username }} 同学</h2>
<el-form :model="passForm" status-icon :rules="rules" ref="passForm" label-width="100px">
<el-form-item label="旧密码" prop="currentPass">
<el-input type="password" v-model="passForm.currentPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input type="password" v-model="passForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="checkPass">
<el-input type="password" v-model="passForm.checkPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('passForm')">提交</el-button>
<el-button @click="resetForm('passForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.passForm.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
return {
userInfo: {
},
passForm: {
password: '',
checkPass: '',
currentPass: ''
},
rules: {
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
checkPass: [
{ required: true, validator: validatePass, trigger: 'blur' }
],
currentPass: [
{ required: true, message: '请输入当前密码', trigger: 'blur' }
]
}
}
},
created () {
this.getUserInfo()
},
methods: {
getUserInfo () {
this.$axios.get('/sys/userInfo').then(res => {
this.userInfo = res.data.data
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/sys/user/updatePass', this.passForm).then(res => {
_this.$alert(res.data.msg, '提示', {
confirmButtonText: '确定',
callback: action => {
this.$refs[formName].resetFields()
}
})
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
}
}
}
</script>
<style scoped>
.el-form {
width: 420px;
margin: 50px auto;
}
</style>
需要在 Home .vue 里写这句话
在index 里面设置我们的路由就可以了
点击之后
动态菜单栏开发
上面的代码中,左侧的菜单栏的数据是写死的,在实际情况中不应该是写死的,因为菜单是需要根据登录用户的权限动态显示菜单的,也就是用户看到的菜单栏可能是不一样的,这些数据需要去后端访问获取到
应该把数据简化成一个json 数组数据,然后for 循环展示出来,代码如下
SideMenu.vue
<template>
<el-menu class="el-menu-vertical-demo" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<router-link to="/index">
<el-menu-item index="Index">
<template slot="title"><i class="el-icon-s-home"></i> <span slot="title">首页</span></template>
</el-menu-item>
</router-link>
<el-submenu :index="menu.name" v-for="menu in menuList" :key="menu.name">
<template slot="title">
<i :class="menu.icon"></i>
<span>{{menu.title}}</span>
</template>
<router-link :to="item.path" v-for="item in menu.children" :key="item.path">
<el-menu-item :index="item.name" @click="selectMenu(item)">
<template slot="title">
<i :class="item.icon"></i>
<span slot="title">{{item.title}}</span>
</template>
</el-menu-item>
</router-link>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "SideMenu",
data() {
return {
menuList: [{
name: 'SysManga',
title: '系统管理',
icon: 'el-icon-s-operation',
path: '',
component: '',
children: [{
name: 'SysUser',
title: '用户管理',
icon: 'el-icon-s-custom',
path: '/user',
children: []
}]
}, {
name: 'SysTools',
title: '系统工具',
icon: 'el-icon-s-tools',
path: '',
children: [{
name: 'SysDict',
title: '数字字典',
icon: 'el-icon-s-order',
path: '/sys/dicts',
children: []
},]
}],
}
}
}
</script>
<style scoped>
/*设置侧边栏的高度*/
.el-menu-vertical-demo {
height: 100vh;
}
/*取除a 标签的下划线*/
a {
text-decoration: none;
}
</style>
可以看到,用 for 循环显示数据,那么这样变动菜单栏的时候只需要修改data 中的menuList 即可,效果和之前的完全一样。现在menuList 的数据我们是直接写到页面data 上的,一般我们是要请求后端的,所以需要定义一个mock 的接口,应为是动态的菜单,我们也要考虑到权限问题,所以我们请求数据的时候一般出来动态菜单,还要权限的数据,比如菜单的添加,删除是否有权限,是否能显示等。。
Mock.js
Mock.mock('/sys/menu/nav', 'get', () => {
// 菜单json
let nav = [
{
name: 'SysManga',
...
},
{
name: 'SysTools',
...
}
]
// 权限数据
let authoritys = ['SysUser', "SysUser:save"]
Result.data = {}
Result.data.nav = nav
Result.data.authoritys = authoritys
return Result
定义好导航菜单的接口,应该在登录成功完成之后调用,但是并不是每一次打开都需要登录,也就是浏览器已经存储到用户token 的时候我们不需要再去登录了所以我们不能放在登录完成的方法里了。
这里需要考虑一个问题,就是导航菜单的路由问题,当我们点击菜单之后路由到那个页面是需要在 router 中声明
解决方案: 动态渲染,把加载到导航菜单数据动态绑定路由
把加载菜单数据这个动作放在 router.js 中,Router 有个前缀拦截,就是在路由到页面之前我们可以做一些判断或者加载数据
在router.js中添加一下代码:
src/router/index.js
// 动态导航栏的
router.beforeEach((to, from, next) => {
let hasRoute = store.state.menus.hasRoute
const token = localStorage.getItem('token')
if (to.path === '/login') {
next()
} else if (!token) {
next({ path: '/login' })
}
if (token && !hasRoute) {
axios.get('/sys/menu/nav', {
headers: {
Authorization: localStorage.getItem('token')
}
}).then(res => {
console.log(res.data.data)
store.commit('setMenuList', res.data.data.nav)
store.commit('setPermList', res.data.data.authoritys)
// 动态绑定路由
const newRoutes = router.options.routes
res.data.data.nav.forEach(menu => {
if (menu.children) {
menu.children.forEach(e => {
const route = menuToRoute(e)
if (route) {
newRoutes[0].children.push(route)
}
})
}
})
console.log(newRoutes)
console.log('newRoutes')
router.addRoutes(newRoutes)
hasRoute = true
store.commit('changeRouteStatus', hasRoute)
})
}
next()
})
const menuToRoute = (menu) => {
if (!menu.component) {
return null
}
// 复制到属性
const route = {
path: menu.path,
name: menu.name,
meta: {
icon: menu.icon,
title: menu.title
}
}
route.component = () => import('@/views/' + menu.component + '.vue')
console.log(route)
console.log('route')
return route
}
export default router
可以看到,我们通过menuToRoute 就是把menu 数据 转换成路由对象,然后router.addRoutes(newRoutes)动态添加路由对象同时上面的menu对象中,有个menu.component,这个就是连接对应的组件,我们需要添加上去,比如说/sys/users链接对应到component(sys/User)。同时上面的menu对象中,有个menu.component,这个就是连接对应的组件,我们需要添加上去,比如说/sys/users链接对应到component(sys/User)。
// 菜单的json
const nav = [{
name: 'SysManga',
title: '系统管理',
icon: 'el-icon-s-operation',
component: '',
path: '',
children: [
{
title: '用户管理',
icon: 'el-icon-s-custom',
path: '/sys/users',
name: 'SysUser',
component: 'sys/User',
children: []
},
{
name: 'SysRole',
title: '角色管理',
icon: 'el-icon-rank',
path: '/sys/roles',
component: 'sys/Role',
children: []
},
{
name: 'SysMenu',
title: '菜单管理',
icon: 'el-icon-menu',
path: '/sys/menus',
component: 'sys/Menu',
children: []
}
]
}, {
name: 'SysTools',
title: '系统工具',
icon: 'el-icon-s-tools',
path: '',
component: '',
children: [
{
title: '数字字典',
icon: 'el-icon-s-order',
path: '/sys/dicts',
component: '',
children: []
}]
}]
同时上面router中我们还通过判断是否登录页面,是否有token等判断提前判断是否能加载菜单,同时还做了个开关hasRoute来动态判断是否已经加载过菜单。还需要在store中定义几个方法用于存储数据,我们定义一个menu模块,所以在store中新建文件夹modules,然后新建menus.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default {
state: { // 菜单栏数据
menuList: [],
// 权限数据
permList: [],
hasRoute: false
}, mutations: {
changeRouteStatus(state, hasRoute) {
state.hasRoute = hasRoute
sessionStorage.setItem("hasRoute", hasRoute)
}, setMenuList(state, menus) {
state.menuList = menus
}, setPermList(state, authoritys) {
state.permList = authoritys
}
}
}
记得在store中import这个模块,然后添加到modules:src/store/index.js
import menus from "./modules/menus"
...
export default new Vuex.Store({
... modules:
{ menus }})
这样我们菜单的数据就可以加载了,然后再SideMenu.vue中直接获取store中的menuList数据即可显示菜单出来了。src/views/inc/SideMenu.vue
data() {
return {
menuList: this.$store.state.menus.menuList,
}}
效果如下
动态标签页开发
1,当我们点击导航菜单,上方会添加一个对应的标签,注意不能重复添加,发现已存在标签直接切换到这标签就可以
2,删除当前标签的时候会自动切换到前一个标签页
3,点击标签的时候会调整到对应的内容页中
我们先和左侧菜单一样单独定义一个组件Tabs.vue放在views/inc文件夹内:
src/views/inc/Tabs.vue
<template>
<el-tabs v-model="editableTabsValue" type="card" closable @tab-remove="removeTab" @tab-click="clickTab">
<el-tab-pane
v-for="(item) in editableTabs"
:key="item.name"
:label="item.title"
:name="item.name"
>
</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
name: 'Tabs',
data () {
return {
editableTabsValue: this.$store.state.menus.editableTabsValue,
tabIndex: 2
}
},
computed: {
editableTabs: {
get () {
return this.$store.state.menus.editableTabs
},
set (val) {
this.$store.state.menus.editableTabs = val
}
}
},
methods: {
removeTab (targetName) {
const tabs = this.editableTabs
let activeName = this.editableTabsValue
if (targetName === 'Index') {
return
}
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
const 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 (target) {
this.$router.push({ name: target.name })
}
}
}
</script>
<style scoped>
</style>
computed 表示当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值,其他clickTab removeTab 的逻辑就比较简单,特别是removeTab 注意考虑多种情况就可以,修改meun.js 添加editableTabsValue 和 editableTabs 然后把首页作为默认显示的页面
src/store/modules/menus.js
state: {
// 菜单栏数据
menuList: [],
// 权限数据
permList: [],
hasRoute: false,
editableTabsValue: 'Index',
editableTabs: [{
title: '首页',
name: 'Index'
}]
},
然后再Home.vue中引入我们Tabs.vue这个组件
# 引入组件import Tabs from "./inc/Tabs"
# 声明组件
components: {
SideMenu, Tabs
},
<el-main>
# 使用组件
<Tabs></Tabs>
<div style="margin: 0 15px;">
<router-view></router-view>
</div>
</el-main>
现在的效果
完成了第一步之后,现在我们需要点击菜单导航,然后tabs 列表中添加tab 标签页,那么我们应该修改sideMeun.vue 页面
因为tabs标签列表我们是存储在store中的,因此我们需要commit提交事件,因此我们在menu.js中添加addTabs方法:
刷新浏览器之后链接/sys/users不变,内容不变,但是Tab却不见了,所以我们需要修补一下,当用户是直接通过输入链接形式打开页面的时候我们也能根据链接自动添加激活指定的tab。那么在哪里添加这个回显的方法呢?router中?其实可以,只不过我们需要做判断,因为每次点击导航都会触发router。有没有更简便的方法?有的!因为刷新或者打开页面都是一次性的行为,所以我们可以在更高层的App.vue中做这个回显动作,具体如下:
上面代码可以看到,除了login页面,其他页面都会触发addTabs方法,这样我们就可以添加tab和激活tab了。
个人中心
个人中心的页面
创建 UserCenCenter.vue
<template>
<div style="text-align: center;">
<h2>你好!{{ userInfo.username }} 同学</h2>
<el-form :model="passForm" status-icon :rules="rules" ref="passForm" label-width="100px">
<el-form-item label="旧密码" prop="currentPass">
<el-input type="password" v-model="passForm.currentPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input type="password" v-model="passForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="checkPass">
<el-input type="password" v-model="passForm.checkPass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('passForm')">提交</el-button>
<el-button @click="resetForm('passForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.passForm.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
return {
userInfo: {
},
passForm: {
password: '',
checkPass: '',
currentPass: ''
},
rules: {
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 12, message: '长度在 6 到 12 个字符', trigger: 'blur' }
],
checkPass: [
{ required: true, validator: validatePass, trigger: 'blur' }
],
currentPass: [
{ required: true, message: '请输入当前密码', trigger: 'blur' }
]
}
}
},
created () {
this.getUserInfo()
},
methods: {
getUserInfo () {
this.$axios.get('/sys/userInfo').then(res => {
this.userInfo = res.data.data
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
const _this = this
this.$axios.post('/sys/user/updatePass', this.passForm).then(res => {
_this.$alert(res.data.msg, '提示', {
confirmButtonText: '确定',
callback: action => {
this.$refs[formName].resetFields()
}
})
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
}
}
}
</script>
<style scoped>
.el-form {
width: 420px;
margin: 50px auto;
}
</style>
菜单界面
菜单页面
菜单管理我们用到了Table表格组件的树形结构数据,我们只需要根据例子自己组装数据,就可以自动显示出来了
这里原来应该是一个 树形结构 但是elemenui 没有就加了 -
具体代码
src/views/sys/Menu.vue
<template>
<div>
<el-form :inline="true" :model="formInline" ref="editForm" class="demo-form-inline">
<el-form-item>
<el-button type="primary" @click="dialogVisible = true">新增</el-button>
</el-form-item>
</el-form>
<el-table
:data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
stripe
default-expand-all
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column
prop="name"
label="名称"
sortable
width="180">
</el-table-column>
<el-table-column
prop="perm"
label="权限编码"
sortable
width="180">
</el-table-column>
<el-table-column
prop="icon"
label="图标">
</el-table-column>
<el-table-column
prop="type"
label="类型">
<template slot-scope="scope">
<el-tag size="small" v-if="scope.row.type === 0">目录</el-tag>
<el-tag size="small" v-else-if="scope.row.type === 1" type="success">菜单</el-tag>
<el-tag size="small" v-else-if="scope.row.type === 2" type="info">按钮</el-tag>
</template>
</el-table-column>
<el-table-column
prop="path"
label="菜单URL">
</el-table-column>
<el-table-column
prop="component"
label="菜单组件">
</el-table-column>
<el-table-column
prop="orderNum"
label="排序号">
</el-table-column>
<el-table-column
prop="statu"
label="状态">
<template slot-scope="scope">
<el-tag size="small" v-if="scope.row.statu === 1" type="success">正常</el-tag>
<el-tag size="small" v-else-if="scope.row.statu === 0" type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="icon"
label="操作">
<template slot-scope="scope">
<el-button type="text" @click="editHandle(scope.row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<template>
<el-popconfirm title="这是一段内容确定删除吗?" @confirm="delHandle(scope.row.id)">
<el-button type="text" slot="reference" >删除</el-button>
</el-popconfirm>
</template>
</template>
</el-table-column>
</el-table>
<!-- 新增对话框-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="600px"
:before-close="handleClose">
<el-form :model="editForm" :rules="editFormRules" ref="editForm" label-width="100px" class="demo-editForm">
<el-form-item label="上级菜单" prop="parentId">
<el-select v-model="editForm.parentId" placeholder="请选择上级菜单">
<template v-for="item in tableData">
<el-option :label="item.name" :value="item.id"></el-option>
<template v-for="chid in item.children">
<el-option :label="chid.name" :value="chid.id"></el-option>
<span>{{ '- ' + chid.name}}</span>
</template>
</template>
</el-select>
</el-form-item>
<el-form-item label="菜单名称" prop="name" label-width="100px">
<el-input v-model="editForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="权限编码" prop="perms" label-width="100px">
<el-input v-model="editForm.perms" 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="statu" label-width="100px">
<el-radio-group v-model="editForm.statu">
<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-item>
<el-button type="primary" @click="submitForm('editForm')">立即创建</el-button>
<el-button @click="resetForm('editForm')">重置</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'Meun',
data () {
return {
dialogVisible: false,
editForm: [],
editFormRules: {
parentId: [
{ required: true, message: '请选择上级菜单', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入名称', trigger: 'blur' }
],
perms: [
{ required: true, message: '请输入权限编码', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择状态', trigger: 'blur' }
],
orderNum: [
{ required: true, message: '请填入排序号', trigger: 'blur' }
],
statu: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
},
tableData: []
}
},
created () {
this.getMenuTree()
},
methods: {
getMenuTree () {
this.$axios.get('/sys/menu/list').then(res => {
this.tableData = res.data.data
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$axios.post('/sys/menu/' + (this.editForm.id ? 'update' : 'save'), this.editForm)
.then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getMenuTree()
}
})
this.dialogVisible = false
})
} else {
console.log('error submit!!')
return false
}
})
},
editHandle (id) {
this.dialogVisible = true
this.$axios.get('/sys/menu/info/' + id).then(res => {
console.log(res.data.data + '99999999')
this.editForm = res.data.data
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
this.dialogVisible = false
this.editForm = {}
},
handleClose () {
this.resetForm('editForm')
},
delHandle (id) {
this.$axios.post('/sys/menu/delete/' + id).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getMenuTree()
}
})
})
}
}
}
</script>
<style scoped>
</style>
角色管理
角色页面
角色需要和菜单权限做关联,菜单是个树形结构的,
src/views/sys/Role.vue
<template>
<div>
<el-form :inline="true">
<el-form-item>
<el-input
v-model="searchForm.name"
placeholder="名称"
clearable
>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="getRoleList">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogVisible = true">新增</el-button>
</el-form-item>
<el-form-item>
<el-popconfirm title="这是确定批量删除吗?" @confirm="delHandle(null)">
<el-button type="danger" slot="reference" :disabled="delBtlStatu">批量删除</el-button>
</el-popconfirm>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
border
stripe
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="name"
label="名称"
width="120">
</el-table-column>
<el-table-column
prop="code"
label="唯一编码"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="remark"
label="描述"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="statu"
label="状态">
<template slot-scope="scope">
<el-tag size="small" v-if="scope.row.statu === 1" type="success">正常</el-tag>
<el-tag size="small" v-else-if="scope.row.statu === 0" type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="icon"
label="操作">
<template slot-scope="scope">
<el-button type="text" @click="permHandle(scope.row.id)">分配权限</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="editHandle(scope.row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<template>
<el-popconfirm title="这是一段内容确定删除吗?" @confirm="delHandle(scope.row.id)">
<el-button type="text" slot="reference">删除</el-button>
</el-popconfirm>
</template>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
:current-page="current"
:page-size="size"
:total="total">
</el-pagination>
<!--新增对话框-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="600px"
:before-close="handleClose">
<el-form :model="editForm" :rules="editFormRules" ref="editForm" label-width="100px" class="demo-editForm">
<el-form-item label="角色名称" prop="name" label-width="100px">
<el-input v-model="editForm.name" 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="statu" label-width="100px">
<el-radio-group v-model="editForm.statu">
<el-radio :label=0>禁用</el-radio>
<el-radio :label=1>正常</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('editForm')">立即创建</el-button>
<el-button @click="resetForm('editForm')">重置</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog
title="分配权限"
:visible.sync="permDialogVisible"
width="600px">
<el-form :model="permForm">
<el-tree
:data="permTreeData"
show-checkbox
ref="permTree"
:default-expand-all=true
node-key="id"
:check-strictly=true
:props="defaultProps">
</el-tree>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="permDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitPermFormHandle('permForm')">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'Role',
data () {
return {
searchForm: {},
delBtlStatu: true,
total: 0,
size: 10,
current: 1,
dialogVisible: false,
editForm: {
},
tableData: [],
editFormRules: {
name: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
code: [
{ required: true, message: '请输入唯一编码', trigger: 'blur' }
],
statu: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
},
multipleSelection: [],
permDialogVisible: false,
permForm: {},
defaultProps: {
children: 'children',
label: 'name'
},
permTreeData: []
}
},
created () {
this.getRoleList()
this.$axios.get('/sys/menu/list').then(res => {
this.permTreeData = res.data.data
})
},
methods: {
toggleSelection (rows) {
if (rows) {
rows.forEach(row => {
this.$refs.multipleTable.toggleRowSelection(row)
})
} else {
this.$refs.multipleTable.clearSelection()
}
},
handleSelectionChange (val) {
console.log('勾选')
console.log(val)
this.multipleSelection = val
this.delBtlStatu = val.length === 0
},
handleSizeChange (val) {
console.log(`每页 ${val} 条`)
this.size = val
this.getRoleList()
},
handleCurrentChange (val) {
console.log(`当前页: ${val}`)
this.current = val
this.getRoleList()
},
resetForm (formName) {
this.$refs[formName].resetFields()
this.dialogVisible = false
this.editForm = {}
},
handleClose () {
this.resetForm('editForm')
},
getRoleList () {
this.$axios.get('/sys/role/list', {
params: {
name: this.searchForm.name,
current: this.current,
size: this.size
}
}).then(res => {
this.tableData = res.data.data.records
this.size = res.data.data.size
this.current = res.data.data.current
this.total = res.data.data.total
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$axios.post('/sys/role/' + (this.editForm.id ? 'update' : 'save'), this.editForm)
.then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getRoleList()
}
})
this.dialogVisible = false
this.resetForm(formName)
})
} else {
console.log('error submit!!')
return false
}
})
},
editHandle (id) {
this.$axios.get('/sys/role/info/' + id).then(res => {
this.editForm = res.data.data
this.dialogVisible = true
})
},
delHandle (id) {
var ids = []
if (id) {
ids.push(id)
} else {
this.multipleSelection.forEach(row => {
ids.push(row.id)
})
}
console.log(ids)
this.$axios.post('/sys/role/delete', ids).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getRoleList()
}
})
})
},
permHandle (id) {
this.permDialogVisible = true
this.$axios.get('/sys/role/info/' + id).then(res => {
this.$refs.permTree.setCheckedKeys(res.data.data.menuIds)
this.permForm = res.data.data
})
},
submitPermFormHandle (formName) {
var menuIds = this.$refs.permTree.getCheckedKeys()
console.log(menuIds)
this.$axios.post('/sys/role/perm/' + this.permForm.id, menuIds).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getRoleList()
}
})
this.permDialogVisible = false
this.resetForm(formName)
})
}
}
}
</script>
<style scoped>
.el-pagination {
float: right;
margin-top: 22px;
}
</style>
用户界面
用户页面
用户管理有个操作叫分配角色,和角色添加权限差不多的操作
<template>
<div>
<el-form :inline="true">
<el-form-item>
<el-input
v-model="searchForm.username"
placeholder="用户名"
clearable
>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="getUserList">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogVisible = true" v-if="hasAuth('sys:user:save')">新增</el-button>
</el-form-item>
<el-form-item>
<el-popconfirm title="这是确定批量删除吗?" @confirm="delHandle(null)">
<el-button type="danger" slot="reference" :disabled="delBtlStatu" v-if="hasAuth('sys:user:delete')">批量删除</el-button>
</el-popconfirm>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
border
stripe
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
label="头像"
width="50">
<template slot-scope="scope">
<el-avatar size="small" :src="scope.row.avatar"></el-avatar>
</template>
</el-table-column>
<el-table-column
prop="username"
label="用户名"
width="120">
</el-table-column>
<el-table-column
prop="code"
label="角色名称">
<template slot-scope="scope">
<el-tag size="small" type="info" v-for="item in scope.row.sysRoles">{{item.name}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="email"
label="邮箱">
</el-table-column>
<el-table-column
prop="phone"
label="手机号">
</el-table-column>
<el-table-column
prop="statu"
label="状态">
<template slot-scope="scope">
<el-tag size="small" v-if="scope.row.statu === 1" type="success">正常</el-tag>
<el-tag size="small" v-else-if="scope.row.statu === 0" type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="created"
width="200"
label="创建时间"
>
</el-table-column>
<el-table-column
prop="icon"
width="260px"
label="操作">
<template slot-scope="scope">
<el-button type="text" @click="roleHandle(scope.row.id)">分配角色</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="repassHandle(scope.row.id, scope.row.username)">重置密码</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" @click="editHandle(scope.row.id)">编辑</el-button>
<el-divider direction="vertical"></el-divider>
<template>
<el-popconfirm title="这是一段内容确定删除吗?" @confirm="delHandle(scope.row.id)">
<el-button type="text" slot="reference">删除</el-button>
</el-popconfirm>
</template>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
:current-page="current"
:page-size="size"
:total="total">
</el-pagination>
<!--新增对话框-->
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="600px"
:before-close="handleClose">
<el-form :model="editForm" :rules="editFormRules" ref="editForm">
<el-form-item label="用户名" prop="username" label-width="100px">
<el-input v-model="editForm.username" autocomplete="off"></el-input>
<el-alert
title="初始密码为888888"
:closable="false"
type="info"
style="line-height: 12px;"
></el-alert>
</el-form-item>
<el-form-item label="邮箱" prop="email" label-width="100px">
<el-input v-model="editForm.email" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="phone" label-width="100px">
<el-input v-model="editForm.phone" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="状态" prop="statu" label-width="100px">
<el-radio-group v-model="editForm.statu">
<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="resetForm('editForm')">取 消</el-button>
<el-button type="primary" @click="submitForm('editForm')">确 定</el-button>
</div>
</el-dialog>
<!-- 分配权限对话框 -->
<el-dialog title="分配角色" :visible.sync="roleDialogFormVisible" width="600px">
<el-form :model="roleForm">
<el-tree
:data="roleTreeData"
show-checkbox
ref="roleTree"
:check-strictly=checkStrictly
node-key="id"
:default-expand-all=true
:props="defaultProps">
</el-tree>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="roleDialogFormVisible=false">取 消</el-button>
<el-button type="primary" @click="submitRoleHandle('roleForm')">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SysUser',
data () {
return {
searchForm: {},
delBtlStatu: true,
total: 0,
size: 10,
current: 1,
dialogVisible: false,
editForm: {
},
tableData: [],
editFormRules: {
username: [
{ required: true, message: '请输入用户名称', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' }
],
statu: [
{ required: true, message: '请选择状态', trigger: 'blur' }
]
},
multipleSelection: [],
roleDialogFormVisible: false,
defaultProps: {
children: 'children',
label: 'name'
},
roleForm: {},
roleTreeData: [],
treeCheckedKeys: [],
checkStrictly: true
}
},
created () {
this.getUserList()
this.$axios.get('/sys/role/list').then(res => {
this.roleTreeData = res.data.data.records
})
},
methods: {
toggleSelection (rows) {
if (rows) {
rows.forEach(row => {
this.$refs.multipleTable.toggleRowSelection(row)
})
} else {
this.$refs.multipleTable.clearSelection()
}
},
handleSelectionChange (val) {
console.log('勾选')
console.log(val)
this.multipleSelection = val
this.delBtlStatu = val.length === 0
},
handleSizeChange (val) {
console.log(`每页 ${val} 条`)
this.size = val
this.getUserList()
},
handleCurrentChange (val) {
console.log(`当前页: ${val}`)
this.current = val
this.getUserList()
},
resetForm (formName) {
this.$refs[formName].resetFields()
this.dialogVisible = false
this.editForm = {}
},
handleClose () {
this.resetForm('editForm')
},
getUserList () {
this.$axios.get('/sys/user/list', {
params: {
username: this.searchForm.username,
current: this.current,
size: this.size
}
}).then(res => {
this.tableData = res.data.data.records
this.size = res.data.data.size
this.current = res.data.data.current
this.total = res.data.data.total
})
},
submitForm (formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$axios.post('/sys/user/' + (this.editForm.id ? 'update' : 'save'), this.editForm)
.then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getUserList()
}
})
this.dialogVisible = false
})
} else {
console.log('error submit!!')
return false
}
})
},
editHandle (id) {
this.$axios.get('/sys/user/info/' + id).then(res => {
this.editForm = res.data.data
this.dialogVisible = true
})
},
delHandle (id) {
var ids = []
if (id) {
ids.push(id)
} else {
this.multipleSelection.forEach(row => {
ids.push(row.id)
})
}
console.log(ids)
this.$axios.post('/sys/user/delete', ids).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getUserList()
}
})
})
},
roleHandle (id) {
this.roleDialogFormVisible = true
this.$axios.get('/sys/user/info/' + id).then(res => {
this.roleForm = res.data.data
const roleIds = []
res.data.data.sysRoles.forEach(row => {
roleIds.push(row.id)
})
this.$refs.roleTree.setCheckedKeys(roleIds)
})
},
submitRoleHandle (formName) {
var roleIds = this.$refs.roleTree.getCheckedKeys()
console.log(roleIds)
this.$axios.post('/sys/user/role/' + this.roleForm.id, roleIds).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
this.getUserList()
}
})
this.roleDialogFormVisible = false
})
},
repassHandle (id, username) {
this.$confirm('将重置用户【' + username + '】的密码, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post('/sys/user/repass', id).then(res => {
this.$message({
showClose: true,
message: '恭喜你,操作成功',
type: 'success',
onClose: () => {
}
})
})
})
}
}
}
</script>
<style scoped>
.el-pagination {
float: right;
margin-top: 22px;
}
</style>
按钮权限控制
上面的菜单,角色,用户有的操作,不是每个用户都有的,没有权限的用户我们应该隐藏按钮。
我们再src下面新建一个js文件用于定义一个全局使用的方法:、
src/globalFun.js
import Vue from 'vue'
Vue.mixin({
methods: {
hasAuth (perm) {
var authority = this.$store.state.menus.permList
return authority.indexOf(perm) > -1
}
}
})
在架子啊菜单的时候 要同时架子啊权限数据,现在需要用到权限数据 这里数组,因此我们通过按钮的权限是否在权限列表内就可以了。mixin 的作用是多个组件可以共享数据和方法,在使用mixin 的组件中引入后,mixin 中的方法和属性也就并入到该组件中,可以直接使用,在已有的组件数据和方法进行扩充。在main.js 引入这个文件
src\main.js
import gobal from "./globalFun"
这样全局就可以使用啦,比如我们在新增按钮这里做判断:
src/views/sys/Menu.vue
<el-button type="primary" @click="dialogFormVisible = true" v-if="hasAuth('sys:menu:save')">新增</el-button>
通过v-if来判断返回是否为true从而判断是否显示。
效果
当登录的是 test 的时候没有新增的按钮
admin 的时候是有的
后端的学习笔记记录
前言
技术栈
SpringBoot + mybatis plus + spring security + lombok +redis + hibernate validatior + jwt
新建Springboot 项目,注意版本
开发工具与环境:
idea
mysql
maven 3.3.9
项目的结构
pom.xml
<dependencies>
<!-- Spring boot 启动的 jar -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 热部署的jar -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- lombok get ,set 生成 jar-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--整合mybatis plus https://baomidou.com/-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--mp代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- springboot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MySQL 依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
</dependencies>
配置文件
server:
port: 8081
# DataSource Config
spring:
# Spring security 的账号密码修改
security:
user:
name: user
password: 111111
# 热部署的
freemarker:
cache: false
devtools:
restart:
enabled: true
additional-exclude: WEB-INF/**
#数据库的
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
# mybatis-plus 的
mybatis-plus:
mapper-locations: classpath*:/mapper/**Mapper.xml
# 设置 jwt 的配置
fjj:
jwt:
header: Authorization
expire: 604800 #7天,秒单位
secret: ji8n3439n439n43ld9ne9343fdfer49h
开启mapper接口扫描,添加分页,防全表更新插件
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,
* * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false
* * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
// 加入注解
@Configuration
// 扫描 mapper 包
@MapperScan("com.example.springbootvue.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页的插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 防止全表更新的插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer () {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
给Mybatis plus 添加了2个 拦截器,根据 mp 官网配置的
PaginationInnerInterceptor:新的分页插件
BlockAttackInnerInterceptor:防止全表更新和删除
创建数据库和表
主要 5个 表 用户表,角色表,菜单权限表,以及关联的用户角色中间表,菜单角色中间表。5个表。
数据库脚本
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`name` varchar(64) NOT NULL,
`path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
`perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
`component` varchar(255) DEFAULT NULL,
`type` int(5) NOT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
`icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
`orderNum` int(11) DEFAULT NULL COMMENT '排序',
`created` datetime NOT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '0', '系统管理', '', 'sys:manage', '', '0', 'el-icon-s-operation', '1', '2021-01-15 18:58:18', '2021-01-15 18:58:20', '1');
INSERT INTO `sys_menu` VALUES ('2', '1', '用户管理', '/sys/users', 'sys:user:list', 'sys/User', '1', 'el-icon-s-custom', '1', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('3', '1', '角色管理', '/sys/roles', 'sys:role:list', 'sys/Role', '1', 'el-icon-rank', '2', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('4', '1', '菜单管理', '/sys/menus', 'sys:menu:list', 'sys/Menu', '1', 'el-icon-menu', '3', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('5', '0', '系统工具', '', 'sys:tools', null, '0', 'el-icon-s-tools', '2', '2021-01-15 19:06:11', null, '1');
INSERT INTO `sys_menu` VALUES ('6', '5', '数字字典', '/sys/dicts', 'sys:dict:list', 'sys/Dict', '1', 'el-icon-s-order', '1', '2021-01-15 19:07:18', '2021-01-18 16:32:13', '1');
INSERT INTO `sys_menu` VALUES ('7', '3', '添加角色', '', 'sys:role:save', '', '2', '', '1', '2021-01-15 23:02:25', '2021-01-17 21:53:14', '0');
INSERT INTO `sys_menu` VALUES ('9', '2', '添加用户', null, 'sys:user:save', null, '2', null, '1', '2021-01-17 21:48:32', null, '1');
INSERT INTO `sys_menu` VALUES ('10', '2', '修改用户', null, 'sys:user:update', null, '2', null, '2', '2021-01-17 21:49:03', '2021-01-17 21:53:04', '1');
INSERT INTO `sys_menu` VALUES ('11', '2', '删除用户', null, 'sys:user:delete', null, '2', null, '3', '2021-01-17 21:49:21', null, '1');
INSERT INTO `sys_menu` VALUES ('12', '2', '分配角色', null, 'sys:user:role', null, '2', null, '4', '2021-01-17 21:49:58', null, '1');
INSERT INTO `sys_menu` VALUES ('13', '2', '重置密码', null, 'sys:user:repass', null, '2', null, '5', '2021-01-17 21:50:36', null, '1');
INSERT INTO `sys_menu` VALUES ('14', '3', '修改角色', null, 'sys:role:update', null, '2', null, '2', '2021-01-17 21:51:14', null, '1');
INSERT INTO `sys_menu` VALUES ('15', '3', '删除角色', null, 'sys:role:delete', null, '2', null, '3', '2021-01-17 21:51:39', null, '1');
INSERT INTO `sys_menu` VALUES ('16', '3', '分配权限', null, 'sys:role:perm', null, '2', null, '5', '2021-01-17 21:52:02', null, '1');
INSERT INTO `sys_menu` VALUES ('17', '4', '添加菜单', null, 'sys:menu:save', null, '2', null, '1', '2021-01-17 21:53:53', '2021-01-17 21:55:28', '1');
INSERT INTO `sys_menu` VALUES ('18', '4', '修改菜单', null, 'sys:menu:update', null, '2', null, '2', '2021-01-17 21:56:12', null, '1');
INSERT INTO `sys_menu` VALUES ('19', '4', '删除菜单', null, 'sys:menu:delete', null, '2', null, '3', '2021-01-17 21:56:36', null, '1');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`code` varchar(64) NOT NULL,
`remark` varchar(64) DEFAULT NULL COMMENT '备注',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE,
UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('3', '普通用户', 'normal', '只有基本查看功能', '2021-01-04 10:09:14', '2021-01-30 08:19:52', '1');
INSERT INTO `sys_role` VALUES ('6', '超级管理员', 'admin', '系统默认最高权限,不可以编辑和任意修改', '2021-01-16 13:29:03', '2021-01-17 15:50:45', '1');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL,
`menu_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('60', '6', '1');
INSERT INTO `sys_role_menu` VALUES ('61', '6', '2');
INSERT INTO `sys_role_menu` VALUES ('62', '6', '9');
INSERT INTO `sys_role_menu` VALUES ('63', '6', '10');
INSERT INTO `sys_role_menu` VALUES ('64', '6', '11');
INSERT INTO `sys_role_menu` VALUES ('65', '6', '12');
INSERT INTO `sys_role_menu` VALUES ('66', '6', '13');
INSERT INTO `sys_role_menu` VALUES ('67', '6', '3');
INSERT INTO `sys_role_menu` VALUES ('68', '6', '7');
INSERT INTO `sys_role_menu` VALUES ('69', '6', '14');
INSERT INTO `sys_role_menu` VALUES ('70', '6', '15');
INSERT INTO `sys_role_menu` VALUES ('71', '6', '16');
INSERT INTO `sys_role_menu` VALUES ('72', '6', '4');
INSERT INTO `sys_role_menu` VALUES ('73', '6', '17');
INSERT INTO `sys_role_menu` VALUES ('74', '6', '18');
INSERT INTO `sys_role_menu` VALUES ('75', '6', '19');
INSERT INTO `sys_role_menu` VALUES ('76', '6', '5');
INSERT INTO `sys_role_menu` VALUES ('77', '6', '6');
INSERT INTO `sys_role_menu` VALUES ('96', '3', '1');
INSERT INTO `sys_role_menu` VALUES ('97', '3', '2');
INSERT INTO `sys_role_menu` VALUES ('98', '3', '3');
INSERT INTO `sys_role_menu` VALUES ('99', '3', '4');
INSERT INTO `sys_role_menu` VALUES ('100', '3', '5');
INSERT INTO `sys_role_menu` VALUES ('101', '3', '6');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(64) DEFAULT NULL,
`password` varchar(64) DEFAULT NULL,
`avatar` varchar(255) DEFAULT NULL,
`email` varchar(64) DEFAULT NULL,
`city` varchar(64) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
`last_login` datetime DEFAULT NULL,
`statu` int(5) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '$2a$10$R7zegeWzOXPw871CmNuJ6upC0v8D373GuLuTw8jn6NET4BkPRZfgK', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '123@qq.com', '广州', '2021-01-12 22:13:53', '2021-01-16 16:57:32', '2020-12-30 08:38:37', '1');
INSERT INTO `sys_user` VALUES ('2', 'test', '$2a$10$0ilP4ZD1kLugYwLCs4pmb.ZT9cFqzOZTNaMiHxrBnVIQUGUwEvBIO', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', 'test@qq.com', null, '2021-01-30 08:20:22', '2021-01-30 08:55:57', null, '1');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('4', '1', '6');
INSERT INTO `sys_user_role` VALUES ('7', '1', '3');
INSERT INTO `sys_user_role` VALUES ('13', '2', '3');
代码生成以及测试
package com.example.springbootvue;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class 生成代码 {
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("fjj");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName(scanner("模块名"));
pc.setParent("com.example.demo");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
// 写于父类中的公共字段
strategy.setInclude(scanner("user").split(","));
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
运行
生成所有的代码
测试生成代码
写测试的controller
测试
对结果集进行封装
因为是前后端分离的项目,所以有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了
创建Result 类,用于异步统一放回的结果封装
必要要素
- 是否成功,用code 表示 (200为成功,400 为异常)
- 结果消息
- 结果数据
package com.example.Common;
import lombok.Data;
import java.io.Serializable;
@Data
// 实现序列化的接口
public class Result implements Serializable {
// 状态码
private int code;
// 消息
private String msg;
// 结果 data
private Object data;
public static Result succ(Object data) {
return succ(200, "操作成功", data);
}
// 成功的 结果
public static Result succ(int code, String msg, Object data) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
public static Result fail(String msg) {
return fail(400, msg, null);
}
public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}
public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}
}
另外出了在结果封装类上的code可以提现数据是否正常,我们还可以通过http的状态码来提现访问是否遇到了异常,比如401表示无权限拒绝访问等,注意灵活使用。
全局异常处理
有时候不可避免服务器报错的情况,如果不配置异常处理的机制,就会默认返回tomcat 或者 nginx 的5xx 页面,对普通用户来说,不太友好,我们应该返回一个友好简单的格式 给前端
思路如下
通过使用 @ControllerAdvice 来进行统一异常处理
@ExceptionHandler(value = RuntimeException.class) 来指定捕获的exception各个类型异常,这个异常的处理是全局的,所有类似的异常,都会跑到这个地方处理
定义全局异常处理 @ControllerAdvice 表示定义全局控制器异常处理
@ExceptionHandler 表示针对性异常处理,可对每种异常针对性处理
package com.example.Excepts;
import com.example.Common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 加入注解
@Slf4j
@RestControllerAdvice
public class GlobalExcepts {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
// Accert 异常 ----------------------{}
public Result handelr (IllegalArgumentException e) {
log.error("Accert 异常 ----------------------{}",e.getMessage());
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = RuntimeException.class)
public Result handelr (RuntimeException e) {
log.error("运行时异常------------------{}",e.getMessage());
return Result.fail(e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handelr (MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
log.error("实体校验异常------------------{}",e.getMessage());
return Result.fail(objectError.getDefaultMessage());
}
}
IllegalArgumentException:处理Assert的异常
MethodArgumentNotValidException:处理实体校验的异常
RuntimeException:捕捉其他异常
整合 Spring Security
Security工作原理分析
说明
- 客户端发起一个请求,进入Security 过滤器链
- 当到LogoutFilter 的时候判断是否是登出的路径,如果是登出路径则到logoutHandler ,如果登出成功则到logoutSuccessHandler 登出成功处理,如果不是登出路径则直接进入下一个过滤器
- 当到UsernamePasswordSuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器登录操作,如果失败则到SuthenticationFailureHandler,登录失败处理器处理,如果登录成功则到AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器
- 进入认证BasicAuthenticationFilter 进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder 中的 securityContext 的属性authention 上面。如果认证失败就会 认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
总共需要出现的组件
LogoutFilter - 登出过滤器
logoutSuccessHandler - 登出成功之后的操作类
UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
AuthenticationFailureHandler - 登录失败操作类
AuthenticationSuccessHandler - 登录成功操作类
BasicAuthenticationFilter - Basic身份认证过滤器
SecurityContextHolder - 安全上下文静态工具类
AuthenticationEntryPoint - 认证失败入口
ExceptionTranslationFilter - 异常处理过滤器
AccessDeniedHandler - 权限不足操作类
FilterSecurityInterceptor - 权限判断拦截器、出口
引入Security 与 Jwt
首先我们导入security 包,因为我们前后端交互用户认证凭证用的是Jwt 所以我们也导入jwt 的相关包。导入redis 用作验证码
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
启动redis,然后我们再启动项目,这时候我们再去访问http://localhost:8081/test,会发现系统会先判断到你未登录跳转到http://localhost:8081/login,因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:
spring:
# Spring security 的账号密码修改
security:
user:
name: user
password: 111111
用户认证,验证码生成
分为首次登陆和二次认证
- 首次登陆认证:用户名,密码和验证码完成登陆
- 二次token认证:请求头携带jwt 进行身份认证
使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter就不能使用了。
解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
生成验证码
首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:
KaptchaConfig
// 加上注解
@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;
}
}
上面验证码的长宽字体颜色等,是可以调整的
通过控制器提供生成验证码的方法
**这里需要用到一个redis 的工具类 **
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
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;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
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));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
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) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(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表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}
public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}
/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
}
一个常量类
生成验证码Controller
@RestController
public class AuthController extends BaseController {
// 生成验证码的 控制器
// 注入 我们的图片验证码
@Autowired
private Producer producer;
// 注入redis 的工具类
@Autowired
RedisUtil redisUtil;
@GetMapping("/captcha")
public Result captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取状态码
String code = producer.createText();
// 获取 key 的值 设置为 uuid
String key = UUID.randomUUID().toString();
// 生成图片
BufferedImage image = producer.createImage(code);
// 获取字节数组 输出流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
// 64 位转码
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
// 存到redis 中 并且设置过期时间
redisUtil.hset(Const.CAPTCHA_KEY,key,code,120);
return Result.succ(
MapUtil.builder()
.put("token",key)
.put("base64Img",base64Img)
.build()
);
}
}
因为前后端分离,禁用了session,所以我们把验证码放在了redis 中,使用一个随机字符串作为key 传到前端,前端再把随机字符串和用户输入的验证码提交上来,这样就可以通过随机字符串获取保存的验证码和用户的验证码比较是否正确
因为是图片验证码方式,所以进行了encode ,把图片进行了base64 编码这样前端就可以显示图片了
前端只需要把mock.js 去掉就可以了
验证码认证过滤器
图片验证码静进行认证验证码是否正确
登录失败的 LoginFailureHandler
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 设置头信息 的编码规范
httpServletRequest.setCharacterEncoding("application/json;charset=UTF-8");
// 设置输出流
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
// 设置错误的异常输出结果
Result fail = Result.fail("用户名或者密码错误");
// 写入的 格式
outputStream.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
// 刷新关闭流
outputStream.flush();
outputStream.close();
}
}
自定义Captcha 异常类
CaptchaFilter
/**
* 图片验证码校验过滤器,在登录过滤器前
*/
@Slf4j
@Component
public class CaptchaFilter extends OncePerRequestFilter {
// 登录的url
private final String loginUrl = "/login";
// 注入 redis
@Autowired
RedisUtil redisUtil;
// 失败的登录的
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取 url
String url = httpServletRequest.getRequestURI();
if (loginUrl.equals(url) && httpServletRequest.getMethod().equals("post")) {
log.info("获取到login链接,正在校验验证码 -- " + url);
try {
validate(httpServletRequest);
} catch (CaptchaException e) {
log.info(e.getMessage());
// 交给登录失败处理器处理
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest request) {
// 获取 code 值
String code = request.getParameter("code");
// 获取 token
String token = request.getParameter("token");
// 判断是不是空
if (StringUtils.isBlank(code) || StringUtils.isBlank(token)) {
throw new CaptchaException("验证码不能为空");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, token))) {
throw new CaptchaException("验证码不正确");
}
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, token);
}
}
配置SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
CaptchaFilter captchaFilter;
public static final String[] URL_WHITELIST = {"/webjars/**", "/favicon.ico", "/captcha", "/login", "/logout",};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.formLogin()
.failureHandler(loginFailureHandler)
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
//白名单
.anyRequest().authenticated()
// 不会创建session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 登录验证码校验过滤器 ;
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) ;
}}
首先formLogin我们定义了表单登录提交的方式以及定义了登录失败的处理器,后面我们还要定义登录成功的处理器的。然后authorizeRequests我们除了白名单的链接之外其他请求都会被拦截。再然后就是禁用session,最后是设定验证码过滤器在登录过滤器之前。然后我们打开前端的/login,发现出现了问题,后面我处理,我们先用postman调试接口 查看 验证码能不能出来。
就算我们登录成功,security默认跳转到/链接,但是又会因为没有权限访问/,所有又会教你去登录,所以我们必须取消原先默认的登录成功之后的操作,根据我们之前分析的流程,登录成功之后会走AuthenticationSuccessHandler,因此在登录之前,我们先去自定义这个登录成功操作类:
Jwt 的工具类
package com.example.Util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
// 提供 get ,set
@Data
// 加入Spring 注解
@Component
// 前缀
@ConfigurationProperties(prefix = "fjj.jwt")
public class JwtUtils {
// 设置时长
private long expire;
private String secret;
private String header;
// 生产 jwt
public String generateToken(String username) {
Date date = new Date();
// 过期时间
Date expiredate = new Date(date.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ","JWT")
.setSubject(username)
.setIssuedAt(date)
.setExpiration(expiredate) // 设置7天过期时间
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
// 解析 JWT
public Claims getClaimsBytoken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
// jwt 是否过期
public boolean isToken(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
LoginSuccessHandler 登录成功之后我们利用用户名生成jwt
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
// 注入 Jwt 工具类
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成jwt返回
String jwt = jwtUtils.generateToken(authentication.getName());
response.setHeader(jwtUtils.getHeader(), jwt);
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
然后我们再security配置中添加上登录成功之后的操作类: SecurityConfig
Postman 测试
请求头中有Authorization
搞定,登录成功啦,验证码也正常验证了。
身份认证-1
登录成功之后前端就可以获取到了Jwt 的信息,前端中我们保存在了store 中,同时也保存在了localStorage 中,然后每次axios 请求之前,我们都会添加上我们的头信息。回顾
所以后端进行用户识别的时候,我们需要通过请求头中获取jwt ,然后解析出我们的用户名,这样我们就可以知道是谁在访问接口。判断是否有权限。
自定义过滤器用来识别Jwt
JWTAuthenticationFilter
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
// 注入 jwt 工具类
@Autowired
JwtUtils jwtUtils;
// 注入redis 的工具类
@Autowired
RedisUtil redisUtil;
// 注入 用户的服务类
@Autowired
ISysUserService sysUserService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("jwt 检验 filter");
// 获取到 jwt
String jwt = request.getHeader(jwtUtils.getHeader());
// 判断是不是空
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
// 判断 token 有没有异常
Claims claim = jwtUtils.getClaimsBytoken(jwt);
if (claim == null) {
throw new JwtException("token异常!");
}
// 判断是否过期
if (jwtUtils.isToken(claim)) {
throw new JwtException("token 过期");
}
// 获取当前登录的用户
String username = claim.getSubject();
log.info("用户-{},正在登陆!", username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, new TreeSet<>());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 继续向下执行
chain.doFilter(request, response);
}
}
主要逻辑,获取到用户名之后直接封装成UsernamePasswordAuthenticationToken,之后交给 SecurityContextHolder 参数传递 authentication 对象,这样后续security 就能获取到当前登录的用户信息了,也就完成了用户认证 。当认证失败就会进入 AuthrntionEntryPoint 于是我们自定义认证失败返回的数据
JwtAuthenticationEntryPoint
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.info("认证失败,没有登陆");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail("请先登录!");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
不管是什么原因,认证失败,就要重新登录,所以返回的信息直接明了
添加到SecurityConfig
// 加入注解
@Configuration
// 加入 Security 的注解
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 LoginFailureHandler
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private CaptchaFilter captchaFilter;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager());
return filter;
}
// 注入 密码
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 设置白名单
private static final String[] URL_WHITELIST = {
"/login",
"/logout",
"/captcha",
"/favicon.ico"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
// 退出的
// 禁用 session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
}
}
身份认证 - 2
之前我们的用户名密码配置在配置文件中的,而且密码也用的是明文,这明显不符合我们的要求,我们的用户必须是存储在数据库中,密码也是得经过加密的。所以我们先来解决这个问题,然后再去弄授权
插入一条用户数据,但这里有个问题,就是我们的密码怎么生成?密码怎么来的?这里我们使用Security内置了的BCryptPasswordEncoder,里面就有生成和匹配密码是否正确的方法,也就是加密和验证策略。因此我们再SecurityConfig中进行配置:
这样系统就会使用我们找个新的密码策略进行匹配密码是否正常了。之前我们配置文件配置的用户名密码去掉:
ok,我们先使用BCryptPasswordEncoder给我们生成一个密码,给数据库添加一条数据先,我们再TestController中注入BCryptPasswordEncoder,然后使用encode进行密码加密,对了,记得在SecurityConfig中吧/test/**添加白名单哈,不然访问会提示你登录!!
TestController
@RestController
public class TestController {
// 注入
@Autowired
ISysUserService sysUserService;
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/test")
public Object test() {
return sysUserService.list();
}
// 生成测试密码
@GetMapping("/test/pass")
public Result passEncode() {
// 密码加密
String encode = bCryptPasswordEncoder.encode("123456");
// 密码验证
boolean matches = bCryptPasswordEncoder.matches("123456", encode);
return Result.succ(MapUtil.builder().put("pass", encode).put("marches", matches).build());
}
}
postman 测试
但是先我们登录过程系统不是从我们数据库中获取数据的,因此,我们需要重新定义这个查用户数据的过程,我们需要重写UserDetailsService接口
UserDetailsServiceImpl
@Service
public class UserDetailsServiceimpl implements UserDetailsService {
// 注入 服务层
@Autowired
SysUserServiceImpl SysUserServiceImpl;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用查询名字的方法
SysUser sysUser = SysUserServiceImpl.getByUsername(username);
// 判断名字是不是空的
if (sysUser == null) {
throw new UsernameNotFoundException("用户名或者密码错误");
}
return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
public List<GrantedAuthority> getUserAuthority(Long userId) {
// 角色菜单操作的权限l
// 角色 (ROLE_admin) 菜单操作权限 sys:user:list
String authority = SysUserServiceImpl.getUserAuthorityInfo(userId);
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
因为security在认证用户身份的时候会调用UserDetailsService.loadUserByUsername()方法,因此我们重写了之后security就可以根据我们的流程去查库获取用户了。然后我们把UserDetailsServiceImpl配置到SecurityConfig中:
// 加入注解
@Configuration
// 加入 Security 的注解
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入 LoginFailureHandler
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private CaptchaFilter captchaFilter;
@Autowired
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// 注入 UserDetailsServiceimpl 实现类
@Autowired
UserDetailsServiceimpl userDetailsServiceimpl;
// 注入 JwtLogoutSuccessHandler
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
// 注入 Security jwt 的 过滤器
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// 注入 密码
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 设置白名单
private static final String[] URL_WHITELIST = {
"/login",
"/logout",
"/captcha",
"/favicon.ico"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
// 退出的
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用 session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceimpl);
}
}
然后上面UserDetailsService.loadUserByUsername()默认返回的UserDetails,我们自定义了AccountUser去重写了UserDetails,这也是为了后面我们可能会调整用户的一些数据等。
public class AccountUser implements UserDetails {
private Long userId;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
数据基本没变,我就添加多了一个用户的id而已
4、登录成功,并在请求头中获取到了Authorization,也就是JWT。完美!!
解决授权
然后关于权限部分,也是security的重要功能,当用户认证成功之后,我们就知道谁在访问系统接口,这是又有一个问题,就是这个用户有没有权限来访问我们这个接口呢,要解决这个问题,我们需要知道用户有哪些权限,哪些角色,这样security才能我们做权限判断。
之前我们已经定义及几张表,用户、角色、菜单、以及一些关联表,一般当权限粒度比较细的时候,我们都通过判断用户有没有此菜单或操作的权限,而不是通过角色判断,而用户和菜单是不直接做关联的,是通过用户拥有哪些角色,然后角色拥有哪些菜单权限这样来获得的。
问题1:我们是在哪里赋予用户权限的?有两个地方:
1、用户登录,调用调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。
2、接口调用进行身份认证过滤器时候JWTAuthenticationFilter,需要返回用户权限信息
问题2:在哪里决定什么接口需要什么权限?
Security内置的权限注解:
@PreAuthorize:方法执行前进行权限检查
@PostAuthorize:方法执行后进行权限检查
@Secured:类似于 @PreAuthorize
可以在Controller的方法前添加这些注解表示接口需要什么权限。
比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('sys:user:save')")
授权、验证权限的流程:
1,用户登录或者调用接口时候识别到用户,并获取到用户的权限信息
2,注解标识Controller中的方法需要的权限或者角色
3,Security通过FilterSecurityInterceptor匹配URI和权限是否匹配
4,有权限则可以访问接口,当无权限的时候返回异常交给AccessDeniedHandler操作类处理。
UserDetailsServiceImpl
.JWTAuthenticationFilter
SysUserServiceImpl
/**
* <p>
* 服务实现类
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
// 注入角色表的 service
@Autowired
SysRoleServiceImpl SysRoleServiceImpl;
// 注入 SysMenuServiceImpl
@Autowired
SysMenuServiceImpl sysMenuService;
@Autowired
// 注入 User 表的 Mapper
SysUserMapper sysUserMapper;
// 由于 每次查询都要 查询一次数据库,所以 加入 redis 用来做缓存
@Autowired
RedisUtil redisUtil;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username", username));
}
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = sysUserMapper.selectById(userId);
// 角色
// ROLE_admin,ROLE_normal,sys:user:list
String authority = "";
if (redisUtil.hasKey("GrantedAuthority:"+sysUser.getUsername())) {
authority = (String) redisUtil.get("GrantedAuthority:"+sysUser.getUsername());
} else {
// 获取角色
List<SysRole> roles = SysRoleServiceImpl.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
System.out.println(roles);
if (roles.size() > 0) {
String roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority = roleCodes.concat(",");
;
}
// 获取菜单的操作
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0) {
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
redisUtil.set("GrantedAuthority:" + sysUser.getUsername(), authority, 60 * 60);
}
return authority;
}
@Override
public void clearUserAuthorityInfo(String username) {
// 如果名字发生改变删除缓存
redisUtil.del("GrantedAuthority:" + username);
}
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {
// 如果角色的Id 发生 改变
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>()
.inSql("id", "select user_id from sys_user_role where role_id = " + roleId)
);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
}
可以看到,我通过用户id分别获取到用户的角色信息和菜单信息,然后通过逗号链接起来,因为角色信息我们需要这样“ROLE_”+角色,所以才有了上面的写法:比如用户拥有Admin角色和添加用户权限,则最后的字符串是:ROLE_admin,sys:user:save。同时为了避免多次查库,做了一层缓存。
然后sysUserMapper.getNavMenuIds(userId)因为要查询数据库,具体SQL如下:
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT
DISTINCT rm.menu_id
FROM
sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
WHERE ur.user_id = #{userId}
</select>
上面表示通过用户ID获取用户关联的菜单的id,因此需要用到两个中间表的关联了。ok,这样我们就赋予了用户角色和操作权限了。后面我们只需要在Controller添加上具体注解表示需要的权限,Security就会自动帮我们自动完成权限校验了。
权限缓存
因为上面我在获取用户权限那里添加了个缓存,这时候问题来了,就是权限缓存的实时更新问题,比如当后台更新某个管理员的权限角色信息的时候如果权限缓存信息没有实时更新,就会出现操作无效的问题,那么我们现在点定义几个方法,用于清除某个用户或角色或者某个菜单的权限的方法:
SysUserServiceImpl
/**
* <p>
* 服务实现类
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
// 注入角色表的 service
@Autowired
SysRoleServiceImpl SysRoleServiceImpl;
// 注入 SysMenuServiceImpl
@Autowired
SysMenuServiceImpl sysMenuService;
@Autowired
// 注入 User 表的 Mapper
SysUserMapper sysUserMapper;
// 由于 每次查询都要 查询一次数据库,所以 加入 redis 用来做缓存
@Autowired
RedisUtil redisUtil;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username", username));
}
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = sysUserMapper.selectById(userId);
// 角色
// ROLE_admin,ROLE_normal,sys:user:list
String authority = "";
if (redisUtil.hasKey("GrantedAuthority:"+sysUser.getUsername())) {
authority = (String) redisUtil.get("GrantedAuthority:"+sysUser.getUsername());
} else {
// 获取角色
List<SysRole> roles = SysRoleServiceImpl.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
System.out.println(roles);
if (roles.size() > 0) {
String roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority = roleCodes.concat(",");
;
}
// 获取菜单的操作
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0) {
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
redisUtil.set("GrantedAuthority:" + sysUser.getUsername(), authority, 60 * 60);
}
return authority;
}
@Override
public void clearUserAuthorityInfo(String username) {
// 如果名字发生改变删除缓存
redisUtil.del("GrantedAuthority:" + username);
}
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {
// 如果角色的Id 发生 改变
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>()
.inSql("id", "select user_id from sys_user_role where role_id = " + roleId)
);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
}
上面最后一个方法查到了与菜单关联的所有用户的,具体sql如下:
<select id="listByMenuId" resultType="com.example.demo.entity.SysUser">
SELECT DISTINCT
su.*
FROM
sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN sys_user su ON ur.user_id = su.id
WHERE
rm.menu_id = #{menuId}
</select>
有了这几个方法之后,在哪里调用?这就简单了,在更新、删除角色权限、更新、删除菜单的时候调用,虽然我们现在还没写到这几个方法,后续我们再写增删改查的时候记得加上就行啦。
退出数据返回
jwt -username
token - 随机码 - redis
JwtLogoutSuccessHandler
// 加入Spring 的注解
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
// 注入 jwt 的工具类
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest Request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 判断是否为空 不为空的话 退出
if (authentication != null) {
new SecurityContextLogoutHandler().logout(Request,response,authentication);
}
// 响应的格式
response.setContentType("application/json;charset=UTF-8");
// 输出流
ServletOutputStream outputStream = response.getOutputStream();
// 头信息
response.setHeader(jwtUtils.getHeader(), "");
// 返回的 结果
Results result = Results.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
无权限数据返回
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 设置头信息 的编码规范
httpServletResponse.setContentType("application/json;charset=UTF-8");
// 给一个权限不足的状态吗
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 设置输出流
ServletOutputStream outputStream = httpServletResponse.getOutputStream();
// 设置错误的异常输出结果
Results fail = Results.fail(e.getMessage());
// 写入的 格式
outputStream.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
// 刷新喝关闭流
outputStream.flush();
outputStream.close();
}
}
SpringSecurity就已经完美整合到了我们的项目中来了。
解决跨域问题
上面的调试我们都是使用的postman,如果我们和前端进行对接的时候,会出现跨域的问题
CorsConfig
@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 source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
菜单接口开发
开发菜单的接口,因为这3个表:用户表、角色表、菜单表,才有菜单表是不需要通过其他表来获取信息的。比如用户需要关联角色,角色需要关联菜单,而菜单不需要主动关联其他表。
获取菜单导航和权限的链接是/sys/menu/nav,然后我们的菜单导航的json数据应该是这样的:
{ title:
'角色管理',
icon: 'el-icon-rank',
path: '/sys/roles',
name: 'SysRoles',
component: 'sys/Role',
children: []}
然后返回的权限数据应该是个数组:
["sys:menu:list","sys:menu:save","sys:user:list"...]
注意导航菜单那里有个children,也就是子菜单,是个树形结构,因为我们的菜单可能这样:
系统管理 - 菜单管理 - 添加菜单
这就已经有3级了菜单了。注意这个关系的关联。我们的SysMenu实体类中有个parentId,但是没有children,因此我们可以在SysMenu中添加一个children,当然了其实不添加也可以,因为我们也需要一个dto,这样我们才能按照上面json数据格式返回。
添加一个children吧:
SysMenu
@Data
@EqualsAndHashCode(callSuper = true)
public class SysMenu extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 父菜单ID,一级菜单为0
*/
@NotNull(message = "上级菜单不能为空")
private Long parentId;
@NotBlank(message = "菜单名称不能为空")
private String name;
/**
* 菜单URL
*/
private String path;
/**
* 授权(多个用逗号分隔,如:user:list,user:create)
*/
@NotBlank(message = "菜单授权码不能为空")
private String perms;
private String component;
/**
* 类型 0:目录 1:菜单 2:按钮
*/
@NotNull(message = "菜单类型不能为空")
private Integer type;
/**
* 菜单图标
*/
private String icon;
/**
* 排序
*/
@TableField("orderNum")
private Integer orderNum;
@TableField(exist = false)
private List<SysMenu> children = new ArrayList<>();
}
SysMenuDto吧,知道要返回什么样的数据,我们就只需要去填充数据就好了
package com.example.demo.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class SysMenuDto implements Serializable {
private Long id;
private String name;
private String title;
private String icon;
private String path;
private String component;
private List<SysMenuDto> children = new ArrayList<>();
}
SysMenuController
package com.example.demo.controller;
import cn.hutool.core.map.MapUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.Result.Results;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/menu")
public class SysMenuController extends BaseController {
// 获取 菜单的 链接
@GetMapping("/nav")
// 获取 nav 的导航
public Results nav(Principal principal) {
// 获取当前登录的用户
SysUser byUsername = sysUserService.getByUsername(principal.getName());
// 获取权限信息 是 用逗号隔开的
String userAuthorityInfo = sysUserService.getUserAuthorityInfo(byUsername.getId());
// 转换成数组
String[] strings = StringUtils.tokenizeToStringArray(userAuthorityInfo, ",");
// 获取导航的信息
List<SysMenuDto> navs = sysMenuService.getCurrentUserNav();
// 返回结果
return Results.succ(MapUtil.builder()
.put("authoritys", strings)
.put("nav", navs)
.build()
);
}
// 获取用户的信息
@GetMapping("/userInfo")
public Results userInfo(Principal principal) {
SysUser sysUser = sysUserService.getByUsername(principal.getName());
return Results.succ(MapUtil.builder()
.put("id", sysUser.getId())
.put("username", sysUser.getUsername())
.put("avatar", sysUser.getAvatar())
.put("created", sysUser.getCreated())
.map()
);
}
@GetMapping("/info/{id}")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Results info (@PathVariable(name = "id") Long id) {
return Results.succ(sysMenuService.getById(id));
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Results list () {
List<SysMenu> menus = sysMenuService.tree ();
return Results.succ(menus);
}
//保存
@PostMapping("/save")
@PreAuthorize("hasAuthority('sys:menu:save')")
public Results save (@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setCreated(LocalDateTime.now());
sysMenuService.save(sysMenu);
return Results.succ(sysMenu);
}
// 更新
@PostMapping("/update")
@PreAuthorize("hasAuthority('sys:menu:update')")
public Results update (@Validated @RequestBody SysMenu sysMenu) {
sysMenu.setUpdated(LocalDateTime.now());
sysMenuService.updateById(sysMenu);
// 因为是更新的操作,所以需要清楚缓存
sysUserService.clearUserAuthorityInfoByMenuId(sysMenu.getId());
return Results.succ(sysMenu);
}
// 删除
@PostMapping("/delete/{id}")
@PreAuthorize("hasAuthority('sys:menu:delete')")
public Results delete (@PathVariable ("id") Long id) {
// 判断 子节点还存在不,不存在才可以删除
int parent_id = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id));
if (parent_id > 0) {
return Results.fail("请先删除子菜单");
}
// 清楚所有的缓存
sysUserService.clearUserAuthorityInfoByMenuId(id);
sysMenuService.removeById(id);
//删除关联表的数据
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("menu_id",id));
return Results.succ("success");
}
}
方法中Principal principal表示注入当前用户的信息,getName就可以获取当当前用户的用户名了。sysUserService.getUserAuthorityInfo方法我们之前已经说过了,就在我们登录完成或者身份认证时候需要返回用户权限时候编写的。然后通过StringUtils.tokenizeToStringArray把字符串通过逗号分开组成数组形式。
重点在与sysMenuService.getcurrentUserNav,获取当前用户的菜单导航,
SysMenuServiceImpl
package com.example.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dto.SysMenuDto;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.mapper.SysMenuMapper;
import com.example.demo.mapper.SysUserMapper;
import com.example.demo.service.ISysMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
@Autowired
SysUserServiceImpl sysUserService;
@Autowired
SysUserMapper sysUserMapper;
@Override
public List<SysMenuDto> getCurrentUserNav() {
// 获取 登录的用户
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 通过用户名获取
SysUser sysUser = sysUserService.getByUsername(username);
// 通过 用户Id 获取 菜单Id
List<Long> navMenuIds = sysUserMapper.getNavMenuIds(sysUser.getId());
// 通过菜单 id 获取 菜单
List<SysMenu> sysMenus = this.listByIds(navMenuIds);
// 转换成树桩结构
List<SysMenu> menustree = buildTreeMeun(sysMenus);
// 实体类转换
return convert(menustree);
}
@Override
public List<SysMenu> tree() {
// 获取所有菜单信息
List<SysMenu> sysMenus = this.list(new QueryWrapper<SysMenu>().orderByAsc("orderNum"));
// 转成 树桩结构
List<SysMenu> menus = buildTreeMeun(sysMenus);
return menus;
}
private List<SysMenuDto> convert(List<SysMenu> menustree) {
ArrayList<SysMenuDto> menuDtos = new ArrayList<>();
menustree.forEach(m -> {
SysMenuDto dto = new SysMenuDto();
dto.setId(m.getId());
dto.setName(m.getPerms());
dto.setTitle(m.getName());
dto.setComponent(m.getComponent());
dto.setPath(m.getPath());
// 如果 长度大于就给 字节点复制
if (m.getChildren().size() > 0) {
// 子节点调用当前的方法 进行复制。
dto.setChildren(convert(m.getChildren()));
}
menuDtos.add(dto);
});
return menuDtos;
}
// 转换树桩结构
private List<SysMenu> buildTreeMeun(List<SysMenu> sysMenus) {
// 准备返回的list
ArrayList<SysMenu> finalMenus = new ArrayList<>();
// 找到各自的子节点
for (SysMenu sysMenu : sysMenus) {
for (SysMenu sysMenu1 : sysMenus) {
// 如果 id 相同说明就是自己的孩子了
if (sysMenu.getId() == sysMenu1.getParentId()) {
sysMenu.getChildren().add(sysMenu1);
}
}
// 提取出来父节点
if (sysMenu.getParentId() == 0L) {
finalMenus.add(sysMenu);
}
}
return finalMenus;
}
}
接口中sysUserMapper.getNavMenuIds我们之前就已经写过的了,通过用户id获取菜单的id,然后后面就是转成树形结构,buildTreeMenu方法的思想很简单,我们现实把菜单循环,让所有菜单先找到各自的子节点,然后我们在把最顶级的菜单获取出来,这样顶级下面有二级,二级也有自己的三级。最后就是convert把menu转成menuDto。这个比较简单,就不说了。好了,导航菜单已经开发完毕,我们来写菜单管理的增删改查,因为菜单列表也是个树形接口,这次我们就不是获取当前用户的菜单列表的,而是所有菜单然后组成树形结构,一样的思想,数据不一样而已。
删除、更新菜单的时候记得调用根据菜单id清楚用户权限缓存信息的方法哈。然后每个方法前都会带有权限注解:@PreAuthorize(“hasAuthority(‘sys:menu:delete’)”),这就要求用户有特定的操作权限才能调用这个接口,sys:menu:delete这些数据不是乱写出来的,我们必须和数据库的数据保持一致才行,然后component字段,也是要和前端进行沟通,因为这个是链接到的前端的组件页面。有了增删改查,我们就去先添加我们的所有的菜单权限数据先。效果如下:
角色接口开发
角色的增删改查其实也简单,而且字段这么少
SysRoleController
package com.example.demo.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.entity.SysMenu;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/role")
public class SysRoleController extends BaseController {
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/info/{id}")
public Results info(@PathVariable("id") Long id) {
// 获取到实体类
SysRole byId = sysRoleService.getById(id);
//获取关联表的 meunid
List<SysRoleMenu> role_id = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", byId));
// 通过 流 来获取 出meunid
List<Long> collect = role_id.stream().map(p -> p.getMenuId()).collect(Collectors.toList());
byId.setMenuIds(collect);
return Results.succ(byId);
}
@PreAuthorize("hasAuthority('sys:role:list')")
@GetMapping("/list")
public Results list(String name) {
// 分页查询 ,判断有没有名字
Page page = sysRoleService.page(getPage(),
new QueryWrapper<SysRole>().like(StrUtil.isNotBlank(name), "name", name));
return Results.succ(page);
}
@PreAuthorize("hasAuthority('sys:role:save')")
@PostMapping("/save")
public Results save(@Validated @RequestBody SysRole sysRole) {
// 新增 的方法
// 获取当前的 更改的时间
sysRole.setCreated(LocalDateTime.now());
// 保存的状态
sysRole.setStatu(Const.STATUS_ON);
sysRoleService.save(sysRole);
return Results.succ(sysRole);
}
@PreAuthorize("hasAuthority('sys:role:update')")
// 更新的
@PostMapping("/update")
public Results update(@Validated @RequestBody SysRole sysRole) {
// 设置更新的时间
sysRole.setCreated(LocalDateTime.now());
//调用更新的方法
sysRoleService.updateById(sysRole);
// 删除 缓存
sysUserService.clearUserAuthorityInfoByRoleId(sysRole.getId());
return Results.succ(sysRole);
}
// 批量删除
@PreAuthorize("hasAuthority('sys:role:delete')")
@PostMapping("/delete")
//加上事务避免出现 删除失败的情况
@Transactional
public Results delete(@RequestBody Long[] RoleIds) {
// 调用方法
sysRoleService.removeByIds(Arrays.asList(RoleIds));
// 删除中间表
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().in("role_id",RoleIds));
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id",RoleIds));
// 删除缓存
Arrays.stream(RoleIds).forEach(f -> {
sysUserService.clearUserAuthorityInfoByRoleId(f);
});
return Results.succ("success");
}
@PreAuthorize("hasAuthority('sys:role:perm')")
@PostMapping("/perm/{roleId}")
@Transactional
public Results info(@PathVariable("roleId") Long roleId,@RequestBody Long[] menuId) {
// 用来存放的集合
List<SysRoleMenu> sysRoleMenus = new ArrayList<>();
Arrays.stream(menuId).forEach(menuid -> {
SysRoleMenu roleMenu = new SysRoleMenu();
roleMenu.setMenuId(menuid);
roleMenu.setRoleId(roleId);
sysRoleMenus.add(roleMenu);
});
//删除记录
sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id",roleId));
//保存新的
sysRoleMenuService.saveBatch(sysRoleMenus);
// 删除缓存
sysUserService.clearUserAuthorityInfoByRoleId(roleId);
return Results.succ(menuId);
}
}
上面方法中:info方法获取角色信息的方法,因为我们不仅仅在编辑角色时候会用到这个方法,在回显角色关联菜单的时候也需要被调用,因此我们需要把角色关联的所有的菜单的id也一并查询出来,也就是分配权限的操作。对应到前端就是这样的,点击分配权限,会弹出出所有的菜单列表,然后根据角色已经关联的菜单的id回显勾选上已经关联过的。
用户接口开发
用户管理里面有个用户关联角色的分配角色操作,和角色关联菜单的写法差不多的
package com.example.demo.controller;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.Result.Results;
import com.example.demo.dto.PassDto;
import com.example.demo.entity.SysRole;
import com.example.demo.entity.SysRoleMenu;
import com.example.demo.entity.SysUser;
import com.example.demo.entity.SysUserRole;
import com.example.demo.utils.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author fjj
* @since 2021-07-05
*/
@RestController
@RequestMapping("/sys/user")
public class SysUserController extends BaseController {
// 注入加密的
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@GetMapping("/info/{id}")
@PreAuthorize("hasAuthority('sys:user:list')")
public Results info(@PathVariable("id") Long id) {
SysUser sysUser = sysUserService.getById(id);
// 断言判断是不是为空
Assert.notNull(sysUser, "找不到该管理员");
List<SysRole> sysRoles = sysRoleService.listRolesByUserId(id);
sysUser.setSysRoles(sysRoles);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:list')")
@GetMapping("/list")
public Results list(String username) {
Page<SysUser> page = sysUserService.page(getPage(), new QueryWrapper<SysUser>().like(StringUtils.isNotBlank(username), "username", username));
page.getRecords().forEach(p -> {
p.setSysRoles(sysRoleService.listRolesByUserId(p.getId()));
});
return Results.succ(page);
}
@PreAuthorize("hasAuthority('sys:user:save')")
@PostMapping("/save")
public Results save(@Validated @RequestBody SysUser sysUser) {
// 设置 更新时间
sysUser.setCreated(LocalDateTime.now());
// 默认的状态
sysUser.setStatu(Const.STATUS_ON);
// 设置默认的加密的密码
String password = bCryptPasswordEncoder.encode(Const.PASS_WORD);
sysUser.setPassword(password);
// 设置默认的头像
sysUser.setAvatar(Const.Avatar);
sysUserService.save(sysUser);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:update')")
@PostMapping("/update")
public Results update(@Validated @RequestBody SysUser sysUser) {
// 设置更新的时间
// sysUser.setCreated(LocalDateTime.now());
sysUser.setUpdated(LocalDateTime.now());
sysUserService.updateById(sysUser);
return Results.succ(sysUser);
}
@PreAuthorize("hasAuthority('sys:user:delete')")
@PostMapping("/delete")
@Transactional
public Results delete(@RequestBody Long [] ids) {
sysUserService.removeByIds(Arrays.asList(ids));
// 删除中间的关系表
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id",ids));
return Results.succ("");
}
@PreAuthorize("hasAuthority('sys:user:role')")
@PostMapping("/role/{userId}")
@Transactional
public Results rolePerm(@PathVariable Long userId,@RequestBody Long [] roleIds) {
ArrayList<SysUserRole> UserRoleList = new ArrayList<>();
Arrays.stream(roleIds).forEach(r ->{
SysUserRole userRole = new SysUserRole();
userRole.setRoleId(r);
userRole.setUserId(userId);
UserRoleList.add(userRole);
});
// 删除关联表的数据
sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id",userId));
sysUserRoleService.saveBatch(UserRoleList);
// 删除缓存
SysUser sysUser = sysUserService.getById(userId);
sysUserService.clearUserAuthorityInfo(sysUser.getUsername());
return Results.succ("");
}
@PostMapping("/repass")
@PreAuthorize("hasAuthority('sys:user:repass')")
public Results repass(@RequestBody Long id) {
SysUser byId = sysUserService.getById(id);
byId.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
byId.setCreated(LocalDateTime.now());
sysUserService.save(byId);
return Results.succ("");
}
// 个人中心修改
@PostMapping("/updatePass")
public Results updatePass(@Validated @RequestBody PassDto passDto, Principal principal) {
SysUser sysUser = sysUserService.getByUsername(principal.getName());
boolean matches = bCryptPasswordEncoder.matches(passDto.getCurrentPass(), sysUser.getPassword());
if (!matches) {
return Results.fail("密码不正确");
}
sysUser.setPassword(bCryptPasswordEncoder.encode(Const.PASS_WORD));
sysUser.setUpdated(LocalDateTime.now());
sysUserService.updateById(sysUser);
return Results.succ("");
}
}
上面用到一个sysRoleService.listRolesByUserId,通过用户id获取所有关联的角色,用到了中间表,可以写sql,这里我这样写的
@Overridepublic List<SysRole> listRolesByUserId(Long userId) { return this.list( new QueryWrapper<SysRole>() .inSql("id", "select role_id from sys_user_role where user_id = " + userId));}
userId一定要是自己数据库查出来的,千万别让前端传过来啥就直接调用这个方法,不然会可能会被攻击
结束
所有的资源放在了资源里面可以下载。
参考的up主 添加链接描述