若依前后端分離版源碼分析-前端頭像上傳后傳遞到后台以及在服務器上存儲和數據庫存儲設計


場景

使用若依前后端分離版本時,分析其頭像上傳機制。

可作為續參考學習。

 

 

注:

博客:
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());
    }
}

 

由此實現了整個前后端頭像上傳與存儲於查詢和映射的全過程。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM