入门基础项目(SpringBoot+Vue)
文章目录
- 1. css布局相关
- 2. JS
- 3. Vue 脚手架搭建
- 4. ElementUI
- 4.1 引入ElementUI
- 4.2 首页
- 4.2.1 整体框架
- 4.2.2 Aside-logo
- 4.2.3 Aside-菜单
- 4.2.4 Header-左侧
- 4.2.5 Header-右侧
- 4.2.6 iconfont 自定义图标
- 4.2.7 完整代码
- 4.3 封装前后端交互工具 axios
- 4.3.1 安装 axios
- 4.3.2 /src/utils/目录下建立一个request.js
- 4.3.3 main.js 全局声明
- 4.3.4 跨域访问
- 4.4 登录、注册
- 4.4.1 整体设计
- 4.4.2 验证码组件
- 4.4.3 登录 Login.vue
- 4.4.4 注册 Register.vue
- 4.4.5 router/index.js 配置路由
- 4.4.6 SpringBoot 解决跨域问题 CorsConfig
- 4.4.7 后端接口
- 4.5 SpringBoot集成JWT token实现权限验证
- 4.5.1 pom.xml添加JWT依赖
- 4.5.2 工具类 TokenUtils
- 4.5.3 login() 方法增加 token 返回
- 4.5.4 自定义注解 AuthAccess
- 4.5.5 自定义拦截器 JwtInterceptor
- 4.5.6 配置拦截器 InterceptorConfig
- 4.6 单文件、多文件上传和下载
- 4.6.1 文件上传、下载 Java 代码
- 4.6 个人信息修改、修改密码、重置密码
- 5. 相关学习网站
参考资料:📖【青哥带小白做毕设2024】所有教程资料汇总
1. css布局相关
参考资料:📖CSS-布局-flex
2. JS
变量赋值
b = a?.name
:a 是 undefined 或者 null,b 不报错b = a ?? c
:a 是 undefined 或者 null,则赋值 c 给 bb = a || c
:a 是 undefined 或者 null,则赋值 c 给 b
数组操作
- 新增元素:
push()
- 删除元素:
splice()、pop()、shift()
- 截取数组:
slice()
- 合并数组:
concat()
- 字符串变数组:
split()
- 数组变字符串:
join()
,默认使用,
逗号分割 - 获取元素序号:
indexOf()
filter()
:筛选元素- let newArr = users.filter(v => v.name !== ‘李四’ && v.name !== ‘王二’) // 删除数组的指定元素
find()
:查找map()
:转换- [1,2,3].map(v => v *2)
forEach()
:遍历reduce()
:合并- 语法:
arr.reduce(function(prev, cur, index, arr){...}, init);
- prev:累计器累计回调的返回值,表示上一次调用回调时的返回值,或者初始值 init
- cur:表示当前正在处理的数组元素
- index:表示当前正在处理的数组元素的索引
- arr:表示原数组
- init:初始值
- arr = [1,2,3]; let sum = arr.reduce((pre, cur) => pre + cur)
- 语法:
获取数组中每个字符出现的个数
let names =['a', 'b', 'c', 'a', 'b']
let res = names.reduce((all, cur) => {
if (cur in all) {
all[cur]++;
} else {
all[cur] = 1;
}
return all
}, {})
console.log("res=" , res) // {a: 2, b: 2, c: 1}
3. Vue 脚手架搭建
npm 配置淘宝镜像:
npm config set registry http://registry.npm.taobao.org/
安装vue/cli
npm install -g @vue/cli
vue --version
创建项目
vue create vue
配置文件 vue.config.js
配置启动端口、title等,修改后需要重启生效。
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 7000
},
chainWebpack: config => {
config.plugin('html')
.tap(args => {
args[0].title = "管理平台";
return args;
})
}
})
App.vue
<template>
<div id="app">
<router-view/>
</div>
</template>
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import '@/assets/css/global.css'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
HomeView.vue
<template>
<div>主页</div>
</template>
<script>
export default {
name: "HomeView",
};
</script>
assets/css/global.css
* {
box-sizing: border-box;
}
body {
color: #333;
font-size: 14px;
margin: 0;
padding: 0;
}
4. ElementUI
4.1 引入ElementUI
npm 安装
npm i element-ui -S
在 main.js 里引入 ElementUI
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI, { size: 'small' });
4.2 首页
4.2.1 整体框架
<el-container>
<el-aside>Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
4.2.2 Aside-logo
<div style="height: 60px; line-height: 60px; font-size: 20px; display: flex; align-items: center; justify-content: center">
<img src="@/assets/logo1.png" style="width: 30px;" alt="">
<span class="logo-title" v-show="!isCollapse">Honey2024</span>
</div>
4.2.3 Aside-菜单
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
router
background-color="#001529" text-color="rgba(255, 255, 255, 0.65)" active-text-color="#fff"
style="border: none"
:default-active="$route.path"
>
<el-menu-item index="/">
<i class="el-icon-house"></i> <span slot="title">系统首页</span>
</el-menu-item>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i> <span>信息管理</span>
</template>
<el-menu-item index="/user">用户信息</el-menu-item>
<el-menu-item index="/admin">管理员信息</el-menu-item>
</el-submenu>
</el-menu>
4.2.4 Header-左侧
<i :class="collapseIcon" @click="handleCollapse" style="font-size: 26px"></i>
<el-breadcrumb separator="/" style="margin-left: 20px">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">课程管理</el-breadcrumb-item>
</el-breadcrumb>
4.2.5 Header-右侧
<div style="flex: 1; display: flex; justify-content: flex-end; align-items: center">
<!-- 全屏按钮 -->
<i class="el-icon-full-screen" @click="handleFull" style="font-size: 25px"></i>
<!-- 下拉框 -->
<el-dropdown placement="bottom">
<div style="display: flex; align-items: center; cursor: pointer">
<img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px; margin: 0 5px">
<span>管理员</span>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
4.2.6 iconfont 自定义图标
iconfont-阿里巴巴矢量图标库
搜索需要的图标,添加到自己的项目
设置图标前缀:el-icon-
,Font Family:element-icons
点击 “下载至本地”
赋值4个文件到项目
在 main.js 里引入 iconfont
import '@/assets/css/iconfont/iconfont.css'
4.2.7 完整代码
<template>
<div>
<el-container>
<!-- 侧边栏 -->
<el-aside
:width="asideWidth"
style="min-height: 100vh; background-color: #001529"
>
<!-- logo+项目名称 -->
<div style="height: 60px; color: white; display: flex; align-items: center; justify-content: center">
<img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px">
<span class="logo-title" v-show="!isCollapse">honey2024</span>
</div>
<!-- 侧边菜单栏 -->
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
router
background-color="#001529" text-color="rgba(255, 255, 255, 0.65)" active-text-color="#fff"
style="border: none"
:default-active="$route.path"
>
<el-menu-item index="/">
<i class="el-icon-house"></i> <span slot="title">系统首页</span>
</el-menu-item>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-menu"></i> <span>信息管理</span>
</template>
<el-menu-item index="/user">用户信息</el-menu-item>
<el-menu-item index="/admin">管理员信息</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<!-- 头部区域 -->
<el-header>
<!-- 展开折叠按钮 -->
<i :class="collapseIcon" style="font-size: 26px" @click="handleCollapse"></i>
<!-- 面包屑 -->
<el-breadcrumb
separator-class="el-icon-arrow-right"
style="margin-left: 20px"
>
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/user' }">用户管理</el-breadcrumb-item>
</el-breadcrumb>
<!-- 头像下拉框 -->
<div style="flex: 1; display: flex; justify-content: flex-end; align-items: center">
<!-- 全屏按钮 -->
<i class="el-icon-full-screen" @click="handleFull" style="font-size: 25px"></i>
<!-- 下拉框 -->
<el-dropdown placement="bottom">
<div style="display: flex; align-items: center; cursor: pointer">
<img src="@/assets/logo1.png" alt="" style="width: 40px; height: 40px; margin: 0 5px">
<span>管理员</span>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<!-- 主体区域 -->
<el-main>
<div style="box-shadow: 0 0 10px rgba(0,0,0,.1); padding: 10px 20px; border-radius: 5px; margin-bottom: 10px">
早安,骚年,祝你开心每一天!
</div>
<el-card style="width: 500px">
<div slot="header" class="clearfix">
<span>2024项目管理平台</span>
</div>
<div>
2024项目管理平台正式开始了
<div style="margin-top: 20px">
<div style="margin: 10px 0"><strong>主题色</strong></div>
<el-button type="primary">按钮</el-button>
<el-button type="success">按钮</el-button>
<el-button type="warning">按钮</el-button>
<el-button type="danger">按钮</el-button>
<el-button type="info">按钮</el-button>
</div>
</div>
</el-card>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: "HomeView",
data() {
return {
isCollapse: false, // 不收缩
asideWidth: "200px",
collapseIcon: "el-icon-s-fold"
};
},
methods: {
handleFull() {
document.documentElement.requestFullscreen();
},
handleCollapse() {
this.isCollapse = !this.isCollapse;
this.asideWidth = this.isCollapse ? "64px" : "200px";
this.collapseIcon = this.isCollapse ? "el-icon-s-unfold" : "el-icon-s-fold";
}
}
};
</script>
<style>
.el-menu--inline {
background-color: #000c17 !important;
}
.el-menu--inline .el-menu-item {
background-color: #000c17 !important;
padding-left: 49px !important;
}
.el-menu-item:hover,
.el-submenu__title:hover {
color: #fff !important;
}
.el-submenu__title:hover i {
color: #fff !important;
}
.el-menu-item:hover i {
color: #fff !important;
}
.el-menu-item.is-active {
background-color: #1890ff !important;
border-radius: 5px !important;
width: calc(100% - 8px);
margin-left: 4px;
}
.el-menu-item.is-active i,
.el-menu-item.is-active .el-tooltip {
margin-left: -4px;
}
.el-menu-item {
height: 40px !important;
line-height: 40px !important;
}
.el-submenu__title {
height: 40px !important;
line-height: 40px !important;
}
.el-submenu .el-menu-item {
min-width: 0 !important;
}
.el-menu--inline .el-menu-item.is-active {
padding-left: 45px !important;
}
/*.el-submenu__icon-arrow {*/
/* margin-top: -5px;*/
/*}*/
.el-aside {
transition: width 0.3s;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
}
.logo-title {
margin-left: 5px;
font-size: 20px;
transition: all 0.3s; /* 0.3s */
}
.el-header {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
display: flex;
align-items: center;
}
</style>
4.3 封装前后端交互工具 axios
4.3.1 安装 axios
npm i axios -S
4.3.2 /src/utils/目录下建立一个request.js
import axios from 'axios'
// 创建一个新的 axios 对象
const request = axios.create({
baseURL: 'http://localhost:9090', // 请求后端地址
timeout: 30000 // 超时时间
})
// request 拦截器
// 可以自请求发送前对请求做一些处理
// 比如统一加token,对请求参数统一加密
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
// let user = localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : null
// config.headers['token'] = 'token' // 设置请求头
return config;
}, error => {
console.error('request error: ' + error); // for debug
return Promise.reject(error);
});
// response 拦截器
// 可以在接口响应后统一处理结果
request.interceptors.response.use(
response => {
let res = response.data;
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
return res;
},
error => {
console.error('response error: ' + error) // for debug
return Promise.reject(error)
}
)
export default request
4.3.3 main.js 全局声明
import request from '@/utils/request'
Vue.prototype.$request = request;
4.3.4 跨域访问
前端调用:
this.$request.get("selectAll").then((res) => {
this.tableData = res.data;
});
this.$request.get("selectByPage", {
params: { pageNum: 0, pageSize: 10, username: "gai", name: "盖" }
}).then((res) => {
this.tableData = res.data;
});
⚠️ 报错信息:
这就是常见的 跨域访问
问题。关于 跨域访问
参见 📖 关于跨域和端口问题 。本例报错原因是前端地址是 http://localhost:7000/
,访问后端地址 http://localhost:9090/
,端口不一致,导致 跨域访问
报错。
📌 解决方法:
在 UserController
上加个注解 @CrossOrigin
@CrossOrigin
@RestController
public class UserController {
}
响应头可见,Access-Control-Allow-Origin: *
,后端默认接收所以地址的请求。
4.4 登录、注册
4.4.1 整体设计
4.4.2 验证码组件
conponents/ValidCode.vue
<template>
<div class="ValidCode disabled-select" style="width: 100%; height: 100%" @click="refreshCode">
<span v-for="(item, index) in codeList" :key="index" :style="getStyle(item)">{{item.code}}</span>
</div>
</template>
<script>
export default {
name: 'ValidCode',
data () {
return {
length: 4,
codeList: []
}
},
mounted () {
this.createdCode()
},
methods: {
refreshCode () {
this.createdCode()
},
createdCode () {
let len = this.length,
codeList = [],
chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789',
charsLen = chars.length
// 生成
for (let i = 0; i < len; i++) {
let rgb = [Math.round(Math.random() * 220), Math.round(Math.random() * 240), Math.round(Math.random() * 200)]
codeList.push({
code: chars.charAt(Math.floor(Math.random() * charsLen)),
color: `rgb(${rgb})`,
padding: `${[Math.floor(Math.random() * 10)]}px`,
transform: `rotate(${Math.floor(Math.random() * 90) - Math.floor(Math.random() * 90)}deg)`
})
}
// 指向
this.codeList = codeList
// 将当前数据派发出去
this.$emit('update:value', codeList.map(item => item.code).join(''))
},
getStyle (data) {
return `color: ${data.color}; font-size: ${data.fontSize}; padding: ${data.padding}; transform: ${data.transform}`
}
}
}
</script>
<style>
.ValidCode{
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.ValidCode span {
display: inline-block;
font-size: 18px;
}
</style>
通过 this.$emit('update:value', codeList.map(item => item.code).join(''))
对外暴露方法 update:value
,将生成验证码暴露出去。
4.4.3 登录 Login.vue
<template>
<div style="height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #0f9876">
<div style="display: flex; background-color: white; width: 50%; border-radius: 5px; overflow: hidden">
<!-- 左侧图片区域 -->
<div style="flex: 1">
<img src="@/assets/login.png" alt="" style="width: 100%">
</div>
<!-- 右侧表单区域 -->
<div style="flex: 1; display: flex; align-items: center; justify-content: center">
<el-form :model="user" style="width: 80%" :rules="rules" ref="loginRef">
<div style="font-size: 20px; font-weight: bold; text-align: center; margin-bottom: 20px">欢迎登录后台管理系统</div>
<el-form-item prop="username">
<el-input prefix-icon="el-icon-user" size="medium" placeholder="请输入账号" v-model="user.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input prefix-icon="el-icon-lock" size="medium" placeholder="请输入密码" v-model="user.password" show-password></el-input>
</el-form-item>
<el-form-item prop="code">
<div style="display: flex">
<el-input style="flex: 1" prefix-icon="el-icon-circle-check" size="medium" placeholder="请输入验证码" v-model="user.code"></el-input>
<div style="flex: 1; height: 36px">
<!-- 验证码组件回调方法 -->
<valid-code @update:value="getCode" />
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width: 100%" @click="login">登 录</el-button>
</el-form-item>
<div style="display: flex">
<div style="flex: 1">还没有账号?请 <span style="color: #0f9876; cursor: pointer" @click="$router.push('/register')">注册</span></div>
<div style="flex: 1; text-align: right"><span style="color: #0f9876; cursor: pointer">忘记密码</span></div>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script>
import ValidCode from "@/components/ValidCode.vue";
export default {
name: "Login",
// 引入验证码组件
components: {
ValidCode,
},
data() {
// 自定义验证码校验
const validateCode = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入验证码"));
} else if (value.toLowerCase() !== this.componentCode) {
callback(new Error("验证码错误"));
} else {
callback();
}
};
return {
componentCode: "", // 验证码组件传递过来的 code
user: {
username: "",
password: "",
code: "", // 表单里用户输入的验证码 code
},
rules: {
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
code: [{ validator: validateCode, trigger: "blur" }],
},
};
},
methods: {
getCode(val) {
this.componentCode = val.toLowerCase();
},
login() {
this.$refs["loginRef"].validate((valid) => {
if (valid) {
// 验证通过
this.$request.post("/login", this.user).then((res) => {
if (res.code === "200") {
// 登录成功,跳转到首页
this.$router.push("/");
this.$message.success("登录成功");
localStorage.setItem("honey-user", JSON.stringify(res.data)); // 存储用户数据
} else {
this.$message.error(res.msg);
}
});
}
});
}
}
};
</script>
@update:value="getCode"
接收验证码组件传递参数。
用户名、密码、验证码验证通过后,this.$router.push("/")
跳转到首页,localStorage.setItem("honey-user", JSON.stringify(res.data));
存储用户数据到本地存储,用于后续网页访问读取 token
。
4.4.4 注册 Register.vue
<template>
<div style="height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #669fef">
<div style="display: flex; background-color: white; width: 50%; border-radius: 5px; overflow: hidden">
<div style="flex: 1">
<img src="@/assets/register.png" alt="" style="width: 100%">
</div>
<div style="flex: 1; display: flex; align-items: center; justify-content: center">
<el-form :model="user" style="width: 80%" :rules="rules" ref="registerRef">
<div style="font-size: 20px; font-weight: bold; text-align: center; margin-bottom: 20px">欢迎注册后台管理系统</div>
<el-form-item prop="username">
<el-input prefix-icon="el-icon-user" size="medium" placeholder="请输入账号" v-model="user.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input prefix-icon="el-icon-lock" size="medium" show-password placeholder="请输入密码" v-model="user.password"></el-input>
</el-form-item>
<el-form-item prop="confirmPass">
<el-input prefix-icon="el-icon-lock" size="medium" show-password placeholder="请确认密码" v-model="user.confirmPass"></el-input>
</el-form-item>
<el-form-item>
<el-button type="info" style="width: 100%" @click="register">注 册</el-button>
</el-form-item>
<div style="display: flex">
<div style="flex: 1">已经有账号了?请 <span style="color: #6e77f2; cursor: pointer" @click="$router.push('/login')">登录</span></div>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
// 验证码校验
const validatePassword = (rule, confirmPass, callback) => {
if (confirmPass === '') {
callback(new Error('请确认密码'))
} else if (confirmPass !== this.user.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
return {
user: {
username: '',
password: '',
confirmPass: ''
},
rules: {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
],
confirmPass: [
{ validator: validatePassword, trigger: 'blur' }
],
}
}
},
methods: {
register() {
this.$refs['registerRef'].validate((valid) => {
if (valid) {
// 验证通过
this.$request.post('/register', this.user).then(res => {
if (res.code === '200') {
this.$router.push('/login')
this.$message.success('注册成功')
} else {
this.$message.error(res.msg)
}
})
}
})
}
}
}
</script>
验证通过后,this.$router.push('/login')
跳转到登录页。
4.4.5 router/index.js 配置路由
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue')
},
{
path: '/register',
name: 'register',
component: () => import('../views/Register.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
将 login
、register
组件加入 router
。
4.4.6 SpringBoot 解决跨域问题 CorsConfig
访问地址 http://localhost:7000/login
输入用户名、密码、验证码后,访问后端地址 http://localhost:9090/login
,跨域访问
报错:
上一节给出解决方案:Controller
类加个注解 @CrossOrigin
,但每次新增 Controller
类都需要手工添加注解,比较麻烦。SpringBoot
提供过滤器 CorsFilter
统一处理 跨域访问
问题。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
4.4.7 后端接口
WebController
@RestController
public class WebController {
@Resource
UserService userService;
@PostMapping("/login")
public Result login(@RequestBody User user) {
System.out.println(user);
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return Result.error("数据输入不合法");
}
user = userService.login(user);
return Result.success(user);
}
@PostMapping("/register")
public Result register(@RequestBody User user) {
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return Result.error("数据输入不合法");
}
if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {
return Result.error("数据输入不合法");
}
user = userService.register(user);
return Result.success(user);
}
}
UserServiceImpl
@Override
public User login(User user) {
// 根据用户名查询数据库的用户信息
User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
if (dbUser == null) {
// 抛出一个自定义的异常
throw new ServiceException("用户名或密码错误");
}
if (!user.getPassword().equals(dbUser.getPassword())) {
throw new ServiceException("用户名或密码错误");
}
// 生成token
String token = TokenUtils.createToken(dbUser.getId(), dbUser.getPassword());
dbUser.setToken(token);
return dbUser;
}
@Override
public User register(User user) {
User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
if (dbUser != null) {
// 抛出一个自定义的异常
throw new ServiceException("用户名已存在");
}
user.setName(user.getUsername());
userMapper.insert(user);
return user;
}
自定义异常 ServiceException
@Getter
public class ServiceException extends RuntimeException {
private final String code;
public ServiceException(String msg) {
super(msg);
this.code = "500";
}
public ServiceException(String code, String msg) {
super(msg);
this.code = code;
}
}
GlobalException
@ControllerAdvice
public class GlobalException {
@ExceptionHandler(ServiceException.class)
@ResponseBody
public Result serviceException(ServiceException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
4.5 SpringBoot集成JWT token实现权限验证
4.5.1 pom.xml添加JWT依赖
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.3.0</version>
</dependency>
4.5.2 工具类 TokenUtils
@Component
public class TokenUtils {
private static UserMapper staticUserMapper;
@Resource
UserMapper userMapper;
@PostConstruct
public void setUserService() {
staticUserMapper = userMapper;
}
/**
* 生成token
*
* @return
*/
public static String createToken(String userId, String sign) {
return JWT.create().withAudience(userId) // 将 user id 保存到 token 里面,作为载荷
.withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
.sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
}
/**
* 获取当前登录的用户信息
*
* @return user对象
*/
public static User getCurrentUser() {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
String userId = JWT.decode(token).getAudience().get(0);
return staticUserMapper.selectById(Integer.valueOf(userId));
}
} catch (Exception e) {
return null;
}
return null;
}
}
4.5.3 login() 方法增加 token 返回
@Override
public User login(User user) {
// 根据用户名查询数据库的用户信息
User dbUser = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
if (dbUser == null) {
// 抛出一个自定义的异常
throw new ServiceException("用户名或密码错误");
}
if (!user.getPassword().equals(dbUser.getPassword())) {
throw new ServiceException("用户名或密码错误");
}
// 生成token
String token = TokenUtils.createToken(String.valueOf(dbUser.getId()), dbUser.getPassword());
dbUser.setToken(token);
return dbUser;
}
Login.vue 将返回 token
存储本地
login() {
this.$refs["loginRef"].validate((valid) => {
if (valid) {
// 验证通过
this.$request.post("/login", this.user).then((res) => {
if (res.code === "200") {
// 登录成功,跳转到首页
this.$router.push("/");
this.$message.success("登录成功");
localStorage.setItem("honey-user", JSON.stringify(res.data.token)); // 存储 token 到本地
} else {
this.$message.error(res.msg);
}
});
}
});
登录成功后,本地存储数据:
📌前端接口在每次请求后端数据的时候,都会在请求头带上这个 token
作为验证信息。
📅 request.js:
请求拦截器:对请求头增加 token
request.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json;charset=utf-8';
// 设置请求头,增加token
let token = JSON.parse(localStorage.getItem("honey-user") || '{}')
config.headers['token'] = token
return config;
}
响应拦截器:判断权限不足,重定向登录页面
request.interceptors.response.use(
response => {
let res = response.data;
// 兼容服务端返回的字符串数据
if (typeof res === 'string') {
res = res ? JSON.parse(res) : res
}
// 拦截权限不足的请求,重定向登录页面,防止直接输入网址访问
if (res.code === '401') {
router.push('/login')
}
return res;
}
)
如果不登录直接访问 http://localhost:7000/
,后台接口返回错误码 401
,会被响应拦截器拦截,重定向到登录页面。登录完成后,本地存储 token
,后续访问后端请求从本地存储获取到 token
,才能正常访问。
4.5.4 自定义注解 AuthAccess
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthAccess {
}
该注解用于标注权限放行的方法。
@AuthAccess
@PostMapping("/register")
public Result register(@RequestBody User user) {
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return Result.error("数据输入不合法");
}
if (user.getUsername().length() > 10 || user.getPassword().length() > 20) {
return Result.error("数据输入不合法");
}
user = userService.register(user);
return Result.success(user);
}
WebController 的方法 register()
标注注解 @AuthAccess
,结合下面拦截器 JwtInterceptor
对该注解的处理,register()
方法将被放行。
4.5.5 自定义拦截器 JwtInterceptor
public class JwtInterceptor implements HandlerInterceptor {
@Resource
private UserMapper userMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
token = request.getParameter("token");
}
// 对标注 AuthAccess 注解的方法进行放行
if (handler instanceof HandlerMethod) {
AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class);
if (annotation != null) {
return true;
}
}
// 判断前端上送 token,执行认证
if (StringUtils.isBlank(token)) {
throw new ServiceException("401", "请登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new ServiceException("401", "请登录");
}
// 根据token中的userid查询数据库
User user = userMapper.selectById(userId);
if (user == null) {
throw new ServiceException("401", "请登录");
}
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException("401", "请登录");
}
return true;
}
}
📌 请求头获取 token
字段值,进行JWT 认证判断是否为登录成功生成的 token
,进而进行权限认证。
💦
if (handler instanceof HandlerMethod)
的含义是什么?
1.springmvc 启动时候,扫描所有 controller 类,解析所有映射方法,将每个映射方法封装一个对象
HandlerMethod
,该类包含所有请求映射方法信息(映射路径 / 方法名 / 参数 / 注解 / 返回值),上例中AuthAccess annotation = ((HandlerMethod) handler).getMethodAnnotation(AuthAccess.class)
,就是获取请求方法是否标注AuthAccess
注解。
2.springmvc 针对这些请求映射方法信息封装对象类,使用类似 map 的数据结构进行统一管理Map<String, HandlerMethod> map
3.页面发起请求时(/users/currentUser),进入拦截器之后,springmvc 自动解析请求路径,得到 url(/users/currentUser),获取url之后,进而获取 /users/currentUser 路径对应的映射方法HandlerMethod
实例
4.调用拦截器preHandle
方法并将请求对象、响应对象、映射方法对象handler
一起传入。
📖 登录拦截器原理
1.在Spring MVC中,拦截器的
preHandle
、postHandle
、afterCompletion
方法的第三个参数是一个 Object 类型的handler
参数。这个handler
参数实际上就是处理当前请求的处理器。
2.在Spring MVC中,处理器不一定是HandlerMethod
类型的。例如,当请求的URL对应的是一个静态资源时,处理器可能是ResourceHttpRequestHandler
类型的。
3.因此,如果你的拦截器的代码只适用于HandlerMethod
类型的处理器,你需要在代码中加入if (handler instanceof HandlerMethod)
这样的判断,以确保代码不会在处理其他类型的处理器时出错。
4.在Spring MVC中,HandlerMethod
是一个特殊的处理器类型,它用于处理由@RequestMapping
注解(或其变体,如@GetMapping、@PostMapping等)标注的方法。
📖 Springmvc拦截器的时候要加判断 handler instanceof HandlerMethod
4.5.6 配置拦截器 InterceptorConfig
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**") // 1. 设置拦截路径
.excludePathPatterns("/login"); // 2. 设置放行路径
super.addInterceptors(registry);
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
.addPathPatterns("/**")
:对所用请求地址进行拦截
.excludePathPatterns("/login")
:设置放行路径,此处对 /login
进行放行,不进行拦截处理,即不校验 token
。如果想对整个路径放行,可以设置 /login/**
,即对 /login
下所有路径放行。
.excludePathPatterns(url)
和 注解@AuthAccess
结合使用,可以灵活设置放行方法。
4.6 单文件、多文件上传和下载
4.6.1 文件上传、下载 Java 代码
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${ip:localhost}")
String ip;
@Value("${server.port}")
String port;
private static final String ROOT_PATH = System.getProperty("user.dir") + File.separator + "files";
@PostMapping("/upload")
public Result upload(MultipartFile file) throws IOException {
// 文件的原始名称
String originalFilename = file.getOriginalFilename();
// 获取文件名称、后缀名
String mainName = FileUtil.mainName(originalFilename);
String extName = FileUtil.extName(originalFilename);
// 如果当前文件的父级目录不存在,就创建
if (!FileUtil.exist(ROOT_PATH)) {
FileUtil.mkdir(ROOT_PATH);
}
// 如果当前上传的文件已经存在了,那么重命名一个文件
if (FileUtil.exist(ROOT_PATH + File.separator + originalFilename)) {
originalFilename = System.currentTimeMillis() + "_" + mainName + "." + extName;
}
File saveFile = new File(ROOT_PATH + File.separator + originalFilename);
// 存储文件到本地的磁盘里面去
file.transferTo(saveFile);
String url = "http://" + ip + ":" + port + "/file/download/" + originalFilename;
// 返回文件的链接,这个链接就是文件的下载地址,这个下载地址就是我的后台提供出来的
return Result.success(url);
}
@AuthAccess
@GetMapping("/download/{fileName}")
public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {
// 附件下载
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
// 预览
// response.addHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
String filePath = ROOT_PATH + File.separator + fileName;
if (!FileUtil.exist(filePath)) {
return;
}
byte[] bytes = FileUtil.readBytes(filePath);
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
📅 响应头 Content-Disposition
为 attachment;filename=
,文件以附件形式下载;
📅Content-Disposition
为 inline;filename=
,图片和 pdf 可以预览,其他文件类型还是以附件形式下载。
4.6 个人信息修改、修改密码、重置密码
📚 ElementUI官网
📖 Element-UI自学实践
5. 相关学习网站
详尽的搭建过程可以参考:
📚使用ElementPlus页面布局搭建
📚[bilibili]VUE项目,VUE项目实战,vue后台管理系统,前端面试,前端面试项目
关于跨域和端口问题