若依前後端分離版原始碼分析-前端頭像上傳後傳遞到後臺以及在伺服器上儲存和資料庫儲存設計
場景
使用若依前後端分離版本時,分析其頭像上傳機制。
可作為續參考學習。
注:
部落格:
https://blog.csdn.net/badao_liumang_qizhi
關注公眾號
霸道的程式猿
獲取程式設計相關電子書、教程推送與免費下載。
實現
首先是前端,登入成功點選個人中心
對應的程式碼為
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <div class="avatar-wrapper"> <img :src="avatar" class="user-avatar"> <i class="el-icon-caret-bottom" /> </div> <el-dropdown-menu slot="dropdown"> <router-link to="/user/profile"> <el-dropdown-item>個人中心</el-dropdown-item> </router-link> <el-dropdown-item @click.native="setting = true"> <span>佈局設定</span> </el-dropdown-item> <el-dropdown-item divided @click.native="logout"> <span>退出登入</span> </el-dropdown-item> </el-dropdown-menu> </el-dropdown>
這裡的img就是右上角的頭像圖片,這裡的的src屬性後面講。
然後點選個人中心時,跳轉到user/profile/index.vue,這裡的頭像引用的頭像元件並且傳遞user物件引數。
<div slot="header" class="clearfix"> <span>個人資訊</span> </div> <div> <div class="text-center"> <userAvatar :user="user" /> </div> <ul class="list-group list-group-striped"> <li class="list-group-item"> <svg-icon icon-class="user" />使用者名稱稱 <div class="pull-right">{{ user.userName }}</div> </li>
這裡傳遞的user是從後臺資料庫查詢的使用者資料
created() { this.getUser(); }, methods: { getUser() { getUserProfile().then(response => { this.user = response.data; this.roleGroup = response.roleGroup; this.postGroup = response.postGroup; }); } }
在個人資訊頁面載入完成就請求後臺獲取資料。
請求後臺的資料是呼叫的getUserProfile,此方法是呼叫資料的介面方法
// 查詢使用者個人資訊 export function getUserProfile() { return request({ url: '/system/user/profile', method: 'get' }) }
請求的後臺介面
@GetMapping public AjaxResult profile() { LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); SysUser user = loginUser.getUser(); AjaxResult ajax = AjaxResult.success(user); ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername())); ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername())); return ajax; }
後臺介面中在通過token的服務類獲取登入的使用者的相關資訊,然後返回給前端。
前面講講上面獲取的登入使用者的資訊傳遞給使用者頭像元件,通過如下方式
<userAvatar :user="user" />
然後來到使用者頭像元件,頁面效果為
此元件對應的程式碼為
<template> <div> <img v-bind:src="options.img" @click="editCropper()" title="點選上傳頭像" class="img-circle img-lg" /> <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @opened="modalOpened"> <el-row> <el-col :xs="24" :md="12" :style="{height: '350px'}"> <vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop" :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox" @realTime="realTime" v-if="visible" /> </el-col> <el-col :xs="24" :md="12" :style="{height: '350px'}"> <div class="avatar-upload-preview"> <img :src="previews.url" :style="previews.img" /> </div> </el-col> </el-row> <br /> <el-row> <el-col :lg="2" :md="2"> <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"> <el-button size="small"> 上傳 <i class="el-icon-upload el-icon--right"></i> </el-button> </el-upload> </el-col> <el-col :lg="{span: 1, offset: 2}" :md="2"> <el-button icon="el-icon-plus" size="small" @click="changeScale(1)"></el-button> </el-col> <el-col :lg="{span: 1, offset: 1}" :md="2"> <el-button icon="el-icon-minus" size="small" @click="changeScale(-1)"></el-button> </el-col> <el-col :lg="{span: 1, offset: 1}" :md="2"> <el-button icon="el-icon-refresh-left" size="small" @click="rotateLeft()"></el-button> </el-col> <el-col :lg="{span: 1, offset: 1}" :md="2"> <el-button icon="el-icon-refresh-right" size="small" @click="rotateRight()"></el-button> </el-col> <el-col :lg="{span: 2, offset: 6}" :md="2"> <el-button type="primary" size="small" @click="uploadImg()">提 交</el-button> </el-col> </el-row> </el-dialog> </div> </template> <script> import store from "@/store"; import { VueCropper } from "vue-cropper"; import { uploadAvatar } from "@/api/system/user"; export default { components: { VueCropper }, props: { user: { type: Object } }, data() { return { // 是否顯示彈出層 open: false, // 是否顯示cropper visible: false, // 彈出層標題 title: "修改頭像", options: { img: store.getters.avatar, //裁剪圖片的地址 autoCrop: true, // 是否預設生成截圖框 autoCropWidth: 200, // 預設生成截圖框寬度 autoCropHeight: 200, // 預設生成截圖框高度 fixedBox: true // 固定截圖框大小 不允許改變 }, previews: {} }; }, created(){ this.getUserAvator(); }, methods: { //查詢資料庫獲取使用者頭像 getUserAvator(){ }, // 編輯頭像 editCropper() { this.open = true; }, // 開啟彈出層結束時的回撥 modalOpened() { this.visible = true; }, // 覆蓋預設的上傳行為 requestUpload() { }, // 向左旋轉 rotateLeft() { this.$refs.cropper.rotateLeft(); }, // 向右旋轉 rotateRight() { this.$refs.cropper.rotateRight(); }, // 圖片縮放 changeScale(num) { num = num || 1; this.$refs.cropper.changeScale(num); }, // 上傳預處理 beforeUpload(file) { if (file.type.indexOf("image/") == -1) { this.msgError("檔案格式錯誤,請上傳圖片型別,如:JPG,PNG字尾的檔案。"); } else { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { this.options.img = reader.result; }; } }, // 上傳圖片 uploadImg() { this.$refs.cropper.getCropBlob(data => { let formData = new FormData(); formData.append("avatarfile", data); uploadAvatar(formData).then(response => { if (response.code === 200) { this.open = false; debugger this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl; store.commit('SET_AVATAR', this.options.img); this.msgSuccess("修改成功"); } this.visible = false; }); }); }, // 實時預覽 realTime(data) { this.previews = data; } } }; </script>
在此元件中用到了圖片裁剪元件vue-cropper並且其自帶的預覽效果,然後圖片上傳使用了
el-upload。
在此元件中接收上面傳遞過來的使用者資訊引數
props: {
user: {
type: Object
}
},
但是接收到的使用者資訊在此元件中並沒有使用,對於裁剪圖片的url使用的是
img: store.getters.avatar, //裁剪圖片的地址
store來源外部js
import store from "@/store";
在外部js中
import Vue from 'vue' import Vuex from 'vuex' import app from './modules/app' import user from './modules/user' import tagsView from './modules/tagsView' import permission from './modules/permission' import settings from './modules/settings' import getters from './getters' Vue.use(Vuex) const store = new Vuex.Store({ modules: { app, user, tagsView, permission, settings }, getters }) export default store
使用的是Vuex的Store作為快取。
而且在此元件中也沒有呼叫後臺介面獲取頭像
created(){ this.getUserAvator(); }, methods: { //查詢資料庫獲取使用者頭像 getUserAvator(){ },
方法為空,具體可以根據自己的需要去決定是否請求後臺資料獲取頭像。
這裡是直接從快取中直接取值,快取中的值是在登入的是後請求後臺介面直接獲取使用者的頭像資訊並存入快取。
在登入頁面Login.vue中,點選登入對應的方法中
handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true; if (this.loginForm.rememberMe) { Cookies.set("username", this.loginForm.username, { expires: 30 }); Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); } else { Cookies.remove("username"); Cookies.remove("password"); Cookies.remove('rememberMe'); } this.$store .dispatch("Login", this.loginForm) .then(() => { this.$router.push({ path: this.redirect || "/" }); }) .catch(() => { this.loading = false; this.getCode(); }); } }); }
驗證並且儲存使用者名稱密碼等操作後通過
this.$store .dispatch("Login", this.loginForm)
呼叫store下的modules下的user.js下Login
actions: { // 登入 Login({ commit }, userInfo) { const username = userInfo.username.trim() const password = userInfo.password const code = userInfo.code const uuid = userInfo.uuid return new Promise((resolve, reject) => { login(username, password, code, uuid).then(res => { setToken(res.token) commit('SET_TOKEN', res.token) resolve() }).catch(error => { reject(error) }) }) },
然後在許可權驗證的permission.js中
import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' import 'nprogress/nprogress.css' import { getToken } from '@/utils/auth' NProgress.configure({ showSpinner: false }) const whiteList = ['/login', '/auth-redirect', '/bind', '/register'] router.beforeEach((to, from, next) => { NProgress.start() if (getToken()) { /* has token*/ if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { if (store.getters.roles.length === 0) { // 判斷當前使用者是否已拉取完user_info資訊 store.dispatch('GetInfo').then(res => { // 拉取user_info const roles = res.roles store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => { // 測試 預設靜態頁面 // store.dispatch('permission/generateRoutes', { roles }).then(accessRoutes => { // 根據roles許可權生成可訪問的路由表 router.addRoutes(accessRoutes) // 動態新增可訪問路由表 next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 }) }) .catch(err => { store.dispatch('FedLogOut').then(() => { Message.error(err) next({ path: '/' }) }) }) } else { next() // 沒有動態改變許可權的需求可直接next() 刪除下方許可權判斷 ↓ // if (hasPermission(store.getters.roles, to.meta.roles)) { // next() // } else { // next({ path: '/401', replace: true, query: { noGoBack: true }}) // } // 可刪 ↑ } } } else { // 沒有token if (whiteList.indexOf(to.path) !== -1) { // 在免登入白名單,直接進入 next() } else { next(`/login?redirect=${to.fullPath}`) // 否則全部重定向到登入頁 NProgress.done() } } }) router.afterEach(() => { NProgress.done() })
又執行了拉取登入使用者資訊的方法,在拉取使用者資訊的方法中
// 獲取使用者資訊 GetInfo({ commit, state }) { return new Promise((resolve, reject) => { getInfo(state.token).then(res => { const user = res.user if (res.roles && res.roles.length > 0) { // 驗證返回的roles是否是一個非空陣列 commit('SET_ROLES', res.roles) commit('SET_PERMISSIONS', res.permissions) } else { commit('SET_ROLES', ['ROLE_DEFAULT']) } commit('SET_NAME', user.userName) commit('SET_AVATAR', avatar) resolve(res) }).catch(error => { reject(error) }) }) },
將使用者頭像儲存進快取中,通過
commit('SET_AVATAR', avatar)
而這裡的獲取使用者頭像的路徑的方法是如下,這裡修改過
判斷從後臺查詢的頭像路徑是否為空,如果為空的話則使用預設圖片。
const avatar = (user.avatar == "" || !user ) ? require("@/assets/image/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
這就是為什麼在進行上傳頭像時能在快取中取到頭像。
然後點選上面的上傳按鈕執行的方法
// 上傳圖片 uploadImg() { this.$refs.cropper.getCropBlob(data => { let formData = new FormData(); formData.append("avatarfile", data); uploadAvatar(formData).then(response => { if (response.code === 200) { this.open = false; debugger this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl; store.commit('SET_AVATAR', this.options.img); this.msgSuccess("修改成功"); } this.visible = false; });
首先將圖片上傳到伺服器,然後上傳成功後將返回的頭像的路徑存在快取中。
呼叫了上傳頭像的介面方法uploadAvator
// 使用者頭像上傳 export function uploadAvatar(data) { return request({ url: '/system/user/profile/avatar', method: 'post', data: data }) }
來到上傳照片對應的後臺介面
@PostMapping("/avatar") public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws IOException { if (!file.isEmpty()) { LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file); if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) { AjaxResult ajax = AjaxResult.success(); ajax.put("imgUrl", avatar); // 更新快取使用者頭像 loginUser.getUser().setAvatar(avatar); tokenService.setLoginUser(loginUser); return ajax; } } return AjaxResult.error("上傳圖片異常,請聯絡管理員"); }
在後臺介面中首先是使用Token的service獲取當前登入的使用者。
然後呼叫檔案上傳工具類的檔案上傳方法upload,引數為在application.yml中設定的上傳檔案的路徑和檔案。
RuoYiConfig.getAvatarPath()中
public static String getAvatarPath() { return getProfile() + "/avatar"; }
呼叫getProfile方法並且拼接一個avatar路徑。
在方法getProfile中
public static String getProfile() { return profile; }
返回profile節點的值
/** 上傳路徑 */
private static
String profile;
此配置類使用了註解就可以獲取到application.yml中配置的profile節點所對應的配置內容
@Component @ConfigurationProperties(prefix = "ruoyi") public class RuoYiConfig { /** 專案名稱 */ private String name; /** 版本 */ private String version; /** 版權年份 */ private String copyrightYear; /** 例項演示開關 */ private boolean demoEnabled; /** 上傳路徑 */ private static String profile;
最終獲取的是下面配置的D:ruoyi/uploadPath路徑
再回到上面的upload方法
public static final String upload(String baseDir, MultipartFile file) throws IOException { try { return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } catch (Exception e) { throw new IOException(e.getMessage(), e); } }
此方法是根據檔案路徑進行上傳,檔案路徑已經在上面進行指定。
其中又呼叫了upload方法,第三個引數是用來指定檔案的字尾名
public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 圖片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // 壓縮檔案 "rar", "zip", "gz", "bz2", // pdf "pdf" };
在上傳的方法中
/** * 檔案上傳 * * @param baseDir 相對應用的基目錄 * @param file 上傳的檔案 * @param extension 上傳檔案型別 * @return 返回上傳成功的檔名 * @throws FileSizeLimitExceededException 如果超出最大大小 * @throws FileNameLengthLimitExceededException 檔名太長 * @throws IOException 比如讀寫檔案出錯時 * @throws InvalidExtensionException 檔案校驗異常 */ public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, InvalidExtensionException { int fileNamelength = file.getOriginalFilename().length(); if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) { throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); } assertAllowed(file, allowedExtension); String fileName = extractFilename(file); File desc = getAbsoluteFile(baseDir, fileName); file.transferTo(desc); String pathFileName = getPathFileName(baseDir, fileName); return pathFileName; }
首先是進行檔案長度的判斷,這裡是指定的100
然後通過assertAllowed對檔案大小進行校驗,具體實現方法
/** * 檔案大小校驗 * * @param file 上傳的檔案 * @return * @throws FileSizeLimitExceededException 如果超出最大大小 * @throws InvalidExtensionException */ public static final void assertAllowed(MultipartFile file, String[] allowedExtension) throws FileSizeLimitExceededException, InvalidExtensionException { long size = file.getSize(); if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE) { throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); } String fileName = file.getOriginalFilename(); String extension = getExtension(file); if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) { throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, fileName); } else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) { throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, fileName); } else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) { throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, fileName); } else { throw new InvalidExtensionException(allowedExtension, extension, fileName); } } }
然後呼叫extractFilename對檔名進行編碼,具體實現
/** * 編碼檔名 */ public static final String extractFilename(MultipartFile file) { String fileName = file.getOriginalFilename(); String extension = getExtension(file); fileName = DateUtils.datePath() + "/" + encodingFilename(fileName) + "." + extension; return fileName; }
這其中獲取檔名和副檔名再呼叫工具類生成一個日期路徑,比如下面的示例2018/08/08
/** * 日期路徑 即年/月/日 如2018/08/08 */ public static final String datePath() { Date now = new Date(); return DateFormatUtils.format(now, "yyyy/MM/dd"); }
encodingFilename:
/** * 編碼檔名 */ private static final String encodingFilename(String fileName) { fileName = fileName.replace("_", " "); fileName = Md5Utils.hash(fileName + System.nanoTime() + counter++); return fileName; }
其中又呼叫了工具類的hash方法和獲取當前時間的納秒並且加上一個遞增的計數變數。
private static int counter = 0;
在hash方法中
public static String hash(String s) { try { return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8"); } catch (Exception e) { log.error("not supported charset...{}", e); return s; } }
完整的MD5加密工具類Md5Utils程式碼
package com.ruoyi.common.utils.security; import java.security.MessageDigest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Md5加密方法 * * @author ruoyi */ public class Md5Utils { private static final Logger log = LoggerFactory.getLogger(Md5Utils.class); private static byte[] md5(String s) { MessageDigest algorithm; try { algorithm = MessageDigest.getInstance("MD5"); algorithm.reset(); algorithm.update(s.getBytes("UTF-8")); byte[] messageDigest = algorithm.digest(); return messageDigest; } catch (Exception e) { log.error("MD5 Error...", e); } return null; } private static final String toHex(byte hash[]) { if (hash == null) { return null; } StringBuffer buf = new StringBuffer(hash.length * 2); int i; for (i = 0; i < hash.length; i++) { if ((hash[i] & 0xff) < 0x10) { buf.append("0"); } buf.append(Long.toString(hash[i] & 0xff, 16)); } return buf.toString(); } public static String hash(String s) { try { return new String(toHex(md5(s)).getBytes("UTF-8"), "UTF-8"); } catch (Exception e) { log.error("not supported charset...{}", e); return s; } } }
再回到上面檔案上傳的具體方法upload中對檔名稱進行編碼後
呼叫獲取檔案絕對路徑的方法
File desc = getAbsoluteFile(baseDir, fileName);
此方法實現
private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { File desc = new File(uploadDir + File.separator + fileName); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } if (!desc.exists()) { desc.createNewFile(); } return desc; }
此時獲取的檔案的絕對路徑類似如下
然後將上傳的檔案轉換成指定的絕對路徑的檔案,就能將上傳的檔案儲存到伺服器上指定的檔案路徑。
file.transferTo(desc);
然後需要將相對路徑返回給前端,所以呼叫
String pathFileName = getPathFileName(baseDir, fileName);
就可以獲取上傳後頭像的相對路徑,此方法的實現為
private static final String getPathFileName(String uploadDir, String fileName) throws IOException { int dirLastIndex = RuoYiConfig.getProfile().length() + 1; String currentDir = StringUtils.substring(uploadDir, dirLastIndex); String pathFileName = Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName; return pathFileName; }
首先獲取的是配置的上傳檔案的路徑的路徑的長度加上1,在將絕對路徑按此進行擷取,只取後面的部分。
然後獲取常量類中的資源對映路徑的字首
/**
* 資源對映路徑 字首
*/
public static final
String RESOURCE_PREFIX = "/profile";
這樣獲取相對路徑為
這樣的話就能將伺服器上需要請求的圖片的路徑獲取到。再回到頭像上傳的介面中。
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file); if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) { AjaxResult ajax = AjaxResult.success(); ajax.put("imgUrl", avatar); // 更新快取使用者頭像 loginUser.getUser().setAvatar(avatar); tokenService.setLoginUser(loginUser); return ajax; }
獲取了頭像地址,將此頭像的地址更新到資料庫中儲存。然後將此頭像地址儲存進快取中並且返回給前端。
在資料庫中將上面的頭像路徑進行儲存
後臺將頭像的路徑返回給前端後
// 上傳圖片 uploadImg() { this.$refs.cropper.getCropBlob(data => { let formData = new FormData(); formData.append("avatarfile", data); uploadAvatar(formData).then(response => { if (response.code === 200) { this.open = false; debugger this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl; var path = this.options.img; console.log(this.options.img); store.commit('SET_AVATAR', this.options.img); this.msgSuccess("修改成功"); } this.visible = false; }); }); },
前端獲取並拼接一個配置的請求url的字首
this.options.img = process.env.VUE_APP_BASE_API + response.imgUrl;
後臺獲取的路徑
最終前端需要的路徑為
然後將其儲存進快取中,所以這張頭像的路徑就是
/dev-api/profile/avatar/2020/08/27/a2869bfe4c34c5eac14211dd0d24c0db.jpeg
為什麼前端通過這就能獲取到後臺的這張照片。
在前端專案的vue.config.js中,配置的請求代理,
devServer: { host: '0.0.0.0', port: port, proxy: { // detail: https://cli.vuejs.org/config/#devserver-proxy [process.env.VUE_APP_BASE_API]: { target: `http://localhost:8080`, changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } } }, disableHostCheck: true },
所有前端的指定的ip加埠加配置的介面字首(這裡是/dev-api)都會被代理到http://localhost:8080
所以這裡的前端請求的/dev-api/profile/avatar/2020/08/27/a2869bfe4c34c5eac14211dd0d24c0db.jpeg
就被代理到
http://localhost:8080/profile/avatar/2020/08/27/a2869bfe4c34c5eac14211dd0d24c0db.jpeg
瀏覽器中可以直接對此頭像檔案進行訪問
那麼在後端是怎樣對這個資源請求進行處理的。
在後臺專案的com.ruoyi.framework.config下的ResourcesConfig使用此配置類配置靜態資源對映
package com.ruoyi.framework.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.ruoyi.common.constant.Constants; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; /** * 通用配置 * * @author ruoyi */ @Configuration public class ResourcesConfig implements WebMvcConfigurer { @Autowired private RepeatSubmitInterceptor repeatSubmitInterceptor; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { /** 本地檔案上傳路徑 */ registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); /** swagger配置 */ registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } /** * 自定義攔截規則 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); } }
通過重寫addResourceHandlers
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
就可以將上面的請求對映為伺服器上本地的路徑。
還有就是在前端請求靜態資源時將許可權驗證放開,即執行匿名訪問。
在com.ruoyi.framework.config下的SecurityConfig中對此請求的url配置為允許匿名訪問。
@Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CRSF禁用,因為不使用session .csrf().disable() // 認證失敗處理類 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基於token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 過濾請求 .authorizeRequests() // 對於登入login 驗證碼captchaImage 允許匿名訪問 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/profile/**").anonymous()
配置類完整程式碼
package com.ruoyi.framework.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; /** * spring security配置 * * @author ruoyi */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定義使用者認證邏輯 */ @Autowired private UserDetailsService userDetailsService; /** * 認證失敗處理類 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出處理類 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token認證過濾器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 解決 無法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有請求路徑 * access | SpringEl表示式結果為true時可以訪問 * anonymous | 匿名可以訪問 * denyAll | 使用者不能訪問 * fullyAuthenticated | 使用者完全認證可以訪問(非remember-me下自動登入) * hasAnyAuthority | 如果有引數,引數表示許可權,則其中任何一個許可權可以訪問 * hasAnyRole | 如果有引數,引數表示角色,則其中任何一個角色可以訪問 * hasAuthority | 如果有引數,引數表示許可權,則其許可權可以訪問 * hasIpAddress | 如果有引數,引數表示IP地址,如果使用者IP和引數匹配,則可以訪問 * hasRole | 如果有引數,引數表示角色,則其角色可以訪問 * permitAll | 使用者可以任意訪問 * rememberMe | 允許通過remember-me登入的使用者訪問 * authenticated | 使用者登入後可訪問 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // CRSF禁用,因為不使用session .csrf().disable() // 認證失敗處理類 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基於token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 過濾請求 .authorizeRequests() // 對於登入login 驗證碼captchaImage 允許匿名訪問 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/profile/**").anonymous() .antMatchers("/common/download**").anonymous() .antMatchers("/common/download/resource**").anonymous() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/druid/**").anonymous() // 除上面外的所有請求全部需要鑑權認證 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 新增JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } /** * 強雜湊雜湊加密實現 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份認證介面 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }
由此實現了整個前後端頭像上傳與儲存於查詢和對映的全過程。