文章目錄
- 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 = falsenew 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"routerbackground-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"routerbackground-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-breadcrumbseparator-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 debugreturn 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 debugreturn 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: "", // 驗證碼組件傳遞過來的 codeuser: {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;@Beanpublic 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 {@ResourceUserService 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("用戶名或密碼錯誤");}// 生成tokenString 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)@ResponseBodypublic 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;@ResourceUserMapper userMapper;@PostConstructpublic 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("用戶名或密碼錯誤");}// 生成tokenString 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';// 設置請求頭,增加tokenlet token = JSON.parse(localStorage.getItem("honey-user") || '{}')config.headers['token'] = tokenreturn 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 {@Resourceprivate UserMapper userMapper;@Overridepublic 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 idString 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", "請登錄");}// 用戶密碼加簽驗證 tokenJWTVerifier 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 {@Overrideprotected void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**") // 1. 設置攔截路徑.excludePathPatterns("/login"); // 2. 設置放行路徑super.addInterceptors(registry);}@Beanpublic 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后臺管理系統,前端面試,前端面試項目
關于跨域和端口問題