密码学小学期实践
☁️

密码学小学期实践

创建时间
Jul 12, 2022 01:08 AM
标签
Author
Published

☁️CUC-cloud-disk-upload-management-system

一个基于Vue和springboot+mybatisplus实现的文件云盘管理系统

演示视频

项目展示地址

💭在线地址http://119.23.64.32/
🎆项目域名http://www.cucinstall.cn/(目前还在备案中。。。。。)

使用手册

请参照中传放心传官方文档

源码地址

GitHub - Xuyan-cmd/CUC-cloud-disk-upload-management-system: 一个基于Vue和springboot+mybatisplus实现的文件云盘管理系统
基于网页的用户注册与登录系统(60分) 使用https绑定证书到域名而非IP地址 【 PKI X.509 】 允许用户注册到系统 用户名的合法字符集范围:中文、英文字母、数字 类似:-、_、.等合法字符集范围之外的字符不允许使用 用户口令长度限制在36个字符之内 对用户输入的口令进行强度校验,禁止使用弱口令 使用合法用户名和口令登录系统 禁止使用明文存储用户口令 【PBKDF2、散列算法、慢速散列、针对散列算法(如MD5、SHA1等)的攻击方法】 存储的口令即使被公开,也无法还原/解码出原始明文口令 (可选)安全的忘记口令 / 找回密码功能 (可选)微信/微博/支付宝的OAuth授权登录 / 注册绑定 基于网页的文件上传加密与数字签名系统(20分) 已完成《基于网页的用户注册与登录系统》所有要求 限制文件大小:小于 10MB 限制文件类型:office文档、常见图片类型 匿名用户禁止上传文件 对文件进行对称加密存储到文件系统,禁止明文存储文件 【 对称加密 密钥管理(如何安全存储对称加密密钥) 对称加密密文的PADDING问题 】 系统对加密后文件进行数字签名 【 数字签名(多种签名工作模式差异) 】 (可选)文件秒传:服务器上已有的文件,客户端可以不必再重复上传了 基于网页的加密文件下载与解密(20分) 已完成《基于网页的文件上传加密与数字签名系统》所有要求 提供匿名用户加密后文件和关联的数字签名文件的下载 客户端对下载后的文件进行数字签名验证 【 非对称(公钥)加密 数字签名 】 客户端对下载后的文件可以解密还原到原始文件 【 对称解密 密钥管理
GitHub - Xuyan-cmd/CUC-cloud-disk-upload-management-system: 一个基于Vue和springboot+mybatisplus实现的文件云盘管理系统

任务清单

第一阶段(7.12-7.17(周日))
  • 基于网页的用户注册与登录系统(60分)
    • 使用https绑定证书到域名而非IP地址 【 PKI X.509 】
      允许用户注册到系统
      • 用户名的合法字符集范围:中文、英文字母、数字
        • 类似:-、_、.等合法字符集范围之外的字符不允许使用
      • 用户口令长度限制在36个字符之内
      禁止使用明文存储用户口令 【PBKDF2、散列算法、慢速散列、针对散列算法(如MD5、SHA1等)的攻击方法】
      • 存储的口令即使被公开,也无法还原/解码出原始明文口令
      • 对用户输入的口令进行强度校验,禁止使用弱口令
      使用合法用户名和口令登录系统
      (可选)安全的忘记口令 / 找回密码功能
      (可选)微信/微博/支付宝的OAuth授权登录 / 注册绑定
      (可选)双因素认证
      • OTP: Google Authenticator
      • Email
      • SMS
      • 扫码登录
第二阶段(7.18-7.24)
  • 基于网页的文件上传加密与数字签名系统(20分)
    • 已完成《基于网页的用户注册与登录系统》所有要求
      限制文件大小:小于 10MB
      限制文件类型:office文档、常见图片类型
      匿名用户禁止上传文件
      对文件进行对称加密存储到文件系统,禁止明文存储文件 【 对称加密 密钥管理(如何安全存储对称加密密钥) 对称加密密文的PADDING问题 】
      系统对加密后文件进行数字签名 【 数字签名(多种签名工作模式差异) 】
      (可选)文件秒传:服务器上已有的文件,客户端可以不必再重复上传了
第三阶段(7.25-8.5)
  • 基于网页的加密文件下载与解密(20分)
    • 已完成《基于网页的文件上传加密与数字签名系统》所有要求
      提供匿名用户加密后文件和关联的数字签名文件的下载
      • 客户端对下载后的文件进行数字签名验证 【 非对称(公钥)加密 数字签名 】
      • 客户端对下载后的文件可以解密还原到原始文件 【 对称解密 密钥管理 】
      提供已登录用户解密后文件下载
      下载URL设置有效期(限制时间或限制下载次数),过期后禁止访问 【 数字签名 消息认证码 Hash Extension Length Attack Hash算法与HMAC算法的区别与联系 】
      提供静态文件的散列值下载,供下载文件完成后本地校验文件完整性 【 散列算法 】

开发日志

前端开发日志
  • 2022.7.11 创建项目仓库,设置工作区
  • 2022.7.12 创建前端Vue项目,并进行初始化
总文件名称Front-end-project
notion image
完成进度:配置对应的登录初始界面,设置基本路由,前端总体开发采用Vue+Element ui
问题反馈:
  • vue中路由的配置与使用
引入路由:
import VueRouter from 'vue-router'1
路由实例化:
`Vue.use(VueRouter)1`
定义路由中的内容:
let router = new VueRouter({ mode: 'history', routes: [//这里定义路由指向的页面 { path: '/',//默认时指向的页面 根目录 component: IndexPage }, { path: '/detail/analysis', component: DetailPage }, { //假设'/detail'与'/detail/analysis'请求的内容相同时, //不能直接写成以下方式,必须写成重定向方式 //重定向的内容是该组件的真正请求路径 /** { path: '/detail', component: DetailPage } */ path: '/detail', component: DetailPage, redirect: '/detail/analysis', //嵌套子路由 children: [ { path: 'analysis', component: DetailAnaPage }, { path: 'count', component: DetailCouPage }, ] } ] })
将路由注入Vue对象:
new Vue({ el: '#app', router, })
Element UI技术文档:Element UI技术文档

2022.7.13 编写首页登陆界面,确定接口传递数据方式

更新内容:
  • 编写login.vue界面,确定用户向后端传递口令和账号信息用ajax传递
  • 主页面初步呈现效果如下:
notion image
notion image
  • 修改页面的路由配置,初步建立文件上传、文件删除的页面跳转规则
import Vue from 'vue' import VueRouter from 'vue-router' import vuexIndex from '@/store/index.js' const Index = () => import('views/index.vue') const Files = () => import('views/files/Files.vue')
  • 修复登陆跳转bug
  • 登录成功能够进入初始页面
notion image

2022.7.14进行前端文件管理页面优化,添加了多个小组件显示效果,优化了对应的界面显示

  • 编写音乐播放组件MusicPlayer.vue,显示效果如下:
notion image
  • 编写视频播放组件以及上传进度条组件,分别为VideoPlayer.vueProgressDialog.vue
  • 组件使用规范如下:
import VideoPlayer from "components/videoPlayer/VideoPlayer.vue"; import MusicPlayer from "components/musicPlayer/MusicPlayer.vue"; import ProgressDialog from "components/progressDialog/ProgressDialog.vue"; export default {  components: {    AsideBar,    UserInfoCard,    VideoPlayer,    MusicPlayer,    ProgressDialog, },}
  • 编写登录后的系统主页index.vue
  • 系统主页预期实现效果如下:
notion image
  • 编写相册页Albums.vue
notion image
  • 编写收藏夹页面Collectes.vue
notion image
data() {    return {      asideBarData: [       {          path: "/files",          name: "文件",          icon: "wenjian",          componentName: "files",          params: { path: "/root" },       },       { path: "/albums", name: "相册", icon: "xiangce1" },       { path: "/collectes", name: "收藏夹", icon: "favorite" },     ],      downloadFileInfo: {        name: "",        url: "",     },   }; }
  • 页面跳转功能实现:
  • 编写用户管理小组件UserInfoCard.vue,效果如下:
notion image

2022.7.16实现登录注册界面功能,实现文件上传下载

更新内容:
  • 编写后端数据接口,实现用户登录、注册口令存储到本地数据库,数据库编写采用SQLite
//登录    @ApiOperation(value = "登录")    @PostMapping("login")    public R loginUser(@RequestBody UcenterMember member) {        //member对象封装手机号和密码        //调用service方法实现登录        //返回token值,使用jwt生成        String token= memberService.login(member);        UcenterMember mem=memberService.login1(member);        //System.out.println(mem);        return R.ok().data("token", token).data("mem",mem);   }    //注册    @PostMapping("register")    public R registerUser(@RequestBody RegisterVo registerVo) {        memberService.register(registerVo);        return R.ok();   }    //查询用户信息    @ApiOperation(value = "根据用户表id查询用户信息")    @GetMapping("getMemberInfo/{id}")    public R getMemberInfo(@PathVariable String id){        QueryWrapper<UcenterMember> wrapper=new QueryWrapper<>();        wrapper.eq("id",id);        UcenterMember ucenterMember = memberService.getOne(wrapper);        return R.ok().data("member",ucenterMember);   }    //修改用户信息    @ApiOperation(value = "更新用户信息")    @PostMapping("updateMemberInfo")    public R updateMemberInfo(@RequestBody UcenterMember ucenterMember){        String id = ucenterMember.getId();        QueryWrapper<UcenterMember> w=new QueryWrapper<>();        w.eq("id",id);        UcenterMember one = memberService.getOne(w);        UcenterMember member=new UcenterMember();        member.setId(ucenterMember.getId());        member.setNeicun(one.getNeicun());        member.setAvatar(ucenterMember.getAvatar());        member.setNickname(ucenterMember.getNickname());        boolean b = memberService.updateById(member);        if (b){            return R.ok();       }else{            return R.error();       }   } }
  • 实现文件、图片格式上传存储
notion image
notion image
  • 对用户的注册信息口令进行存储
在登录注册主页面,将用户信息存储到对应的接口所对应的数据库中
notion image
问题反馈
对于如何加密用户的存储信息和加解密,以及实现多种方式注册信息查阅了相关资料
  • 此处查阅了Vue框架对于登陆界面的规则文档,此出提供了一个思路,通过引入crypto JS去实现对于信息的存储
    • 示例如下:
    • HTML code
    • <template> <el-form :model="ruleForm"> < H3 class = "title" > system login</h3> <el-form-item prop="mobile"><el-input type="text" v-model=" ruleForm.mobile "Auto complete =" off "placeholder =" account "> < / El input > < / El form item > <el-form-item prop="password"><el-input type="text" v-model=" ruleForm.password "Auto complete =" off "placeholder =" password "> < / El input > < / El form item > < El checkbox V-model = "checked" checked > remember password < / El checkbox > <el-form-item><el-button type="primary" @ click.native.prevent= "Handlesubmit" > login < / El button > < / El form item > </el-form> </template>
    • js code
    • <script> Import cryptojs from 'crypto JS' // encrypt JS export default { data() { return { ruleForm: { Mobile: ', // account number Password: '// password }, Checked: true // check remember password. True is selected }; }, //Judge whether to remember the password //Note that true here is a string format, because Boolean will become string when stored in localstorage created(){ //Judge whether to remember the password if(localStorage.getItem("rememberPsw") == 'true'){ this.getCookie() } }, methods:{ //Login method handleSubmit() { var that = this; let loginParams = { mobile: this.ruleForm.mobile , // get the account number password: this.ruleForm.password //Get password }; if (that.checked == true) { //Incoming account, password, save days that.setCookie(that.ruleForm.mobile, that.ruleForm.password, 1); } else { //Empty cookie that.clearCookie(); } localStorage.setItem("rememberPsw", that.checked); //// login request // that.$axios.post(`${api}/auth/login`,loginParams).then((res)=>{ // if(res.data.errCode == 0){ // console.log ('login succeeded ') // if (that.checked == true) { // //Incoming account, password, save days // that.setCookie(that.ruleForm.mobile, that.ruleForm.password, 7); // } else { // //Empty cookie // that.clearCookie(); // } //// jump route // that.$router.push({ path: '/index' }); // }else{ // console.log ('login failed ') // } // }) }, //Set cookie method setCookie(mobile, password, days) { var text = CryptoJS.AES.encrypt (password, 'secret key 123'); // encrypt with cryptojs method Var savedays = new date(); // get time saveDays.setTime ( saveDays.getTime () + 24 * 60 * 60 * 1000 * days); // number of days to save console.log(saveDays) console.log(saveDays.toGMTString()) //String splicing into cookie // window.document.cookie = "mobile" + "=" + mobile + ";path=/;saveDays=" + saveDays.toGMTString(); // window.document.cookie = "password" + "=" + text + ";path=/;saveDays=" + saveDays.toGMTString(); window.document.cookie = "mobile" + "=" + mobile + ";path=/;expires=" + saveDays.toGMTString(); window.document.cookie = "password" + "=" + text + ";path=/;expires=" + saveDays.toGMTString(); }, //Read cookie getCookie() { if (document.cookie.length > 0) { var arr = document.cookie.split (';'; // the format shown here needs to be cut. You can output it by yourself console.log(arr) for (var i = 0; i < arr.length; i++) { Var arr2 = arr [i]. Split ('='); // cut again //Here, the array with mobile as item 0 and the array with password as item 0 will be cut to determine and find the corresponding value if (arr2[0] == 'mobile') { this.ruleForm.mobile =Arr2 [1]; // get the account number } else if (arr2[0] == 'password') { //Get the encrypted password arr2 [1] and decrypt it var bytes = CryptoJS.AES.decrypt(arr2[1].toString(), 'secret key 123'); var plaintext = bytes.toString ( CryptoJS.enc.Utf8 ); // get the decrypted password (the password entered during login) this.ruleForm.password = plaintext; } } } }, //Clear cookies clearCookie() { this.setCookie (',', 0); // set the account password to blank and the number of days to 0 } } } </script>

2022.7.17优化前端功能页面,修复文件下载、排序、显示问题

更新内容:
  • 修复由于接口回调过程中出现下载中断,导致的文件不能下载问题。
  • 新增根据文件大小进行排序功能。
@Override public List<File> getCurFiles(String dir,String id) { QueryWrapper<File> wrapper=new QueryWrapper<>(); wrapper.eq("f_dir",dir); wrapper.eq("mem_id",id); //File files = baseMapper.selectById(wrapper); List<File> files = baseMapper.selectList(wrapper); return files; } @Override public File getFiles(String id) { QueryWrapper<File> wrapper=new QueryWrapper<>(); wrapper.eq("id",id); File file = baseMapper.selectOne(wrapper); return file; } @Override public List<File> getFindFile(String memid,String name) { QueryWrapper<File> wrapper=new QueryWrapper<>(); wrapper.eq("mem_id",memid); wrapper.like("name",name); List<File> fileList = baseMapper.selectList(wrapper); return fileList; }
notion image

2022.7.19修复文件管理页面在从数据库调取数据中出现的文件不能显示和打开,新增文件列表显示功能

更新内容:
  • 在vue前端调取文件拉去接口中赋予每个文件一个用户id用以区分不同身份用户
  • 成功实现不同身份用户上传数据不会出现重复
// axios拦截器 instance.interceptors.request.use(config => { return config }) if (method && method == 'post') { if (type && type == "params") { if (params) { // return instance.post(url, params) if (header == 'json') { return instance.request({ url, data: params, method: 'post', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, }) } else if (type == 'paramsSerializer') { return instance.request({ url, data: qs.stringify(params, { arrayFormat: 'repeat' }), method: 'post', }) } else { return instance.request({ url, data: params, method: 'post', }) } } else { return instance.post(url) } } else { // resful的形式 if (params) { for (var key in params) { // 拼接url url = url + '/' + params[key]; } } return instance.post(url); } } else if (!method || method == 'get') { if (type == 'resful' || !type) { // resful的形式 if (params) { for (var key in params) { // 拼接url url = url + '/' + params[key]; } } return instance.get(url); } else if (type == 'params') { console.log(params); params = { params: params } return instance.get(url, params) } } else if (method && method == 'put') { if (params) { return instance.put(url, params) } else { return instance.put(url) } } else if (method && method == 'delete') { // resful的形式 if (params) { for (var key in params) { // 拼接url url = url + '/' + params[key]; } } return instance.delete(url); } }

2022.7.21修复前端传递数据到后端中由于文件大小导致的TTL过长而请求中断的问题

方法解析:
原因:
  1. 服务器配置:例如在PHP中默认的文件上传大小为8M【post_max_size = 8m】,若你在一个请求体中放入8M以上的内容时,便会出现异常
  1. 请求超时:当你设置了接口的超时时间为10s,那么上传大文件时,一个接口响应时间超过10s,那么便会被Faild掉。
  1. 网络波动:这个就属于不可控因素,也是较常见的问题。
分片上传
  • 创建切片,循环分解文件即可
createFileChunk(file, size = chunkSize) { const fileChunkList = []; var count = 0; while (count < file.size) { fileChunkList.push({ file: file.slice(count, count + size) }); count += size; } return fileChunkList; }
  • 循环创建切片,既然咱们做的是多文件,所以这里就有循环去处理,依次创建文件切片,及切片的上传。
async handleUpload(resume) { if (!this.container.files) return; this.status = Status.uploading; const filesArr = this.container.files; var tempFilesArr = this.tempFilesArr; for (let i = 0; i < tempFilesArr.length; i++) { fileIndex = i; //创建切片 const fileChunkList = this.createFileChunk( filesArr[tempFilesArr[i].index] ); tempFilesArr[i].fileHash ='xxxx'; // 先不用看这个,后面会讲,占个位置 tempFilesArr[i].chunkList = fileChunkList.map(({ file }, index) => ({ fileHash: tempFilesArr[i].hash, fileName: tempFilesArr[i].name, index, hash: tempFilesArr[i].hash + '-' + index, chunk: file, size: file.size, uploaded: false, progress: 0, // 每个块的上传进度 status: 'wait' // 上传状态,用作进度状态显示 })); //上传切片 await this.uploadChunks(this.tempFilesArr[i]); } }
  • 上传切片,这个里需要考虑的问题较多,也算是核心吧,uploadChunks方法只负责构造传递给后端的数据,核心上传功能放到sendRequest方法中
async uploadChunks(data) { var chunkData = data.chunkList; const requestDataList = chunkData .map(({ fileHash, chunk, fileName, index }) => { const formData = new FormData(); formData.append('md5', fileHash); formData.append('file', chunk); formData.append('fileName', index); // 文件名使用切片的下标 return { formData, index, fileName }; }); try { await this.sendRequest(requestDataList, chunkData); } catch (error) { // 上传有被reject的 this.$message.error('亲 上传失败了,考虑重试下呦' + error); return; } // 合并切片 const isUpload = chunkData.some(item => item.uploaded === false); console.log('created -> isUpload', isUpload); if (isUpload) { alert('存在失败的切片'); } else { // 执行合并 await this.mergeRequest(data); } }
  • sendReques。上传这是最重要的地方,也是容易失败的地方,假设有10个分片,那我们若是直接发10个请求的话,很容易达到浏览器的瓶颈,所以需要对请求进行并发处理。
    • 并发处理:这里我使用for循环控制并发的初始并发数,然后在 handler 函数里调用自己,这样就控制了并发。在handler中,通过数组API.shift模拟队列的效果,来上传切片。
    • 重试: retryArr 数组存储每个切片文件请求的重试次数,做累加。比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次。为保证能与文件做对应,const index = formInfo.index; 我们直接从数据中拿之前定义好的index。 若失败后,将失败的请求重新加入队列即可。
    • // 并发处理 sendRequest(forms, chunkData) { var finished = 0; const total = forms.length; const that = this; const retryArr = []; // 数组存储每个文件hash请求的重试次数,做累加 比如[1,0,2],就是第0个文件切片报错1次,第2个报错2次 return new Promise((resolve, reject) => { const handler = () => { if (forms.length) { // 出栈 const formInfo = forms.shift(); const formData = formInfo.formData; const index = formInfo.index; instance.post('fileChunk', formData, { onUploadProgress: that.createProgresshandler(chunkData[index]), cancelToken: new CancelToken(c => this.cancels.push(c)), timeout: 0 }).then(res => { console.log('handler -> res', res); // 更改状态 chunkData[index].uploaded = true; chunkData[index].status = 'success'; finished++; handler(); }) .catch(e => { // 若暂停,则禁止重试 if (this.status === Status.pause) return; if (typeof retryArr[index] !== 'number') { retryArr[index] = 0; } // 更新状态 chunkData[index].status = 'warning'; // 累加错误次数 retryArr[index]++; // 重试3次 if (retryArr[index] >= this.chunkRetry) { return reject('重试失败', retryArr); } this.tempThreads++; // 释放当前占用的通道 // 将失败的重新加入队列 forms.push(formInfo); handler(); }); } if (finished >= total) { resolve('done'); } }; // 控制并发 for (let i = 0; i < this.tempThreads; i++) { handler(); } }); }
  • 切片的上传进度,通过axios的onUploadProgress事件,结合createProgresshandler方法进行维护
// 切片上传进度 createProgresshandler(item) { return p => { item.progress = parseInt(String((p.loaded / p.total) * 100)); this.fileProgress(); }; }
  • 因此此处使用切片进行项目对于文件的上传和拉取:
// 请求文件列表 async getListData() { if (this.$route.params.path.search("search") !== -1) return; let res = await this.$request( `/educenter/file/getCurDirFiles/${this.$store.state.userInfo.id}`, this.$route.params.path, "post", "params", "json" ); if (res.data.success) { if (this.$store.state.sortType == "size") { res.data.data.files.sort((a, b) => { return a.size - b.size; }); } this.listData = res.data.data.files; await this.getVideoList(res.data.data.files); } else { this.$message.error("获取文件列表失败,请刷新页面重试!"); } }, // 获取文件目录树 async getFolderList(dir) { if (!dir) { let res = await this.$request("/educenter/dir/getUserDir", { id: this.$store.state.userInfo.id, }); if (res.data.data.dir == null) { this.$router.replace("/login"); return; } this.folderList = JSON.parse(res.data.data.dir.memDir); this.$store.commit( "updateFolderList", JSON.parse(res.data.data.dir.memDir) ); this.$store.commit("updateIsGetingFolder", false); } else { this.folderList = JSON.parse(dir); this.$store.commit("updateFolderList", JSON.parse(dir)); this.$store.commit("updateIsGetingFolder", false); } }, // 获取当前文件中的所有video文件 getVideoList(listData) { listData.forEach(async (item, index, arr) => { if (item.filetype === "video") { arr[index].url = await this.getVideoUrl(item.videoId); } }); }, // 根据传入videoId获取url async getVideoUrl(videoId) { let res = await this.$request( "/eduoss/fileoss/getPlayAuth?isList=" + videoId, "", "post" ); return res.data.data.urlList[0].url; }, // 将上传成功的对象push到当前listData中 async pushUploadData(item) { if (item.filetype == "video") { item.url = await this.getVideoUrl(item.videoId); } this.listData.push(item); },

2022.7.22修改文件存储方式,改用直接存储到阿里的OSS中

此前文件的上传和拉去均在本地数据库实现,为了能够更好的实现文件的存储管理,因此我开通了阿里的OSS对象存储服务,直接将文件统一上传到线上。
  • 将文件上传接口和拉去接口联通到开通的OSS上(涉及到保密机制,故此处不给出全部代码)
notion image
  • 写一个公用的ali-oss.js
// 引入ali-oss let OSS = require('ali-oss') /** * [accessKeyId] {String}:通过阿里云控制台创建的AccessKey。 * [accessKeySecret] {String}:通过阿里云控制台创建的AccessSecret。 * [bucket] {String}:通过控制台或PutBucket创建的bucket。 * [region] {String}:bucket所在的区域, 默认oss-cn-hangzhou。 */ let client = new OSS({ region: '<oss region>', secure: true, // secure: 配合region使用,如果指定了secure为true,则使用HTTPS访问 accessKeyId: '<Your accessKeyId>', accessKeySecret: '<Your accessKeySecret>', bucket: '<Your bucket name>' })
  • 调用
/** * 上传文件,大小不能超过30MB * @param {string} ObjName OSS的储存路径和文件名字 * @param {string} fileUrl 本地文件 * @retruns Promise */ export const put = async (ObjName, fileUrl) => { try { let result = await client.put(`${ObjName}`, fileUrl) // ObjName为文件名字,可以只写名字,就直接储存在 bucket 的根路径,如需放在文件夹下面直接在文件名前面加上文件夹名称 return result } catch (e) { console.log(e) } } // 上传成功之后,转换真实的地址 export const signatureUrl= async (ObjName) => { try { let result = await client.signatureUrl(`${ObjName}`) return result } catch (e) { console.log(e) } }
  • 使用Element-UI的 Upload 组件的自定义方法http-request上传,覆盖默认的。
<template> <div class="hello"> <el-upload class="upload-demo" action :http-request="handleUpload" :before-upload="beforeUpload" > <el-button size="small" type="primary">点击上传</el-button> </el-upload> </div> </template> <script> import { put, signatureUrl, getFileNameUUID } from '@/utils/ali-oss' export default { name: 'Upload', }, methods: { beforeUpload(file) { // 限制上传类型 const fileExtensions = getFileName(file.name) === '.doc' || getFileName(file.name) === '.docx' || getFileName(file.name) ==='.pdf' //限制的上限为20M const max20M = file.size / 1024 / 1024 < 20; if (!fileExtensions) { this.$message.error('上传文件类型可以是 .doc, .docx, .pdf 等多种格式'); } if (!max20M) { this.$message.error('上传文件大小不能超过 10MB!'); } return fileExtensions && max20M; }, /** * 自定义上传方法 */ handleUpload(option) { // 获取文件的后缀名 let objName = getFileNameUUID() var obj = option.file.name var index = obj.lastIndexOf("."); obj = obj.substring(index,obj.length); // 生成的文件名,保留文件后缀名,进行拼接 let objName = getFileNameUUID() + getFileName(option.file.name) // 调用 ali-oss 中的方法,flieName是存放的文件夹名称,可自己定义 put(`flieName/${objName}`, option.file).then(res => { console.log(res) // 上传成功之后,转换真实的地址 signatureUrl(`flieName/${objName}`).then(res => { console.log(res) }) }) } } } </script>
notion image

2022.7.24优化文件管理功能增加文件移动、重命名、属性查看功能

更新内容:
  • 新增文件拖拽移动功能,修复文件移动时造成的获取数据失败Bug
notion image
  • 调用ali-oss的数据接口,获取已上传的文件属性内容
    • 用于当前 Vue 实例的初始化选项。需要在选项中包含自定义属性时会有用处。
      $option 是用来获取data外面的数据和方法。
      this.$options 即可以获取自定义属性,也可以增加自定义属性,而且,获取自定义属性的方法有两种。
      this.mydata = this.$options['myoption'] this.mydata1 = this.$options.myoption
      <template> <div> <h2>Vue的实例属性 $options</h2> <button type="button" @click="handleClick">点击获取值</button> <p>data外部变量 {{ $options.obj.name }}---{{ $options.obj.age }}</p> <p v-if="mydata">data数据 {{ mydata.name }}---{{ mydata.age }}</p> </div> </template> <script> export default { // 在data外面定义的属性和方法通过$options可以获取和调用 obj: { name: "muzidigbig", age: 22, }, say() { // this 指向当前实例对象 // console.log(this); console.log("我是 data 外部方法"); }, data() { return { mydata: null, }; }, created() { // 调用 data 外部方法 this.$options.say(); console.log(this, this.$options); console.log(this.$options.obj); }, methods: { handleClick() { // 修改 data 外部 obj 对象的数据,视图也会改变 this.$options.obj["age"] = 23; // 将 obj 赋值给 mydata this.mydata = this.$options["obj"]; }, }, }; </script>
notion image

2022.7.26新增文件查找功能

这里涉及到在vue中定义和调用函数,需要用到关键字methods,然后便可以在里面定义函数了。
这里关键点:
  • document.getElementById('open').files[0].path,这里获取文件路径的方法是获取文件类元素的数组,然后通过path关键字获取文件的绝对路径。
// js递归遍历树形json数据,根据关键字查找节点 //@leafId 查找的id, //@nodes 原始Json数据 //@path 供递归使用 findPathByLeafId(leafId, nodes, path) { if (path === undefined) { path = []; } for (var i = 0; i < nodes.length; i++) { var tmpPath = path.concat(); tmpPath.push(nodes[i].name); if (leafId == nodes[i].name) { return tmpPath; } if (nodes[i].childrenList) { var findResult = this.findPathByLeafId( leafId, nodes[i].childrenList, tmpPath ); if (findResult) { return findResult; } } } }, // 打开当前双击的文件夹 // 点击的是folderList中第 index个子目录 openCurrentFolder(item) { console.log("打开文件", item); let currentFolder = ( "/" + this.findPathByLeafId(item.name, [this.folderList]).join("") ).slice( 0, this.findPathByLeafId(item.name, [this.folderList]).join("").length ); // let currentFolder = // this.$route.params.path + // "/" + // item.name.substr(0, item.name.length - 1); // // 在vuex中更新当前目录 // this.$store.commit("updateCurrentFolder", currentFolder); this.$router.push({ name: "files", params: { path: currentFolder } }); }, // 判断当前所在的文件夹位置 getCurrentLocation() { if (this.$route.params.path.search("search") != -1) return; let currentFolder = this.$route.params.path; currentFolder = currentFolder.slice(1, currentFolder.length); let arr = currentFolder.split("/"); // 如果是/search就不计算当前位置了 // if (arr[arr.length - 1] == "search") { // return; // } this.currentChildrenFolder = this.folderList.childrenList; if (arr.length > 1) { // 说明不在根目录 for (var i = 1; i < arr.length; i++) { let index = this.currentChildrenFolder.findIndex( (item) => item.name.substr(0, item.name.length - 1) == arr[i] ); this.currentFolderId = this.currentChildrenFolder[index].id; this.currentChildrenFolder = this.currentChildrenFolder[ index ].childrenList; } } else { this.currentFolderId = 1; } },
notion image
notion image

2022.7.28用户登录界面对用户口令进行限制和密码强度检测

用户名和口令
限用户输入一些非常容易被破解的口令。如什么qwert,123456,password之类,就像twitter限制用户的口令一样做一个口令的黑名单。另外,可以限制用户口令的长度,是否有大小写,是否有数字,可以用程序做一下校验。当然,这可能会让用户感到很不爽,所以,现在很多网站都提供了UX让用户知道他的口令强度是什么样的这样可以让用户有一个选择,目的就是告诉用户——要想安全,先把口令设得好一点。
基于这种想法,我对于用户在注册时的口令进行了密码强度检验,对密码长度和复杂程度都进行了一个展示,通过下方出现的颜色字条来标识密码强度
例:
  • 当密码为:123456789时,强度颜色条为红色
notion image
  • 当密码为:xu123456789时,强度颜色条为蓝色
notion image
  • 当密码为xu123456789-.(此处设定-、_、.等合法字符集范围之外的字符不允许使用)
notion image
此外,在存储用户口令数据到数据库时,采用了md5方式进行了加密
这里有两种方式可供参考
直接在需要使用md5加密的页面引入
import md5 from 'js-md5';
然后将想要加密的数据放入
let a = md5("111111s"); console.log('a', a); //结果为c85dfcf2cf8e79ba8239eff965483c5b
全局挂载,将js-md5添加到vue原型链上
//在vue项目的mian.js文件中,引入js-md5并挂载原型 import md5 from 'js-md5'; Vue.prototype.$md5 = md5;
然后将想要加密的数据放入
let a = this.$md5("111111s"); console.log('a', a); //结果为c85dfcf2cf8e79ba8239eff965483c5b
在项目中,想要将用户注册的密码由明文转为密文,在传递参数时可以先将密码md5加密后,再传给后端接口,放到数据库中。这样数据库中密码存放的就是密文而不是明文了。
// 收集参数 以便发送给后台 let params = { user: this.addForm.name, password: md5(this.addForm.pass) //此处将用户注册密码加密,再发给后端 };
在用户注册成功后进行登录时,因为js-md5加密是不可逆的,除非进行暴力破解,例如枚举,所以不需要将数据库中存储的密码密文再转为明文,而是在传递登录密码时将用户输入的密码进行md5加密处理,再与之对比验证。
notion image

2022.7.30新增文件加密和散列值获取功能界面

更新内容:
  • 新增文件加密功能按钮选项,并与后端接口联通,成功实现数据加密
notion image
  • 计算文件的散列值,并进行逐比特对比,判断文件是否遭到篡改
1.先下载
npm i browser-md5-file -S
2.在使用的vue页面引入和声明方法
import BMF from 'browser-md5-file' const bmf = new BMF()
3.使用方法:
function handle(e) { const file = e.target.files[0]; bmf.md5( file, (err, md5) => { console.log('err:', err); console.log('md5 string:', md5); // 97027eb624f85892c69c4bcec8ab0f11 }, progress => { console.log('progress number:', progress); }, ); }
4.终止md5计算方法(大文件计算很费时 ):
bmf.abort()
5.代码可以直接复制使用:
<template> <div> <!-- 上传文件 --> <!-- multiple 选择多个文件属性 --> <input type="file" placeholder-class="input-placeholder" @change="handle"> </div> </template> <script> // 先下载 npm i browser-md5-file -S import BMF from 'browser-md5-file' const bmf = new BMF() export default { data () { return {} }, methods: { handle (e) { // console.log(e) const file = e.target.files[0] // console.log(file) bmf.md5( file, (err, md5) => { console.log('err:', err) console.log('md5 string:', md5) // 97027eb624f85892c69c4bcec8ab0f11 // const formData = new FormData() // 每次失败要重置formdata对象 // formData.append('file', e.target.files[0])//文件掉接口方式 // You can abort it before success to md5:中止计算md5--大文件计算很费时 // bmf.abort() }, progress => { console.log('progress number:', progress) } ) } } } </script> <style></style>
notion image

2022.7.31修复界面显示出错、文件数据获取等并发性错误

后端开发日志

2022.7.11 创建项目仓库,设置工作区

2022.7.12创建项目初始化框架,生成demo文件

notion image
  • 项目结构介绍
notion image
如上图所示,Spring Boot 项目结构如下
  • src/main/java 主程序入口和项目开发
  • src/resources 项目配置文件
  • src/test/java 测试程序
此外,建议在包名下分别新建 controller、domain、service、mapper,这些分别表示
  • controller:页面访问控制,也就是 api,
  • domain:主要用于实体类和数据访问层(mapper)
  • service:业务处理
  • SpringBootProjctApplication.java:主程序,创建项目时会自动创建,一般为项目名称+Application.java
最后,启动 SpringBootProjctApplication 主程序。这样就完成 Java 项目配置了。
注:controller、domain、service、mapper 包,需要放在 SpringBootProjctApplication.java 主程序同包名或放在主程序下,否则主程序会扫描不到,导致报错。
  • Web 模块
在配置之前,先说明一下 pom.xml 文件。此文件包含 Spring Boot 版本、项目基本信息、第三方 Jar 包 Maven 引用。
所以,我们引用 Web 模块时,需要在 pom.xml 的 dependencies 添加以下代码:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
其中,pom.xml 有两个默认的 模块
  • spring-boot-starter:Spring Boot的核心启动器,包含了自动配置、日志和YAML。
  • spring-boot-starter-test:支持常规的测试依赖,包括JUnit、Hamcrest、Mockito以及spring-test模块。
编写 Controller api 内容
在 controller 包下新建 TestController.java,内容如下:
@RestController public class TestController { @GetMapping("/test") public String test() { return "你好,我是 Spring Boot 项目测试!"; } }
这时,我们启动主程序,在浏览器输入localhost:8080/test,就可以看到返回的结果。是不是很简单,只需要很少的配置,就可以创建一个 Java Web 项目了。
notion image

2022.7.15建立数据库,设置好对应的表单名称、数据属性

  • 建立数据库的Sql
    /* Navicat Premium Data Transfer Source Server : 11 Source Server Type : MySQL Source Server Version : 50650 Source Host : 47.106.217.172:3306 Source Schema : space Target Server Type : MySQL Target Server Version : 50650 File Encoding : 65001 Date: 19/07/2021 17:39:25 */SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for file -- ---------------------------- DROP TABLE IF EXISTS `file`; CREATE TABLE `file` ( `id` char(19) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `url` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址', `mem_id` char(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户ID', `name` char(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名字', `type` char(19) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件类型', `gmt_create` datetime(0) NOT NULL COMMENT '创建时间', `gmt_modified` datetime(0) NOT NULL COMMENT '更新时间', `collection` tinyint(9) NULL DEFAULT 0 COMMENT '是否收藏', `f_dir` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '/root', `filetype` char(19) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'file', `video_id` char(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `size` bigint(100) NULL DEFAULT NULL COMMENT '文件大小', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Table structure for ucenter_member -- ---------------------------- DROP TABLE IF EXISTS `ucenter_member`; CREATE TABLE `ucenter_member` ( `id` char(19) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '会员id', `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', `gmt_create` datetime(0) NOT NULL COMMENT '创建时间', `gmt_modified` datetime(0) NOT NULL COMMENT '更新时间', `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', `neicun` bigint(100) NULL DEFAULT NULL COMMENT '内存', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员表' ROW_FORMAT = Compact; -- ---------------------------- -- Table structure for user_dir -- ---------------------------- DROP TABLE IF EXISTS `user_dir`; CREATE TABLE `user_dir` ( `mem_id` char(19) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `mem_dir` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户目录结构', PRIMARY KEY (`mem_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Triggers structure for table ucenter_member -- ---------------------------- DROP TRIGGER IF EXISTS `addUserDir`; delimiter ;; CREATE DEFINER = `space`@`%` TRIGGER `addUserDir` AFTER INSERT ON `ucenter_member` FOR EACH ROW BEGIN insert into user_dir values(NEW.id,'{"childrenList":[],"id":1,"name":"root/","parentId":0}'); END ;; delimiter ; -- ---------------------------- -- Triggers structure for table ucenter_member -- ---------------------------- DROP TRIGGER IF EXISTS `delUserDir`; delimiter ;; CREATE DEFINER = `space`@`%` TRIGGER `delUserDir` AFTER DELETE ON `ucenter_member` FOR EACH ROW BEGIN DELETE from user_dir WHERE mem_id =OLD.id; END ;; delimiter ; SET FOREIGN_KEY_CHECKS = 1;
    • 初始化数据库内容:
    notion image

    2022.7.16编写对应的数据接口,测试接口功能是否正常

    try { // 创建OSS实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); File file1=new File(); //获取上传文件输入流 InputStream inputStream = file.getInputStream(); //获取文件名称 String originalFilename = file.getOriginalFilename(); //获取文件类型 String fileType = originalFilename.substring(originalFilename.lastIndexOf(".")); String name=originalFilename.substring(0, originalFilename.indexOf(".")); String type = fileType.substring(1); if(type.equals("bmp")||type.equals("mp3")||type.equals("jpg")||type.equals("jpeg")||type.equals("png")||type.equals("mp3")||type.equals("tif")||type.equals("gif") ||type.equals("pcx")||type.equals("tga")||type.equals("exif")||type.equals("fpx")||type.equals("svg")||type.equals("psd")||type.equals("cdr") ||type.equals("pcd")||type.equals("dxf")||type.equals("ufo")||type.equals("eps")||type.equals("ai")||type.equals("raw") ||type.equals("WMF")||type.equals("webp")||type.equals("mp3")||type.equals("avif")){ file1.setFiletype("image"); } //2 把文件按照日期进行分类 //获取当前日期 // 2019/11/12 String datePath = new DateTime().toString("yyyy/MM/dd"); //拼接 // 2019/11/12/ewtqr313401.jpg originalFilename = datePath + "/" + originalFilename; //调用oss方法实现上传 //第一个参数 Bucket名称 //第二个参数 上传到oss文件路径和文件名称 aa/bb/1.jpg //第三个参数 上传文件输入流 ossClient.putObject(bucketName, originalFilename, inputStream); // 关闭OSSClient。 ossClient.shutdown(); //把上传之后文件路径返回 //需要把上传到阿里云oss路径手动拼接出来 // https://edu-guli-1010.oss-cn-beijing.aliyuncs.com/01.jpg String url = "https://" + bucketName + "." + endpoint + "/" + originalFilename; file1.setName(name); file1.setType(type); file1.setUrl(url); file1.setFDir(catalogue); file1.setSize(file.getSize()); return file1; } catch (Exception e) { e.printStackTrace(); return null; } }

    2022.7.19修复文件上传接口Bug,新增文件状态值检验

    public File upload(MultipartFile file,String catalogue) { // 工具类获取值 String endpoint = ConstanPropertiesUtils.END_POIND; String accessKeyId = ConstanPropertiesUtils.ACCESS_KEY_ID; String accessKeySecret = ConstanPropertiesUtils.ACCESS_KEY_SECRET; String bucketName = ConstanPropertiesUtils.BUCKET_NAME; try { // 创建OSS实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); File file1=new File(); //获取上传文件输入流 InputStream inputStream = file.getInputStream(); //获取文件名称 String originalFilename = file.getOriginalFilename(); //获取文件类型 String fileType = originalFilename.substring(originalFilename.lastIndexOf(".")); String name=originalFilename.substring(0, originalFilename.indexOf(".")); String type = fileType.substring(1); if(type.equals("bmp")||type.equals("mp3")||type.equals("jpg")||type.equals("jpeg")||type.equals("png")||type.equals("mp3")||type.equals("tif")||type.equals("gif") ||type.equals("pcx")||type.equals("tga")||type.equals("exif")||type.equals("fpx")||type.equals("svg")||type.equals("psd")||type.equals("cdr") ||type.equals("pcd")||type.equals("dxf")||type.equals("ufo")||type.equals("eps")||type.equals("ai")||type.equals("raw") ||type.equals("WMF")||type.equals("webp")||type.equals("mp3")||type.equals("avif")){ file1.setFiletype("image"); } //2 把文件按照日期进行分类 //获取当前日期 // 2019/11/12 String datePath = new DateTime().toString("yyyy/MM/dd"); //拼接 // 2019/11/12/ewtqr313401.jpg originalFilename = datePath + "/" + originalFilename; //调用oss方法实现上传 //第一个参数 Bucket名称 //第二个参数 上传到oss文件路径和文件名称 aa/bb/1.jpg //第三个参数 上传文件输入流 ossClient.putObject(bucketName, originalFilename, inputStream); // 关闭OSSClient。 ossClient.shutdown();

    2022.7.20实现对用户口令进行加密和强度检验

    消息摘要(数据的指纹)
    定义
    • 对不固定的消息(字符串,一段文本,一个文件),通过一种特定的算法,得到一个固定长度的文本,固定长度的文本叫做消息摘要
    • 比如我是程序员经过特定的算法之后,得到了消息摘要为:adaf02515dfds7885csdfcdsc
    作用
    • 数据完整性的检验技术,我们将文本转换为消息摘要,然后比较消息摘要的值是否相等,如果相等那么表示两种文本相同
    特性
    • 不可逆的,不能从消息摘要再得到原来的文本
    特定的算法
    1. MD5
    1. SHA
    实现步骤
    • 添加依赖jar包
      • commons-codec
    <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.10</version> </dependency>
    • 测试MD5Hex
      • 得到的是32位的16进制的字符串
    @Test public void test1(){ String str1="你们好,未来的程序员!"; String strMessageString=DigestUtils.md5Hex(str1); System.out.println(strMessageString); } //读取文件 @Test public void test2() throws IOException{ InputStream inputStream=new FileInputStream(new File("/home/chenjiabing/Documents/Blog/AOP.md")); String message=DigestUtils.md5Hex(inputStream); System.out.println(message); }
    密码加密
    • 避免在数据库中明文保存密码,通过消息摘要技术对密码进行加密
    明文
    • 没有加密的文字(字符串),能看懂的文字
    密文
    • 经过加密后的文字(字符串),看不出来明文的意思
    加盐处理 salt
    • 为了提高密码的安全性
    • 就是在用户的密码之后随便添加一个字符串,然后连接在一起生成摘要,那么即使获取摘要,也不会被破解。
    @Test public void test3(){ String str1="123456"; String salt="这个是加盐处理"; //需要加盐,随便定义一个字符串 String message=DigestUtils.md5Hex(str1+salt); //获取加盐之后的消息摘要 System.out.println(message); }
    实现
    • 涉及到密码: 登录,注册,修改密码
    • 创建一个MD5Password工具类,用于加密密码
    /** * 密码加密的类 * @author chenjiabing */ public class MD5Password { private final static String SALT="中传放心传"; //加盐处理 /** * 获取加密之后的密码 * @param password 用户输入的密码 * @return 加密之后的密码 */ public static String getMd5Password(String password){ return DigestUtils.md5Hex(password+SALT); //使用了加盐处理 } }
    • 在注册的时候对输入的密码进行加密存储到数据库中
    /** * 注册 * 1. 调用selectUserByUserName(User user)方法判断用户名是否存在,返回对象u * 2. 判断u是否为null, * 3. 如果为null,调用insertUser(user)方法添加 * 4. 如果不为null,抛出异常提示controller用户名存在(UserNameAlreadyExistException) */ public void register(User user) throws UserNameAlreadyExistException { User u=userMapper.selectUserByUserName(user.getUsername()); //调用usermapper中的方法 if (u!=null) { //如果u不为null,表示用户名已经存在与数据库中,不可以再次注册了,因此抛出异常 throw new UserNameAlreadyExistException("用户名已经存在,请重新输入!!!"); }else { //如果u==null,表示用户名不存在,可以添加 //获取加密之后的密码 String md5Password=MD5Password.getMd5Password(user.getPassword()); //将加密之后的密码设置到user中,保存到数据库中 user.setPassword(md5Password); userMapper.insertUser(user); //直接调用持久层方法插入数据即可 } }
    • 在登录的时候,将用户输入的密码进行加密获取到加密之后的密码,然后和数据库中的密码比较
    /** * 登录方法 * 1. 通过selectUserByUserName返回user对象 * 2.判断user是否为null * 3.如果user=null,抛出UserNotFoundException异常 * 4.如果user!=null,那么验证其中的密码是否正确 * 5.如果密码不匹配,抛出PassWordNotMatchException异常 * 6. 如果密码匹配,那么返回user对象 * @throws UserNotFoundException * @throws PassWordNotMatchException */ public User login(String userName, String passWord) throws UserNotFoundException, PassWordNotMatchException { User user=userMapper.selectUserByUserName(userName); //根据用户名查询,返回user对象 if (user==null) { //user为null,表示用户名不存在 throw new UserNotFoundException("用户名不存在"); }else { //如果用户名存在,验证密码 //获取加密之后的密码,实际是一个消息摘要 String md5Password=MD5Password.getMd5Password(passWord); //使用加密之后获取的消息摘要和数据库中对应的密码比较 if (md5Password.equals(user.getPassword())) { //如果密码匹配 return user; //返回user对象即可 }else { //如果密码不相同,那么直接抛出密码不匹配的异常即可 throw new PassWordNotMatchException("密码不匹配"); } } }
    • 在修改中,将旧密码加密后和数据库中的密码比较,并且将新密码加密更新到数据库中
    /** * 修改密码 * 1. 根据id查询用户信息,返回user * 2. 如果user=null,抛出用户不存在的异常 * 3. 如果user!=null,比较user中的密码和用户输入的旧密码oldPassword是否相同 * 4. 如果密码不相同,抛出密码不匹配的异常 * 5. 如果密码相同,表示用户输入的旧密码是正确的,那么更新密码即可 */ public void updatePassword(Integer id, String oldPassword, String newPassword) throws UserNotFoundException, PassWordNotMatchException { User user=userMapper.seletUserById(id); //根据id查询,返回user对象 if (user==null) { //如果用户不存在 throw new UserNotFoundException("当前登录的用户不存在"); //抛出用户不存在的异常 }else { //如果当前登录的用户存在 //获取旧密码的加密之后的密码 String oldMd5Password=MD5Password.getMd5Password(oldPassword); //使用加密之后的密码和数据库中的密码比较 if (!user.getPassword().equals(oldMd5Password)) { //如果返回的user对象中的密码和用户输入的旧密码不匹配 throw new PassWordNotMatchException("输入的旧密码不匹配"); }else { //如果输出的旧密码正确 User u1=new User(); //创建User对象,封装修改所需的参数 //获取加密之后的新密码 String newMd5Password=MD5Password.getMd5Password(newPassword); u1.setPassword(newMd5Password); //封装新密码,其中是加密之后的密码 u1.setId(id); //封装id userMapper.update(u1); //调用修改的方法 } } }

    2022.7.22新增文件接口参数加密,调整文件存储状态的返回值

    我们先来定义一个加密工具类备用,加密这块有多种方案可以选择,对称加密、非对称加密,其中对称加密又可以使用 AES、DES、3DES 等不同算法,这里我们使用 Java 自带的 Cipher 来实现对称加密,使用 AES 算法:
    public class AESUtils { private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding"; // 获取 cipher private static Cipher getCipher(byte[] key, int model) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance(AES_ALGORITHM); cipher.init(model, secretKeySpec); return cipher; } // AES加密 public static String encrypt(byte[] data, byte[] key) throws Exception { Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE); return Base64.getEncoder().encodeToString(cipher.doFinal(data)); } // AES解密 public static byte[] decrypt(byte[] data, byte[] key) throws Exception { Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE); return cipher.doFinal(Base64.getDecoder().decode(data)); } }
    这个工具类比较简单,不需要多解释。需要说明的是,加密后的数据可能不具备可读性,因此我们一般需要对加密后的数据再使用 Base64 算法进行编码,获取可读字符串。换言之,上面的 AES 加密方法的返回值是一个 Base64 编码之后的字符串,AES 解密方法的参数也是一个 Base64 编码之后的字符串,先对该字符串进行解码,然后再解密。
    public class RespBean { private Integer status; private String msg; private Object obj; public static RespBean build() { return new RespBean(); } public static RespBean ok(String msg) { return new RespBean(200, msg, null); } public static RespBean ok(String msg, Object obj) { return new RespBean(200, msg, obj); } public static RespBean error(String msg) { return new RespBean(500, msg, null); } public static RespBean error(String msg, Object obj) { return new RespBean(500, msg, obj); } private RespBean() { } private RespBean(Integer status, String msg, Object obj) { this.status = status; this.msg = msg; this.obj = obj; } public Integer getStatus() { return status; } public RespBean setStatus(Integer status) { this.status = status; return this; } public String getMsg() { return msg; } public RespBean setMsg(String msg) { this.msg = msg; return this; } public Object getObj() { return obj; } public RespBean setObj(Object obj) { this.obj = obj; return this; } }
    接下来我们定义两个注解 @Decrypt 和 @Encrypt
    @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.PARAMETER}) public @interface Decrypt { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Encrypt { }
    这两个注解就是两个标记,在以后使用的过程中,哪个接口方法添加了 @Encrypt 注解就对哪个接口的数据加密返回,哪个接口/参数添加了 @Decrypt 注解就对哪个接口/参数进行解密。这个定义也比较简单,没啥好说的,需要注意的是 @Decrypt 比 @Encrypt 多了一个使用场景就是 @Decrypt 可以用在参数上。
    考虑到用户可能会自己配置加密的 key,因此我们再来定义一个 EncryptProperties 类来读取用户配置的 key:
    @ConfigurationProperties(prefix = "spring.encrypt") public class EncryptProperties { private final static String DEFAULT_KEY = "www.itboyhub.com"; private String key = DEFAULT_KEY; public String getKey() { return key; } public void setKey(String key) { this.key = key; } }
    另外还有一点需要注意,ResponseBodyAdvice 在你使用了 @ResponseBody 注解的时候才会生效,RequestBodyAdvice 在你使用了 @RequestBody 注解的时候才会生效,换言之,前后端都是 JSON 交互的时候,这两个才有用。不过一般来说接口加解密的场景也都是前后端分离的时候才可能有的事。
    先来看接口加密:
    @EnableConfigurationProperties(EncryptProperties.class) @ControllerAdvice public class EncryptResponse implements ResponseBodyAdvice<RespBean> { private ObjectMapper om = new ObjectMapper(); @Autowired EncryptProperties encryptProperties; @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return returnType.hasMethodAnnotation(Encrypt.class); } @Override public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { byte[] keyBytes = encryptProperties.getKey().getBytes(); try { if (body.getMsg()!=null) { body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes)); } if (body.getObj() != null) { body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes)); } } catch (Exception e) { e.printStackTrace(); } return body; } }
    我们自定义 EncryptResponse 类实现 ResponseBodyAdvice接口,泛型表示接口的返回类型,这里一共要实现两个方法:
    1. supports:这个方法用来判断什么样的接口需要加密,参数 returnType 表示返回类型,我们这里的判断逻辑就是方法是否含有 @Encrypt 注解,如果有,表示该接口需要加密处理,如果没有,表示该接口不需要加密处理。
    1. beforeBodyWrite:这个方法会在数据响应之前执行,也就是我们先对响应数据进行二次处理,处理完成后,才会转成 json 返回。我们这里的处理方式很简单,RespBean 中的 status 是状态码就不用加密了,另外两个字段重新加密后重新设置值即可。
    1. 另外需要注意,自定义的 ResponseBodyAdvice 需要用 @ControllerAdvice 注解来标记。
    再来看接口解密:
    @EnableConfigurationProperties(EncryptProperties.class) @ControllerAdvice public class DecryptRequest extends RequestBodyAdviceAdapter { @Autowired EncryptProperties encryptProperties; @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class); } @Override public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { byte[] body = new byte[inputMessage.getBody().available()]; inputMessage.getBody().read(body); try { byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes()); final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt); return new HttpInputMessage() { @Override public InputStream getBody() throws IOException { return bais; } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); } }; } catch (Exception e) { e.printStackTrace(); } return super.beforeBodyRead(inputMessage, parameter, targetType, converterType); } }
    1. supports:该方法用来判断哪些接口需要处理接口解密,我们这里的判断逻辑是方法上或者参数上含有 @Decrypt 注解的接口,处理解密问题。
    1. beforeBodyRead:这个方法会在参数转换成具体的对象之前执行,我们先从流中加载到数据,然后对数据进行解密,解密完成后再重新构造 HttpInputMessage 对象返回。
    接下来,我们再来定义一个自动化配置类,如下:
    @Configuration @ComponentScan("org.javaboy.encrypt.starter") public class EncryptAutoConfiguration { }
    最后,resources 目录下定义 META-INF,然后再定义 spring.factories 文件,内容如下:
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.javaboy.encrypt.starter.autoconfig.EncryptAutoConfiguration

    2022.7.26创建文件删除、修改接口并实现功能,修复用户登录接口由于用户ID重复造成的状态回显异常

    • 文件删除、修改接口
    @ApiOperation(value = "根据传入用户memid、目录路径url、和父文件夹id的删除文件夹") @PostMapping("deleteDirStruct/{memid}/{id}") public R deleteDirStruct(@PathVariable String memid, @PathVariable long id,@RequestBody String url) { UserDir userDir = userDirService.getUserDir(memid); TreeNode treeNode = JSON.parseObject(userDir.getMemDir(), new TypeReference<TreeNode>() { }); boolean i=userDirService.deleteStruct(memid,url); System.out.println(i); if (i){ StringBuffer sb=new StringBuffer(); delete(treeNode, id,sb); //System.out.println(sb); String s = JSONObject.toJSONString(treeNode); userDir.setMemDir(s); userDirService.setUserDir(userDir); return R.ok(); }else { return R.error(); } } public static void insert(TreeNode treeNode, long id, TreeNode newNode) { List<TreeNode> list = treeNode.getChildrenList(); List arrayList=new ArrayList(); for (int i=0;i<list.size();i++){ arrayList.add(list.get(i).getName()); } arrayList.add(newNode.getName()); System.out.println(arrayList); HashSet set=new HashSet<>(arrayList); Boolean result=set.size()==arrayList.size()?true:false; if (result==false){ System.out.println("名字不可重复"); throw new SpaceException(20001, "名字不可重复"); } if (id==treeNode.getId()) { treeNode.getChildrenList().add(newNode); return; } //System.out.println(list.size()); if (list == null || list.isEmpty()) { return; //若该结点 的子结点集合为空 返回 } else { for (int i = 0; i < list.size(); i++) { if (result==true){ insert(list.get(i), id, newNode); } } } }
    • 用户登录接口查询检查
    //登录 @ApiOperation(value = "登录") @PostMapping("login") public R loginUser(@RequestBody UcenterMember member) { //member对象封装手机号和密码 //调用service方法实现登录 //返回token值,使用jwt生成 String token= memberService.login(member); UcenterMember mem=memberService.login1(member); //System.out.println(mem); return R.ok().data("token", token).data("mem",mem); } //注册 @PostMapping("register") public R registerUser(@RequestBody RegisterVo registerVo) { memberService.register(registerVo); return R.ok(); } //查询用户信息 @ApiOperation(value = "根据用户表id查询用户信息") @GetMapping("getMemberInfo/{id}") public R getMemberInfo(@PathVariable String id){ QueryWrapper<UcenterMember> wrapper=new QueryWrapper<>(); wrapper.eq("id",id); UcenterMember ucenterMember = memberService.getOne(wrapper); return R.ok().data("member",ucenterMember); } //修改用户信息 @ApiOperation(value = "更新用户信息") @PostMapping("updateMemberInfo") public R updateMemberInfo(@RequestBody UcenterMember ucenterMember){ String id = ucenterMember.getId(); QueryWrapper<UcenterMember> w=new QueryWrapper<>(); w.eq("id",id); UcenterMember one = memberService.getOne(w); UcenterMember member=new UcenterMember(); member.setId(ucenterMember.getId()); member.setNeicun(one.getNeicun()); member.setAvatar(ucenterMember.getAvatar()); member.setNickname(ucenterMember.getNickname()); boolean b = memberService.updateById(member); if (b){ return R.ok(); }else{ return R.error(); } } }

    2022.7.30修改文件删除中的实现逻辑,使得在OSS的源文件能够备份,而前端不再拉取文件数据

    • 为了能够对用户的上传文件进行身份验证,故不对数据库的源文件进行删除,而是在后端返回的data字段中不返回对应url值,即前端不再拉取对应的文件数据
    public String delete(String id) { String endpoint = ConstanPropertiesUtils.END_POIND; String accessKeyId = ConstanPropertiesUtils.ACCESS_KEY_ID; String accessKeySecret = ConstanPropertiesUtils.ACCESS_KEY_SECRET; String bucketName = ConstanPropertiesUtils.BUCKET_NAME; // 日期目录 // 注意,这里虽然写成这种固定获取日期目录的形式,逻辑上确实存在问题,但是实际上,filePath的日期目录应该是从数据库查询的 QueryWrapper<File> wrapper=new QueryWrapper<>(); wrapper.eq("id",id); File fileServiceOne = fileService.getOne(wrapper); String name = fileServiceOne.getName(); //System.out.println(fileServiceOne); boolean remove = fileService.remove(wrapper); if (remove==true){ System.out.println("删除成功"); } else{ System.out.println("删除失败"); } SimpleDateFormat data = new SimpleDateFormat("yyyy/MM/dd"); Date gmtCreate = fileServiceOne.getGmtCreate(); data.format(gmtCreate); //String filePath = new DateTime().toString("yyyy/MM/dd"); String filePath = data.toString(); //System.out.println(filePath); try { /** * 注意:在实际项目中,不需要删除OSS文件服务器中的文件, * 只需要删除数据库存储的文件路径即可! */ // 建议在方法中创建OSSClient 而不是使用@Bean注入,不然容易出现Connection pool shut down OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret); // 根据BucketName,filetName删除文件 // 删除目录中的文件,如果是最后一个文件fileoath目录会被删除。 String fileKey =filePath + "/" + name; ossClient.deleteObject(bucketName, fileKey); try { } finally { ossClient.shutdown(); } System.out.println("文件删除!"); return "success"; } catch (Exception e) { e.printStackTrace(); return "error"; } }

    2022.7.31实现对文件按大小、时间日期排序,优化显示效果

    @Override public File upload(MultipartFile file,String catalogue) { // 工具类获取值 String endpoint = ConstanPropertiesUtils.END_POIND; String accessKeyId = ConstanPropertiesUtils.ACCESS_KEY_ID; String accessKeySecret = ConstanPropertiesUtils.ACCESS_KEY_SECRET; String bucketName = ConstanPropertiesUtils.BUCKET_NAME; try { // 创建OSS实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); File file1=new File(); //获取上传文件输入流 InputStream inputStream = file.getInputStream(); //获取文件名称 String originalFilename = file.getOriginalFilename(); //获取文件类型 String fileType = originalFilename.substring(originalFilename.lastIndexOf(".")); String name=originalFilename.substring(0, originalFilename.indexOf(".")); String type = fileType.substring(1); if(type.equals("bmp")||type.equals("mp3")||type.equals("jpg")||type.equals("jpeg")||type.equals("png")||type.equals("mp3")||type.equals("tif")||type.equals("gif") ||type.equals("pcx")||type.equals("tga")||type.equals("exif")||type.equals("fpx")||type.equals("svg")||type.equals("psd")||type.equals("cdr") ||type.equals("pcd")||type.equals("dxf")||type.equals("ufo")||type.equals("eps")||type.equals("ai")||type.equals("raw") ||type.equals("WMF")||type.equals("webp")||type.equals("mp3")||type.equals("avif")){ file1.setFiletype("image"); } //2 把文件按照日期进行分类 //获取当前日期 // 2019/11/12 String datePath = new DateTime().toString("yyyy/MM/dd"); //拼接 // 2019/11/12/ewtqr313401.jpg originalFilename = datePath + "/" + originalFilename; //调用oss方法实现上传 //第一个参数 Bucket名称 //第二个参数 上传到oss文件路径和文件名称 aa/bb/1.jpg //第三个参数 上传文件输入流 ossClient.putObject(bucketName, originalFilename, inputStream); // 关闭OSSClient。 ossClient.shutdown(); //把上传之后文件路径返回 //需要把上传到阿里云oss路径手动拼接出来 // https://edu-guli-1010.oss-cn-beijing.aliyuncs.com/01.jpg String url = "https://" + bucketName + "." + endpoint + "/" + originalFilename; file1.setName(name); file1.setType(type); file1.setUrl(url); file1.setFDir(catalogue); file1.setSize(file.getSize()); return file1; } catch (Exception e) { e.printStackTrace(); return null; } }