当前位置: 首页 > news >正文

SpringSecurity+vue通用权限系统

SpringSecurity+vue通用权限系统

采用主流的技术栈实现,Mysql数据库,SpringBoot2+Mybatis Plus后端,redis缓存,安全框架
SpringSecurity ,Vue3.2+Element Plus实现后台管理。基于JWT技术实现前后端分离。项目开发同时采
用MybatisX插件生成代码,提高开发效率。

基于SpringSecurity实现了 登录验证鉴权功能,用户管理,角色管理,权限管理。

后端框架搭建

pom.xml初始化

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.2.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.java</groupId><artifactId> RightManagement</artifactId><version>0.0.1-SNAPSHOT</version><name>RightManagement</name><description>RightManagement-admin</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- 连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.10</version></dependency><!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.40</version></dependency><!-- JWT --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.2.0</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.5</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- spring boot redis 缓存引入 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- lettuce pool 缓存连接池 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!-- hutool工具类--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.3.2.RELEASE</version></plugin></plugins></build></project>

新建yml文件
在这里插入图片描述

server:port: 80servlet:context-path: /spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/db_admin3?serverTimezone=Asia/Shanghaiusername: rootpassword: 123456redis: # redis配置host: 127.0.0.1 # IPport: 6379  # 端口password:  # 密码connect-timeout: 10s  # 连接超时时间lettuce: # lettuce redis客户端配置pool: # 连接池配置max-active: 8  # 连接池最大连接数(使用负值表示没有限制) 默认 8max-wait: 200s  # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-idle: 8 # 连接池中的最大空闲连接 默认 8min-idle: 0 # 连接池中的最小空闲连接 默认 0mybatis-plus:global-config:db-config:id-type: autoconfiguration:map-underscore-to-camel-case: trueauto-mapping-behavior: fulllog-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath:mapper/*.xml

新建数据库db_admin3

CREATE TABLE `sys_user` (`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` VARCHAR(100) DEFAULT NULL COMMENT '用户名',`password` VARCHAR(100) DEFAULT NULL COMMENT '密码',`avatar` VARCHAR(255) DEFAULT 'default.jpg' COMMENT '用户头像',`email` VARCHAR(100) DEFAULT '' COMMENT '用户邮箱',`phonenumber` VARCHAR(11) DEFAULT '' COMMENT '手机号码',`login_date` DATETIME DEFAULT NULL COMMENT '最后登录时间',`status` CHAR(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;/*Data for the table `sys_user` */INSERT  INTO `sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`) VALUES (1,'java1234','$2a$10$Kib4zuVhTzg3I1CoqJfd0unuY9G9ysI7cfbhyT3fi7k7Z/4pr3bGW','20220727112556000000325.jpg','caofeng4017@126.com','18862857417','2022-08-29 22:10:52','0','2022-06-09 08:47:52','2022-06-22 08:47:54','备注'),(2,'common','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','222.jpg','','','2022-08-22 21:34:39','0',NULL,NULL,NULL),(3,'test','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','333.jpg','','','2022-07-24 17:36:07','0',NULL,NULL,NULL),(4,'1','$2a$10$lD0Fx7oMsFFmX9hVkmYy7eJteH8pBaXXro1X9DEMP5sbM.Z6Co55m','default.jpg','','',NULL,'1',NULL,NULL,NULL),(5,'2',NULL,'default.jpg','','',NULL,'1',NULL,NULL,NULL),(15,'fdsfs','$2a$10$AQVcp4hQ7REc5o7ztVnI7eX.sJdcYy3d1x2jm5CfrcCoMZMPacfpi','default.jpg','fdfa4@qq.com','18862851414','2022-08-02 02:22:45','1','2022-08-02 02:21:24','2022-08-01 18:23:16','fdfds4'),(28,'sdfss2','$2a$10$7aNJxwVmefI0XAk64vrzYuOqeeImYJUQnoBrtKP9pLTGTWO2CXQ/y','default.jpg','dfds3@qq.com','18862857413',NULL,'1','2022-08-07 00:42:46','2022-08-06 16:43:04','ddd33'),(29,'ccc','$2a$10$7cbWeVwDWO9Hh3qbJrvTHOn0E/DLYXxnIZpxZei0jY4ChfQbJuhi.','20220829080150000000341.jpg','3242@qq.com','18862584120','2022-08-29 19:52:27','0','2022-08-29 17:04:58',NULL,'xxx'),(30,'ccc666','$2a$10$Tmw5VCM/K2vb837AZDYHQOqE3gPiRZKevxLsh/ozndpTSjdwABqaK','20220829100454000000771.jpg','fdafds@qq.com','18865259845','2022-08-29 22:05:18','0','2022-08-29 22:00:39',NULL,'ccc');

安装插件
在这里插入图片描述
连接mysql
在这里插入图片描述
生成
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
启动类增加MapperScan
在这里插入图片描述
添加封装类

package com.java.entity;import java.util.HashMap;
import java.util.Map;/*** 页面响应entity* @author java1234_小锋* @site www.java1234.com* @company Java知识分享网* @create 2019-08-13 上午 10:00*/
public class R extends HashMap<String, Object> {private static final long serialVersionUID = 1L;public R() {put("code", 200);}public static R error() {return error(500, "未知异常,请联系管理员");}public static R error(String msg) {return error(500, msg);}public static R error(int code, String msg) {R r = new R();r.put("code", code);r.put("msg", msg);return r;}public static R ok(String msg) {R r = new R();r.put("msg", msg);return r;}public static R ok(Map<String, Object> map) {R r = new R();r.putAll(map);return r;}public static R ok() {return new R();}public R put(String key, Object value) {super.put(key, value);return this;}
}

新建testController

package com.java.controller;import com.java.entity.R;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @author: javaLiuwb* @date: 2025-05-04 17:21* @description:*/
@RestController
@RequestMapping("/test")
public class TestController {@Autowiredprivate SysUserService sysUserService;@RequestMapping("/user/list")public R userList(){Map<String,Object> resutlMap=new HashMap<>();List<SysUser> userList = sysUserService.list();resutlMap.put("userList",userList);return R.ok(resutlMap);}}

测试
在这里插入图片描述

前端架构搭建

用vue ui来搭建vue项目;
vue ui是一个可视化图形界面,方便你去创建、更新和管理vue项目,包括下载router,vuex,axios,
elementui等插件,配置好一些属性以及依赖关系,方便我们使用,我个人第一次接触它就感觉非常非
常非常智能和强大。

安装node

在这里插入图片描述

安装Vue Cli

vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目。
因为vue ui是在Vue CLI基础上封装的

npm install -g @vue/cli

vue ui搭建vue项目

vue ui

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
创建成功
在这里插入图片描述
webstorm导入
启动

npm run serve

在这里插入图片描述
启动成功在这里插入图片描述
在这里插入图片描述

安装axios element-plus在这里插入图片描述

element-plus官网:https://element-plus.org/zh-CN/guide/design.html

main.js里添加代码

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'createApp(App).use(store).use(router).use(ElementPlus).mount('#app')

在这里插入图片描述
添加代码
在这里插入图片描述

<template><el-row class="mb-4"><el-button>Default</el-button><el-button type="primary">Primary</el-button><el-button type="success">Success</el-button><el-button type="info">Info</el-button><el-button type="warning">Warning</el-button><el-button type="danger">Danger</el-button></el-row><el-row class="mb-4"><el-button plain>Plain</el-button><el-button type="primary" plain>Primary</el-button><el-button type="success" plain>Success</el-button><el-button type="info" plain>Info</el-button><el-button type="warning" plain>Warning</el-button><el-button type="danger" plain>Danger</el-button></el-row><el-row class="mb-4"><el-button round>Round</el-button><el-button type="primary" round>Primary</el-button><el-button type="success" round>Success</el-button><el-button type="info" round>Info</el-button><el-button type="warning" round>Warning</el-button><el-button type="danger" round>Danger</el-button></el-row><el-row><el-button :icon="Search" circle /><el-button type="primary" :icon="Edit" circle /><el-button type="success" :icon="Check" circle /><el-button type="info" :icon="Message" circle /><el-button type="warning" :icon="Star" circle /><el-button type="danger" :icon="Delete" circle /></el-row>
</template><script  setup>
import {Check,Delete,Edit,Message,Search,Star,
} from '@element-plus/icons-vue'
</script>

点击about 测试成功
在这里插入图片描述

引入JWT前后端交互

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC
7519);
JWT就是一段字符串,用来进行用户身份认证的凭证,该字符串分成三段【头部、载荷、签证】
在这里插入图片描述

在这里插入图片描述
JwtUtils

package com.java.util;import com.java.common.constant.JwtConstant;
import com.java.entity.CheckResult;
import io.jsonwebtoken.*;
import org.bouncycastle.util.encoders.Base64;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;/*** jwt加密和解密的工具类*/
public class JwtUtils {/*** 签发JWT* @param id* @param subject 可以是JSON数据 尽可能少* @param ttlMillis* @return*/public static String createJWT(String id, String subject, long ttlMillis) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);SecretKey secretKey = generalKey();JwtBuilder builder = Jwts.builder().setId(id).setSubject(subject)   // 主题.setIssuer("Java1234")     // 签发者.setIssuedAt(now)      // 签发时间.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙if (ttlMillis >= 0) {long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);builder.setExpiration(expDate); // 过期时间}return builder.compact();}/*** 生成jwt token* @param username* @return*/public static String genJwtToken(String username){return createJWT(username,username,60*60*1000);}/*** 验证JWT* @param jwtStr* @return*/public static CheckResult validateJWT(String jwtStr) {CheckResult checkResult = new CheckResult();Claims claims = null;try {claims = parseJWT(jwtStr);checkResult.setSuccess(true);checkResult.setClaims(claims);} catch (ExpiredJwtException e) {checkResult.setErrCode(JwtConstant.JWT_ERRCODE_EXPIRE);checkResult.setSuccess(false);} catch (SignatureException e) {checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL);checkResult.setSuccess(false);} catch (Exception e) {checkResult.setErrCode(JwtConstant.JWT_ERRCODE_FAIL);checkResult.setSuccess(false);}return checkResult;}/*** 生成加密Key* @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.decode(JwtConstant.JWT_SECERT);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 解析JWT字符串* @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}public static void main(String[] args) throws InterruptedException {//小明失效 10sString sc = createJWT("1","小明", 60 * 60 * 1000);System.out.println(sc);System.out.println(validateJWT(sc).getErrCode());System.out.println(validateJWT(sc).getClaims().getId());System.out.println(validateJWT(sc).getClaims().getSubject());//Thread.sleep(3000);System.out.println(validateJWT(sc).getClaims());Claims claims = validateJWT(sc).getClaims();String sc2 = createJWT(claims.getId(),claims.getSubject(), JwtConstant.JWT_TTL);System.out.println(sc2);}}

JwtConstant

 package com.java.common.constant;/*** 系统级静态变量*/
public class JwtConstant {/*** token*/public static final int JWT_ERRCODE_NULL = 4000;			//Token不存在public static final int JWT_ERRCODE_EXPIRE = 4001;			//Token过期public static final int JWT_ERRCODE_FAIL = 4002;			//验证不通过/*** JWT*/public static final String JWT_SECERT = "8677df7fc3a34e26a61c034d5ec8245d";			//密匙public static final long JWT_TTL = 24*60 * 60 * 1000;									//token有效时间
}

CheckResult

 package com.java.entity;import io.jsonwebtoken.Claims;/*** jwt验证信息*/
public class CheckResult {private int errCode;private boolean success;private Claims claims;public int getErrCode() {return errCode;}public void setErrCode(int errCode) {this.errCode = errCode;}public boolean isSuccess() {return success;}public void setSuccess(boolean success) {this.success = success;}public Claims getClaims() {return claims;}public void setClaims(Claims claims) {this.claims = claims;}}

测试
TestController

 @RequestMapping("/user/list")public R userList(@RequestHeader(required = false)String token){if(StringUtil.isNotEmpty(token)){Map<String,Object> resutlMap=new HashMap<>();List<SysUser> userList = sysUserService.list();resutlMap.put("userList",userList);return R.ok(resutlMap);}else{return R.error(401,"没有权限访问");}}@RequestMapping("/login")public R login(){String token = JwtUtils.genJwtToken("java1234");return R.ok().put("token",token);}

在这里插入图片描述
引入stringUtil

package com.java.util;import java.util.ArrayList;
import java.util.List;
import java.util.Random;/*** 字符串工具类* @author **/
public class StringUtil {/*** 判断是否是空* @param str* @return*/public static boolean isEmpty(String str){if(str==null||"".equals(str.trim())){return true;}else{return false;}}/*** 判断是否不是空* @param str* @return*/public static boolean isNotEmpty(String str){if((str!=null)&&!"".equals(str.trim())){return true;}else{return false;}}/*** 格式化模糊查询* @param str* @return*/public static String formatLike(String str){if(isNotEmpty(str)){return "%"+str+"%";}else{return null;}}/*** 过滤掉集合里的空格* @param list* @return*/public static List<String> filterWhite(List<String> list){List<String> resultList=new ArrayList<String>();for(String l:list){if(isNotEmpty(l)){resultList.add(l);}}return resultList;}/*** 去除html标签*/public static String stripHtml(String content) { // <p>段落替换为换行 content = content.replaceAll("<p .*?>", "\r\n"); // <br><br/>替换为换行 content = content.replaceAll("<br\\s*/?>", "\r\n"); // 去掉其它的<>之间的东西 content = content.replaceAll("\\<.*?>", ""); // 去掉空格 content = content.replaceAll(" ", ""); return content;   }/*** 生成六位随机数* @return*/public static String genSixRandomNum(){Random random = new Random();String result="";for (int i=0;i<6;i++){result+=random.nextInt(10);}return result;}/*** 生成由[A-Z,0-9]生成的随机字符串* @param length  欲生成的字符串长度* @return*/public static String getRandomString(int length){Random random = new Random();StringBuffer sb = new StringBuffer();for(int i = 0; i < length; ++i){int number = random.nextInt(2);long result = 0;switch(number){case 0:result = Math.round(Math.random() * 25 + 65);sb.append(String.valueOf((char)result));break;case 1:sb.append(String.valueOf(new Random().nextInt(10)));break;}}return sb.toString();}}

在这里插入图片描述
前端请求后端用axios
前端新建 request.js

// 引入axios
import axios from 'axios';
import store from '@/store'let baseUrl="http://localhost:8080/";
// 创建axios实例
const httpService = axios.create({// url前缀-'http:xxx.xxx'// baseURL: process.env.BASE_API, // 需自定义baseURL:baseUrl,// 请求超时时间timeout: 3000 // 需自定义
});//添加请求和响应拦截器
// 添加请求拦截器
httpService.interceptors.request.use(function (config) {// 在发送请求之前做些什么//config.headers.token=window.sessionStorage.getItem('token');console.log("store="+store.getters.GET_TOKEN)config.headers.token=store.getters.GET_TOKENreturn config;
}, function (error) {// 对请求错误做些什么return Promise.reject(error);
});// 添加响应拦截器
httpService.interceptors.response.use(function (response) {// 对响应数据做点什么return response;
}, function (error) {// 对响应错误做点什么return Promise.reject(error);
});/*网络请求部分*//**  get请求*  url:请求地址*  params:参数* */
export function get(url, params = {}) {return new Promise((resolve, reject) => {httpService({url: url,method: 'get',params: params}).then(response => {resolve(response);}).catch(error => {reject(error);});});
}/**  post请求*  url:请求地址*  params:参数* */
export function post(url, params = {}) {return new Promise((resolve, reject) => {httpService({url: url,method: 'post',data: params}).then(response => {console.log(response)resolve(response);}).catch(error => {console.log(error)reject(error);});});
}/**  文件上传*  url:请求地址*  params:参数* */
export function fileUpload(url, params = {}) {return new Promise((resolve, reject) => {httpService({url: url,method: 'post',data: params,headers: { 'Content-Type': 'multipart/form-data' }}).then(response => {resolve(response);}).catch(error => {reject(error);});});
}export function getServerUrl(){return baseUrl;
}export default {get,post,fileUpload,getServerUrl
}

APP.VUE

<template><el-button type="primary" @click="handleLogin">测试登录</el-button><el-button type="danger" @click="handleUserList">测试获取用户请求</el-button>
</template>
<script setup>
import requestUtil from '@/util/request'
import store from '@/store'const handleLogin=async ()=>{let result=await requestUtil.get("test/login");let data=result.data;if(data.code=200){const token=data.token;console.log("登录成功:token="+token);store.commit('SET_TOKEN',token);}else{console.log("登录出错!");}
}const handleUserList=async ()=>{let result=await requestUtil.get("test/user/list");let data=result.data;if(data.code=200){const userList=data.userList;console.log("用户列表信息:userList="+userList);}
}</script>
<style>
</style>

store.js里

import { createStore } from 'vuex'export default createStore({state: {},getters: {GET_TOKEN:state => {return sessionStorage.getItem("token")}},mutations: {SET_TOKEN:(state,token)=>{sessionStorage.setItem("token",token);}},actions: {},modules: {}
})

跨域问题报错
在这里插入图片描述

在这里插入图片描述
后端新建config
在这里插入图片描述

 package com.java.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** web项目配置类*/
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowCredentials(true).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE","OPTIONS").maxAge(3600);}}

测试成功
在这里插入图片描述

登录功能实现

前端新建文件
在这里插入图片描述
border.css

 @charset "utf-8";
.border,
.border-top,
.border-right,
.border-bottom,
.border-left,
.border-topbottom,
.border-rightleft,
.border-topleft,
.border-rightbottom,
.border-topright,
.border-bottomleft {position: relative;
}
.border::before,
.border-top::before,
.border-right::before,
.border-bottom::before,
.border-left::before,
.border-topbottom::before,
.border-topbottom::after,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::before,
.border-topleft::after,
.border-rightbottom::before,
.border-rightbottom::after,
.border-topright::before,
.border-topright::after,
.border-bottomleft::before,
.border-bottomleft::after {content: "\0020";overflow: hidden;position: absolute;
}
/* border* 因,边框是由伪元素区域遮盖在父级* 故,子级若有交互,需要对子级设置* 定位 及 z轴*/
.border::before {box-sizing: border-box;top: 0;left: 0;height: 100%;width: 100%;border: 1px solid #eaeaea;transform-origin: 0 0;
}
.border-top::before,
.border-bottom::before,
.border-topbottom::before,
.border-topbottom::after,
.border-topleft::before,
.border-rightbottom::after,
.border-topright::before,
.border-bottomleft::before {left: 0;width: 100%;height: 1px;
}
.border-right::before,
.border-left::before,
.border-rightleft::before,
.border-rightleft::after,
.border-topleft::after,
.border-rightbottom::before,
.border-topright::after,
.border-bottomleft::after {top: 0;width: 1px;height: 100%;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {border-top: 1px solid #eaeaea;transform-origin: 0 0;
}
.border-right::before,
.border-rightbottom::before,
.border-rightleft::before,
.border-topright::after {border-right: 1px solid #eaeaea;transform-origin: 100% 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::before {border-bottom: 1px solid #eaeaea;transform-origin: 0 100%;
}
.border-left::before,
.border-topleft::after,
.border-rightleft::after,
.border-bottomleft::after {border-left: 1px solid #eaeaea;transform-origin: 0 0;
}
.border-top::before,
.border-topbottom::before,
.border-topleft::before,
.border-topright::before {top: 0;
}
.border-right::before,
.border-rightleft::after,
.border-rightbottom::before,
.border-topright::after {right: 0;
}
.border-bottom::before,
.border-topbottom::after,
.border-rightbottom::after,
.border-bottomleft::after {bottom: 0;
}
.border-left::before,
.border-rightleft::before,
.border-topleft::after,
.border-bottomleft::before {left: 0;
}
@media (max--moz-device-pixel-ratio: 1.49), (-webkit-max-device-pixel-ratio: 1.49), (max-device-pixel-ratio: 1.49), (max-resolution: 143dpi), (max-resolution: 1.49dppx) {/* 默认值,无需重置 */
}
@media (min--moz-device-pixel-ratio: 1.5) and (max--moz-device-pixel-ratio: 2.49), (-webkit-min-device-pixel-ratio: 1.5) and (-webkit-max-device-pixel-ratio: 2.49), (min-device-pixel-ratio: 1.5) and (max-device-pixel-ratio: 2.49), (min-resolution: 144dpi) and (max-resolution: 239dpi), (min-resolution: 1.5dppx) and (max-resolution: 2.49dppx) {.border::before {width: 200%;height: 200%;transform: scale(.5);}.border-top::before,.border-bottom::before,.border-topbottom::before,.border-topbottom::after,.border-topleft::before,.border-rightbottom::after,.border-topright::before,.border-bottomleft::before {transform: scaleY(.5);}.border-right::before,.border-left::before,.border-rightleft::before,.border-rightleft::after,.border-topleft::after,.border-rightbottom::before,.border-topright::after,.border-bottomleft::after {transform: scaleX(.5);}
}
@media (min--moz-device-pixel-ratio: 2.5), (-webkit-min-device-pixel-ratio: 2.5), (min-device-pixel-ratio: 2.5), (min-resolution: 240dpi), (min-resolution: 2.5dppx) {.border::before {width: 300%;height: 300%;transform: scale(.33333);}.border-top::before,.border-bottom::before,.border-topbottom::before,.border-topbottom::after,.border-topleft::before,.border-rightbottom::after,.border-topright::before,.border-bottomleft::before {transform: scaleY(.33333);}.border-right::before,.border-left::before,.border-rightleft::before,.border-rightleft::after,.border-topleft::after,.border-rightbottom::before,.border-topright::after,.border-bottomleft::after {transform: scaleX(.33333);}
}

reset.css

 @charset "utf-8";
html{font-size:12px}
body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%}
h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif}
h1,h2,h3,h4,h5,h6,b,strong{font-weight:normal}
address,cite,dfn,em,i,optgroup,var{font-style:normal}
table{border-collapse:collapse;border-spacing:0;text-align:left}
caption,th{text-align:inherit}
ul,ol,menu{list-style:none}
fieldset,img{border:0}
img,object,input,textarea,button,select{vertical-align:middle}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1}
blockquote:before,blockquote:after,q:before,q:after{content:"\0020"}
textarea{overflow:auto;resize:vertical}
input,textarea,button,select,a{outline:0 none;border: none;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}
mark{background-color:transparent}
a,ins,s,u,del{text-decoration:none}
sup,sub{vertical-align:baseline}
html {overflow-x: hidden;height: 100%;font-size: 50px;-webkit-tap-highlight-color: transparent;}
body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;color: #333;font-size: .28em;line-height: 1;-webkit-text-size-adjust: none;}
hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;}
a {color: #25a4bb;text-decoration: none;}

main.js导入样式文件:

import '@/assets/styles/border.css'
import '@/assets/styles/reset.css'

添加路由

在这里插入图片描述
安装sass和sass-loader依赖
在这里插入图片描述
在这里插入图片描述

新建view Login.vue

 <template>
<div class="login"><el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"><h3 class="title">Java1234 Vue3 后台管理系统</h3><el-form-item prop="username"><el-inputtype="text"size="large"auto-complete="off"placeholder="账号"></el-input></el-form-item><el-form-item prop="password"><el-inputtype="password"size="large"auto-complete="off"placeholder="密码"@keyup.enter="handleLogin"></el-input></el-form-item><el-checkbox  style="margin:0px 0px 25px 0px;">记住密码</el-checkbox><el-form-item style="width:100%;"><el-buttonsize="large"type="primary"style="width:100%;"@click.prevent="handleLogin"><span>登 录</span></el-button></el-form-item></el-form><!--  底部  --><div class="el-login-footer"><span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">java1234.vip</a> 版权所有.</span></div></div>
</template><script setup></script><style lang="scss" scoped>
a{color:white
}
.login {display: flex;justify-content: center;align-items: center;height: 100%;background-image: url("../assets/images/login-background.jpg");background-size: cover;
}
.title {margin: 0px auto 30px auto;text-align: center;color: #707070;
}.login-form {border-radius: 6px;background: #ffffff;width: 400px;padding: 25px 25px 5px 25px;.el-input {height: 40px;input {display: inline-block;height: 40px;}}.input-icon {height: 39px;width: 14px;margin-left: 0px;}}
.login-tip {font-size: 13px;text-align: center;color: #bfbfbf;
}
.login-code {width: 33%;height: 40px;float: right;img {cursor: pointer;vertical-align: middle;}
}
.el-login-footer {height: 40px;line-height: 40px;position: fixed;bottom: 0;width: 100%;text-align: center;color: #fff;font-family: Arial;font-size: 12px;letter-spacing: 1px;
}
.login-code-img {height: 40px;padding-left: 12px;
}
</style>

App.vue设置下全局样式:

<style>
html,body,#app{height: 100%;
}
.app-container{padding:20px
}
</style>

在这里插入图片描述

自定义icon实现

新建
在这里插入图片描述

 <template><svg class="svg-icon" aria-hidden="true"><use :xlink:href="iconName"></use></svg>
</template><script setup>
import { defineProps, computed } from 'vue'
const props = defineProps({icon: {type: String,required: true}
})const iconName = computed(() => {return `#icon-${props.icon}`
})
</script><style lang="scss" scoped>
.svg-icon {width: 1em;height: 1em;vertical-align: -0.15em;fill: currentColor;overflow: hidden;
}
</style>

src下新建文件夹 新建index.js
在这里插入图片描述

安装依赖 webpack
在这里插入图片描述

安装依赖 svg-sprite-loader
在这里插入图片描述

vue.config.js在这里插入图片描述

 // const { defineConfig } = require('@vue/cli-service')
// module.exports = defineConfig({
//   transpileDependencies: true
// })
const webpack = require('webpack');const path = require('path')
function resolve(dir) {return path.join(__dirname, dir)
}module.exports = {lintOnSave: false,chainWebpack(config) {// 设置 svg-sprite-loader// config 为 webpack 配置对象// config.module 表示创建一个具名规则,以后用来修改规则config.module// 规则.rule('svg')// 忽略.exclude.add(resolve('src/icons'))// 结束.end()// config.module 表示创建一个具名规则,以后用来修改规则config.module// 规则.rule('icons')// 正则,解析 .svg 格式文件.test(/\.svg$/)// 解析的文件.include.add(resolve('src/icons'))// 结束.end()// 新增了一个解析的loader.use('svg-sprite-loader')// 具体的loader.loader('svg-sprite-loader')// loader 的配置.options({symbolId: 'icon-[name]'})// 结束.end()config.plugin('ignore').use(new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/))config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end().use('svg-sprite-loader').loader('svg-sprite-loader').options({symbolId: 'icon-[name]'}).end()}
}

修改main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'import '@/assets/styles/border.css'
import '@/assets/styles/reset.css'import SvgIcon from '@/icons'
//
// createApp(App).use(store).use(router).use(ElementPlus).mount('#app')const app=createApp(App);SvgIcon(app);app.use(store)
app.use(router)
app.use(ElementPlus)
app.mount('#app')

Login.vue添加代码
在这里插入图片描述

在这里插入图片描述

SpringSecurity执行原理概述

spring security的简单原理:
SpringSecurity有很多很多的拦截器,在执行流程里面主要有两个核心的拦截器
1,登陆验证拦截器AuthenticationProcessingFilter
2,资源管理拦截器AbstractSecurityInterceptor
但拦截器里面的实现需要一些组件来实现,所以就有了AuthenticationManager认证管理器、
accessDecisionManager决策管理器等组件来支撑。
在这里插入图片描述
FilterChainProxy是一个代理,真正起作用的是各个Filter,这些Filter作为Bean被Spring管理,是
Spring Security核心,各有各的职责,不直接处理认证和授权,交由认证管理器和决策管理器处理!
大概流程
在这里插入图片描述
认证管理
流程图解读:
1、用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器
获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实
现类。
2、然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证 。
3、认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权
限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
4、SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过
SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出
AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实
现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个
List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道
web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着
一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至
Authentication。
授权管理
在这里插入图片描述
访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用
SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器
AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户
的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票决
定,一票否定,少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转
到403页面、自定义页面。
转载自:https://blog.csdn.net/weixin_51542566/article/details/119705963

项目整合SpringSecurity

pom.xml加下springsecurity依赖

   <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>

新建配置类

package com.java.config;import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;/*** spring security配置*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {private static final String URL_WHITELIST[] ={"/login","/logout","/captcha","/password","/image/**","/test/**"} ;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {super.configure(auth);}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 开启跨域 以及csrf攻击 关闭http.cors().and().csrf().disable()// 登录登出配置.formLogin()
//            .successHandler()
//            .failureHandler()
//        .and()
//            .logout()
//            .logoutSuccessHandler()// session禁用配置.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 无状态// 拦截规则配置.and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll()  // 白名单 放行.anyRequest().authenticated();// 异常处理配置// 自定义过滤器配置}
}

重写登录成功和登录失败处理器

common下新建security包,再新建两个类,LoginSuccessHandler和LoginFailureHandler
在这里插入图片描述
LoginSuccessHandler

 package com.java.common.security;import cn.hutool.json.JSONUtil;import com.java.entity.R;
import com.java.util.JwtUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 登录成功处理器*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();String username="user";String token = JwtUtils.genJwtToken(username);outputStream.write(JSONUtil.toJsonStr(R.ok("登录成功").put("authorization",token)).getBytes());outputStream.flush();outputStream.close();}
}

LoginFailureHandler

 package com.java.common.security;import cn.hutool.json.JSONUtil;import com.java.entity.R;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 登录成功处理器*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();String message=e.getMessage();if(e instanceof BadCredentialsException){message="用户名或者密码错误!";}outputStream.write(JSONUtil.toJsonStr(R.error(message)).getBytes("UTF-8"));outputStream.flush();outputStream.close();}
}

注入
在这里插入图片描述
在这里插入图片描述
前端
Login.vue页面修改
在这里插入图片描述

script 里添加代码


<script setup>import {ref} from 'vue'
import requestUtil from '@/util/request'
import store from '@/store'
import qs from "qs"
import {ElMessage} from "element-plus"const loginRef=ref(null)const loginForm=ref({username:"",password:""
})const loginRules = {username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],password: [{ required: true, trigger: "blur", message: "请输入您的密码" }]
};const handleLogin=()=>{loginRef.value.validate(async(valid)=>{if(valid){let result=await requestUtil.post("login?"+qs.stringify(loginForm.value))let data=result.data;if(data.code==200){const token=data.authorizationstore.commit('SET_TOKEN',token);}else{ElMessage.error(data.msg)}}else{console.log("验证失败")}})
}</script>

安装qs
在这里插入图片描述
什么都不输入验证失败
在这里插入图片描述
随便输入密码
在这里插入图片描述
输入正确密码验证成功
在这里插入图片描述
token也存在
在这里插入图片描述

用户登录SpringSecurity查库实现

security包下新建MyUserDetailServiceImpl

package com.java.common.security;import com.java.common.exception.UserCountLockException;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** 自定义UserDetails*/
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {@AutowiredSysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser=sysUserService.getByUsername(username);if(sysUser==null){throw new UsernameNotFoundException("用户名或者密码错误!");}else if("1".equals(sysUser.getStatus())){throw new UserCountLockException("该用户账号被封禁,具体请联系管理员!");}return new User(sysUser.getUsername(),sysUser.getPassword(),getUserAuthority());}private List<GrantedAuthority> getUserAuthority() {return new ArrayList<>();}
}

SysUserService里新建接口

  SysUser getByUsername(String username);

以及对应实现
SysUserServiceImpl

    @Overridepublic SysUser getByUsername(String username) {return getOne(new QueryWrapper<SysUser>().eq("username",username));}

以及自定义异常类
在这里插入图片描述
全局异常
在这里插入图片描述

package com.java.common.exception;import com.java.entity.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全部异常处理*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(value = RuntimeException.class)public R handler(RuntimeException e){log.error("运行时异常:---------{}"+e.getMessage());return R.error(e.getMessage());}
}
 package com.java.common.security;import com.java.common.exception.UserCountLockException;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** 自定义UserDetails*/
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {@AutowiredSysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser=sysUserService.getByUsername(username);if(sysUser==null){throw new UsernameNotFoundException("用户名或者密码错误!");}else if("1".equals(sysUser.getStatus())){throw new UserCountLockException("该用户账号被封禁,具体请联系管理员!");}return new User(sysUser.getUsername(),sysUser.getPassword(),getUserAuthority());}private List<GrantedAuthority> getUserAuthority() {return new ArrayList<>();}
}

修改SecurityConfig.java

在这里插入图片描述
测试成功
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实现JWT认证过滤器

router配置,加下首页路由

{path: '/',name: '首页',component: () => import('../layout')
},

新建主页
在这里插入图片描述

<template>
<el-button type="danger" @click="testHandler">测试接口</el-button>
</template><script setup>
import requestUtil from '@/util/request'const testHandler=async ()=>{let result=await requestUtil.get("test/user/list");}
</script><style scoped></style>

Login.vue

import router from "@/router"const handleLogin=()=>{loginRef.value.validate(async(valid)=>{if(valid){let result=await requestUtil.post("login?"+qs.stringify(loginForm.value))let data=result.data;if(data.code==200){const token=data.authorizationstore.commit('SET_TOKEN',token);router.replace("/")}else{ElMessage.error(data.msg)}}else{console.log("验证失败")}})
}

成功进入主页
在这里插入图片描述
删掉这部分
在这里插入图片描述
securityconfig里添加配置

  @BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {JwtAuthenticationFilter jwtAuthenticationFilter=new JwtAuthenticationFilter(authenticationManager());return jwtAuthenticationFilter;}// 自定义过滤器配置.and().addFilter(jwtAuthenticationFilter());

LoginSuccessHandler里修改代码
在这里插入图片描述
新建
在这里插入图片描述

 package com.java.common.security;import com.java.common.constant.JwtConstant;
import com.java.entity.CheckResult;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import com.java.util.JwtUtils;
import com.java.util.StringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;/***/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate MyUserDetailsServiceImpl myUserDetailsService;private static final String URL_WHITELIST[] ={"/login","/logout","/captcha","/password","/image/**"} ;public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {String token = request.getHeader("token");System.out.println("请求url:"+request.getRequestURI());// 如果token是空 或者 url在白名单里,则放行if(StringUtil.isEmpty(token) || new ArrayList<String>(Arrays.asList(URL_WHITELIST)).contains(request.getRequestURI())){chain.doFilter(request,response);return;}CheckResult checkResult = JwtUtils.validateJWT(token);if(!checkResult.isSuccess()){switch (checkResult.getErrCode()){case JwtConstant.JWT_ERRCODE_NULL:throw new JwtException("Token不存在");case JwtConstant.JWT_ERRCODE_FAIL:throw new JwtException("Token验证不通过");case JwtConstant.JWT_ERRCODE_EXPIRE:throw new JwtException("Token过期");}}Claims claims = JwtUtils.parseJWT(token);String username = claims.getSubject();SysUser sysUser = sysUserService.getByUsername(username);UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(username,null,myUserDetailsService.getUserAuthority());SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);chain.doFilter(request,response);}
}

在这里插入图片描述
修改token
在这里插入图片描述
不可访问
在这里插入图片描述

实现JWT认证异常处理器

新建 JwtAuthenticationEntryPoint

package com.java.common.security;import cn.hutool.json.JSONUtil;import com.java.entity.R;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** jwt认证失败处理*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();outputStream.write(JSONUtil.toJsonStr(R.error(HttpServletResponse.SC_UNAUTHORIZED,"认证失败,请登录!")).getBytes());outputStream.flush();outputStream.close();}
}

SecurityConfig里添加配置

  @Autowiredprivate JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;// 异常处理配置.and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)

修改token
在这里插入图片描述

认证失败
在这里插入图片描述

实现自定义logout处理

默认logout请求实现是有状态的,返回到login请求页面;我们现在是前后端分离处理,所以需要自定义
实现logout
新建JwtLogoutSuccessHandler

package com.java.common.security;import cn.hutool.json.JSONUtil;import com.java.entity.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 自定义logout处理*/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();outputStream.write(JSONUtil.toJsonStr(R.ok("登出成功")).getBytes());outputStream.flush();outputStream.close();}
}

SecurityConfig添加配置

 @Autowiredprivate JwtLogoutSuccessHandler jwtLogoutSuccessHandler;.and().logout().logoutSuccessHandler(jwtLogoutSuccessHandler)

前端添加按钮

 <template><el-button type="danger" @click="testHandler">测试接口</el-button><el-button type="danger" @click="testLogoutHandler">测试logout登出</el-button>
</template><script setup>
import requestUtil from '@/util/request'const testHandler=async ()=>{let result=await requestUtil.get("test/user/list");}const testLogoutHandler=async ()=>{let result=await requestUtil.get("logout");}
</script><style scoped></style>

登出成功
在这里插入图片描述

获取用户角色权限信息实现

springsecurity鉴权需要获取用户的角色权限系统,包括前端也需要这些信息;
首先我们新建角色表sys_role,菜单权限表sys_menu,用户角色关联表sys_user_role,角色菜单权限关
联表sys_role_menu

角色表sys_role 菜单权限表sys_menu 用户角色关联表sys_user_role 角色菜单权限关联表sys_role_menu

/*
SQLyog Ultimate v11.33 (64 bit)
MySQL - 5.7.18-log : Database - db_admin3
*********************************************************************
*//*!40101 SET NAMES utf8 */;/*!40101 SET SQL_MODE=''*/;/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_admin3` /*!40100 DEFAULT CHARACTER SET utf8 */;USE `db_admin3`;/*Table structure for table `sys_menu` */DROP TABLE IF EXISTS `sys_menu`;CREATE TABLE `sys_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单主键ID',`name` varchar(50) DEFAULT NULL COMMENT '菜单名称',`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID',`order_num` int(11) DEFAULT '0' COMMENT '显示顺序',`path` varchar(200) DEFAULT '' COMMENT '路由地址',`component` varchar(255) DEFAULT NULL COMMENT '组件路径',`menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',`perms` varchar(100) DEFAULT '' COMMENT '权限标识',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;/*Data for the table `sys_menu` */insert  into `sys_menu`(`id`,`name`,`icon`,`parent_id`,`order_num`,`path`,`component`,`menu_type`,`perms`,`create_time`,`update_time`,`remark`) values (1,'系统管理','system',0,1,'/sys','','M','','2022-07-04 14:56:29','2022-07-04 14:56:31','系统管理目录'),(2,'业务管理','monitor',0,2,'/bsns','','M','','2022-07-04 14:59:43','2022-07-04 14:59:45','业务管理目录'),(3,'用户管理','user',1,1,'/sys/user','sys/user/index','C','system:user:list','2022-07-04 15:20:51','2022-07-04 15:20:53','用户管理菜单'),(4,'角色管理','peoples',1,2,'/sys/role','sys/role/index','C','system:role:list','2022-07-04 15:23:35','2022-07-04 15:23:39','角色管理菜单'),(5,'菜单管理','tree-table',1,3,'/sys/menu','sys/menu/index','C','system:menu:list','2022-07-04 15:23:41','2022-07-04 15:23:43','菜单管理菜单'),(6,'部门管理','tree',2,1,'/bsns/department','bsns/Department','C','','2022-07-04 15:24:40','2022-07-04 15:24:44','部门管理菜单'),(7,'岗位管理','post',2,2,'/bsns/post','bsns/Post','C','','2022-07-04 15:24:42','2022-07-04 15:24:46','岗位管理菜单'),(8,'用户新增','#',3,2,'','','F','system:user:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加用户按钮'),(9,'用户修改','#',3,3,'','','F','system:user:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改用户按钮'),(10,'用户删除','#',3,4,'','','F','system:user:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除用户按钮'),(11,'分配角色','#',3,5,'','','F','system:user:role','2022-07-04 15:24:42','2022-07-04 15:24:46','分配角色按钮'),(12,'重置密码','#',3,6,'','','F','system:user:resetPwd','2022-07-04 15:24:42','2022-07-04 15:24:46','重置密码按钮'),(13,'角色新增','#',4,2,'','','F','system:role:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加用户按钮'),(14,'角色修改','#',4,3,'','','F','system:role:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改用户按钮'),(15,'角色删除','#',4,4,'',NULL,'F','system:role:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除用户按钮'),(16,'分配权限','#',4,5,'','','F','system:role:menu','2022-07-04 15:24:42','2022-07-04 15:24:46','分配权限按钮'),(17,'菜单新增','#',5,2,'',NULL,'F','system:menu:add','2022-07-04 15:24:42','2022-07-04 15:24:46','添加菜单按钮'),(18,'菜单修改','#',5,3,'',NULL,'F','system:menu:edit','2022-07-04 15:24:42','2022-07-04 15:24:46','修改菜单按钮'),(19,'菜单删除','#',5,4,'',NULL,'F','system:menu:delete','2022-07-04 15:24:42','2022-07-04 15:24:46','删除菜单按钮'),(20,'用户查询','#',3,1,'',NULL,'F','system:user:query','2022-07-04 15:24:42','2022-07-04 15:24:46','用户查询按钮'),(21,'角色查询','#',4,1,'',NULL,'F','system:role:query','2022-07-04 15:24:42','2022-07-04 15:24:46','角色查询按钮'),(22,'菜单查询','#',5,1,'',NULL,'F','system:menu:query','2022-07-04 15:24:42','2022-07-04 15:24:46','菜单查询按钮'),(33,'测速22','122',3,3,'','34','M','33','2022-08-19 03:11:20','2022-08-18 19:11:33',NULL);/*Table structure for table `sys_role` */DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色主键ID',`name` varchar(30) DEFAULT NULL COMMENT '角色名称',`code` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;/*Data for the table `sys_role` */insert  into `sys_role`(`id`,`name`,`code`,`create_time`,`update_time`,`remark`) values (1,'超级管理员','admin','2022-07-04 14:40:44','2022-07-04 14:40:47','拥有系统最高权限'),(2,'普通角色','common','2022-07-04 14:41:56','2022-07-04 14:41:58','普通角色'),(3,'测试角色','test','2022-07-04 14:42:24','2022-07-04 14:42:27','测试角色'),(4,'2',NULL,NULL,NULL,NULL),(5,'3',NULL,NULL,NULL,NULL),(6,'4',NULL,NULL,NULL,NULL),(7,'5',NULL,NULL,NULL,NULL),(14,'6',NULL,NULL,NULL,NULL),(16,'8',NULL,NULL,NULL,NULL),(17,'0',NULL,NULL,NULL,NULL),(19,'测2','cc2','2022-08-13 21:06:21','2022-08-13 13:06:27','eewew2'),(20,'ccc测试','test2','2022-08-29 17:10:33',NULL,'xxx'),(21,'今天测试角色','todytest','2022-08-29 22:01:11',NULL,'ccc');/*Table structure for table `sys_role_menu` */DROP TABLE IF EXISTS `sys_role_menu`;CREATE TABLE `sys_role_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色菜单主键ID',`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',`menu_id` bigint(20) DEFAULT NULL COMMENT '菜单ID',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=239 DEFAULT CHARSET=utf8;/*Data for the table `sys_role_menu` */insert  into `sys_role_menu`(`id`,`role_id`,`menu_id`) values (8,2,1),(9,2,2),(10,2,3),(11,2,4),(12,2,5),(13,2,6),(14,2,7),(15,3,2),(16,3,6),(17,3,7),(21,7,1),(22,7,2),(23,7,6),(24,7,7),(25,6,1),(26,6,3),(27,6,9),(28,6,10),(29,19,1),(30,19,3),(31,19,2),(32,19,6),(33,1,1),(34,1,3),(35,1,20),(36,1,8),(37,1,9),(38,1,10),(39,1,11),(40,1,12),(41,1,4),(42,1,21),(43,1,13),(44,1,14),(45,1,15),(46,1,16),(47,1,23),(48,1,5),(49,1,22),(50,1,17),(51,1,18),(52,1,19),(53,1,2),(54,1,6),(55,1,7),(208,20,1),(209,20,3),(210,20,20),(211,20,8),(212,20,9),(213,20,33),(214,20,10),(215,20,11),(216,20,4),(217,20,21),(218,20,13),(219,20,5),(220,20,22),(221,20,17),(222,20,18),(223,20,2),(224,20,6),(225,20,7),(232,21,1),(233,21,9),(234,21,4),(235,21,21),(236,21,2),(237,21,6),(238,21,7);/*Table structure for table `sys_user` */DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(100) DEFAULT NULL COMMENT '用户名',`password` varchar(100) DEFAULT NULL COMMENT '密码',`avatar` varchar(255) DEFAULT 'default.jpg' COMMENT '用户头像',`email` varchar(100) DEFAULT '' COMMENT '用户邮箱',`phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',`status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',`create_time` datetime DEFAULT NULL COMMENT '创建时间',`update_time` datetime DEFAULT NULL COMMENT '更新时间',`remark` varchar(500) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8;/*Data for the table `sys_user` */insert  into `sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`) values (1,'java1234','$2a$10$Kib4zuVhTzg3I1CoqJfd0unuY9G9ysI7cfbhyT3fi7k7Z/4pr3bGW','20220727112556000000325.jpg','caofeng4017@126.com','18862857417','2022-08-29 22:10:52','0','2022-06-09 08:47:52','2022-06-22 08:47:54','备注'),(2,'common','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','222.jpg','','','2022-08-22 21:34:39','0',NULL,NULL,NULL),(3,'test','$2a$10$tiArwm0GxChyEP5k0JGzsOuzyY15IKA.ZTl8S2aj3haYlKAfpwfl.','333.jpg','','','2022-07-24 17:36:07','0',NULL,NULL,NULL),(4,'1','$2a$10$lD0Fx7oMsFFmX9hVkmYy7eJteH8pBaXXro1X9DEMP5sbM.Z6Co55m','default.jpg','','',NULL,'1',NULL,NULL,NULL),(5,'2',NULL,'default.jpg','','',NULL,'1',NULL,NULL,NULL),(15,'fdsfs','$2a$10$AQVcp4hQ7REc5o7ztVnI7eX.sJdcYy3d1x2jm5CfrcCoMZMPacfpi','default.jpg','fdfa4@qq.com','18862851414','2022-08-02 02:22:45','1','2022-08-02 02:21:24','2022-08-01 18:23:16','fdfds4'),(28,'sdfss2','$2a$10$7aNJxwVmefI0XAk64vrzYuOqeeImYJUQnoBrtKP9pLTGTWO2CXQ/y','default.jpg','dfds3@qq.com','18862857413',NULL,'1','2022-08-07 00:42:46','2022-08-06 16:43:04','ddd33'),(29,'ccc','$2a$10$7cbWeVwDWO9Hh3qbJrvTHOn0E/DLYXxnIZpxZei0jY4ChfQbJuhi.','20220829080150000000341.jpg','3242@qq.com','18862584120','2022-08-29 19:52:27','0','2022-08-29 17:04:58',NULL,'xxx'),(30,'ccc666','$2a$10$Tmw5VCM/K2vb837AZDYHQOqE3gPiRZKevxLsh/ozndpTSjdwABqaK','20220829100454000000771.jpg','fdafds@qq.com','18865259845','2022-08-29 22:05:18','0','2022-08-29 22:00:39',NULL,'ccc');/*Table structure for table `sys_user_role` */DROP TABLE IF EXISTS `sys_user_role`;CREATE TABLE `sys_user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户角色主键ID',`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',`role_id` bigint(20) DEFAULT NULL COMMENT '角色ID',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8;/*Data for the table `sys_user_role` */insert  into `sys_user_role`(`id`,`user_id`,`role_id`) values (1,1,1),(2,2,2),(4,1,2),(6,3,3),(7,3,2),(9,4,3),(10,5,3),(11,15,3),(16,28,2),(17,28,3),(20,29,20),(21,30,17),(22,30,21);/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

我们通过MybatisX生成代码:
在这里插入图片描述
因为每个实体都有几个通用属性,id,createTime,updateTime,remark,所以我们搞一个通用基础
实体类,让其他类继承下;
方便维护;
新建BaseEntity

package com.java.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;import java.io.Serializable;
import java.util.Date;/*** 公共基础实体类*/
@Data
public class BaseEntity implements Serializable {@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 创建日期*/@JsonSerialize(using=CustomDateTimeSerializer.class)@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")private Date createTime;/*** 更新日期*/@JsonSerialize(using=CustomDateTimeSerializer.class)@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss")private Date updateTime;/*** 备注*/private String remark;}

自定义返回JSON 数据格式中日期格式化处理

 package com.java.entity;import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;/*** 自定义返回JSON 数据格式中日期格式化处理**/
public class CustomDateTimeSerializer extends JsonSerializer<Date>{@Overridepublic void serialize(Date value, JsonGenerator gen, SerializerProvider serializers)throws IOException {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));gen.writeString(sdf.format(value));  }}

继承该类,删除相同属性
在这里插入图片描述
SysUserServiceImpl``的实现方法getUserAuthorityInfo

  public List<GrantedAuthority> getUserAuthority(Long userId) {//  格式ROLE_admin,ROLE_common,system:user:resetPwd,system:role:delete,system:user:list,system:menu:query,system:menu:list,system:menu:add,system:user:delete,system:role:list,system:role:menu,system:user:edit,system:user:query,system:role:edit,system:user:add,system:user:role,system:menu:delete,system:role:add,system:role:query,system:menu:editString authority=sysUserService.getUserAuthorityInfo(userId);return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);}

生成方法
在这里插入图片描述
实现

package com.java.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.java.entity.SysMenu;
import com.java.entity.SysRole;
import com.java.entity.SysUser;
import com.java.mapper.SysMenuMapper;
import com.java.mapper.SysRoleMapper;
import com.java.service.SysUserService;
import com.java.mapper.SysUserMapper;
import com.java.util.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.HashSet;
import java.util.List;
import java.util.Set;/**
* @author 86182
* @description 针对表【sys_user】的数据库操作Service实现
* @createDate 2025-05-04 17:19:02
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>implements SysUserService{@AutowiredSysRoleMapper sysRoleMapper;@AutowiredSysMenuMapper sysMenuMapper;@Overridepublic SysUser getByUsername(String username) {return getOne(new QueryWrapper<SysUser>().eq("username",username));}@Overridepublic String getUserAuthorityInfo(Long userId) {StringBuffer authority=new StringBuffer();// 根据用户id获取所有的角色信息List<SysRole> roleList = sysRoleMapper.selectList(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id=" + userId));if(roleList.size()>0){String roleCodeStrs = roleList.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));authority.append(roleCodeStrs);}// 遍历所有的角色,获取所有菜单权限 而且不重复Set<String> menuCodeSet=new HashSet<>();for(SysRole sysRole:roleList){List<SysMenu> sysMenuList = sysMenuMapper.selectList(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id=" + sysRole.getId()));for(SysMenu sysMenu:sysMenuList){String perms=sysMenu.getPerms();if(StringUtil.isNotEmpty(perms)){menuCodeSet.add(perms);}}}if(menuCodeSet.size()>0){authority.append(",");String menuCodeStrs = menuCodeSet.stream().collect(Collectors.joining(","));authority.append(menuCodeStrs);}System.out.println("authority:"+authority.toString());return authority.toString();}
}

测试成功
在这里插入图片描述
测试
在这里插入图片描述
可以访问
改为
在这里插入图片描述
不可访问
在这里插入图片描述
测试权限
在这里插入图片描述
不可访问
在这里插入图片描述

删除2可访问

    @PreAuthorize("hasAuthority('system:user:list')")

在这里插入图片描述

记住密码功能实现

记住密码,我们通过cookie来实现,先安装依赖 ‘js-cookie’
存储用户密码,为了安全需要加密,获取密码解密。所以我们安装依赖’jsencrypt’
util下新建jsencrypt.js

import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey ='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
const privateKey ='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +'7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +'UP8iWi1Qw0Y='
// 加密
export function encrypt(txt) {const encryptor = new JSEncrypt()encryptor.setPublicKey(publicKey) // 设置公钥return encryptor.encrypt(txt) // 对数据进行加密
}
// 解密
export function decrypt(txt) {const encryptor = new JSEncrypt()encryptor.setPrivateKey(privateKey) // 设置私钥return encryptor.decrypt(txt) // 对数据进行解密
}

Login.vue导入

import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/util/jsencrypt";

在这里插入图片描述
添加代码

   // 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码if (loginForm.value.rememberMe) {Cookies.set("username", loginForm.value.username, { expires: 30 });Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });} else {// 否则移除Cookies.remove("username");Cookies.remove("password");Cookies.remove("rememberMe");}

在这里插入图片描述

添加查询代码

function getCookie() {const username = Cookies.get("username");const password = Cookies.get("password");const rememberMe = Cookies.get("rememberMe");loginForm.value = {username: username === undefined ? loginForm.value.username : username,password: password === undefined ? loginForm.value.password :decrypt(password),rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)};
}
getCookie();

记录成功
在这里插入图片描述

不选中就不会自动填充
在这里插入图片描述

主页面功能实现

主页面布局实现

<template><div class="app-wrapper"><el-container><el-aside width="200px" class="sidebar-container"><Menu/></el-aside><el-container><el-header><Header/></el-header><el-main><Tabs/></el-main><el-footer><Footer/></el-footer></el-container></el-container></div>
</template><script setup>
import Menu from '@/layout/menu'
import Header from '@/layout/header'
import Footer from '@/layout/footer'
import Tabs from '@/layout/tabs'
</script><style scoped>
.app-wrapper {position: relative;width: 100%;height: 100%;
}.sidebar-container {background-color: #2d3a4b;height: 100%;
}.el-container{height:100%
}.el-header{padding-left: 0px;padding-right: 0px;
}:deep(ul.el-menu){border-right-width: 0px
}</style>

新建文件
在这里插入图片描述
成功显示
在这里插入图片描述
LoginSuccessHandler里添加

  @Autowiredprivate SysUserService sysUserService;@Autowiredprivate SysRoleService sysRoleService;@Autowiredprivate SysMenuService sysMenuService;@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=UTF-8");ServletOutputStream outputStream = httpServletResponse.getOutputStream();String username=authentication.getName();String token = JwtUtils.genJwtToken(username);SysUser currentUser = sysUserService.getByUsername(username);// 根据用户id获取所有的角色信息List<SysRole> roleList = sysRoleService.list(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id=" + currentUser.getId()));// 遍历所有的角色,获取所有菜单权限 而且不重复Set<SysMenu> menuSet=new HashSet<>();for(SysRole sysRole:roleList){List<SysMenu> sysMenuList = sysMenuService.list(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id=" + sysRole.getId()));for(SysMenu sysMenu:sysMenuList){menuSet.add(sysMenu);}}List<SysMenu> sysMenuList=new ArrayList<>(menuSet);// 排序sysMenuList.sort(Comparator.comparing(SysMenu::getOrderNum));// 转菜单树List<SysMenu> menuList=sysMenuService.buildTreeMenu(sysMenuList);outputStream.write(JSONUtil.toJsonStr(R.ok("登录成功").put("authorization",token).put("currentUser",currentUser).put("menuList",menuList)).getBytes());outputStream.flush();outputStream.close();}

SysMenuService里新建方法
在这里插入图片描述
对应实现

    @Overridepublic List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList) {List<SysMenu> resultMenuList=new ArrayList<>();for(SysMenu sysMenu:sysMenuList){// 寻找子节点for(SysMenu e:sysMenuList){if(e.getParentId()==sysMenu.getId()){sysMenu.getChildren().add(e);}}if(sysMenu.getParentId()==0L){resultMenuList.add(sysMenu);}}return resultMenuList;}

SysMenu里添加字段

 @TableField(exist = false)private List<SysMenu> children=new ArrayList<>();

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<template><el-menuactive-text-color="#ffd04b"background-color="#2d3a4b"class="el-menu-vertical-demo"text-color="#fff"router:default-active="'/index'"><el-menu-item index="/index"><el-icon><home-filled /></el-icon><span>首页</span></el-menu-item><el-sub-menu :index="menu.path" v-for="menu in menuList"><template #title><el-icon><svg-icon :icon="menu.icon"/></el-icon><span>{{ menu.name }}</span></template><el-menu-item :index="item.path" v-for="item in menu.children"><el-icon><svg-icon :icon="item.icon"/></el-icon><span>{{ item.name }}</span></el-menu-item></el-sub-menu></el-menu>
</template><script setup>
import {HomeFilled,User,Tickets,Goods,DocumentAdd,Management,Setting,Edit,SwitchButton,Promotion} from '@element-plus/icons-vue'
import {ref} from 'vue'
import store from '@/store'const menuList=ref(store.getters.GET_MENULIST);
</script><style lang="scss" scoped></style>

显示成功
在这里插入图片描述

右上角用户头像显示实现

这里有个用户头像,虚拟路径映射配置下:
WebAppConfigurer类:

  @Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/image/userAvatar/**").addResourceLocations("file:D:\\codeWorkspace\\javaWorkspace\\data\\rightUserAvatar\\");}

在这里插入图片描述
Login.vue
在这里插入图片描述
在这里插入图片描述
avatar.vue

<template>
<el-dropdown><span class="el-dropdown-link"><el-avatar shape="square" :size="40" :src="squareUrl" />&nbsp;&nbsp;{{currentUser.username}}<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item @click="logout">安全退出</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template><script setup>
import { ArrowDown } from '@element-plus/icons-vue'
import {ref} from 'vue'
import store from '@/store'
import requestUtil,{getServerUrl} from '@/util/request'const currentUser=ref(store.getters.GET_USERINFO);const squareUrl=ref(getServerUrl()+'image/userAvatar/'+currentUser.value.avatar)const logout=async ()=>{let result=await requestUtil.get("/logout")if(result.data.code==200){store.dispatch('logout')}
}
</script><style lang="scss" scoped>
.el-dropdown-link {cursor: pointer;color: var(--el-color-primary);display: flex;align-items: center;
}
</style>

breadcrumb.vue

 <template>
面包屑
</template><script setup></script><style lang="scss" scoped></style>

index.vue

 <template><div class="navbar"><Breadcrumb/><div class="navbar-right"><Avatar/></div></div>
</template><script setup>
import Breadcrumb from './components/breadcrumb.vue'
import Avatar from './components/avatar.vue'
</script><style lang="scss" scoped>
.navbar {width: 100%;height: 60px;overflow: hidden;background-color: #F5F5F5;box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);padding: 0 16px;display: flex;align-items: center;box-sizing: border-box;position: relative;.navbar-right {flex: 1;display: flex;align-items: center;justify-content: flex-end;:deep(.navbar-item) {display: inline-block;margin-left: 18px;font-size: 22px;color: #5a5e66;box-sizing: border-box;cursor: pointer;}}
}
</style>

在这里插入图片描述

显示成功
在这里插入图片描述
在这里插入图片描述

登出成功
在这里插入图片描述

路由守卫功能实现

前端如果没有登录过,也就没有token,则自动跳转到登录页面,这个就是路由守卫。
我们通过 router.beforeEach((to, from, next)=>{}) 实现
router目录下新建permission.js
在这里插入图片描述

import router from "@/router/index"
import store from "@/store"router.beforeEach((to,from,next)=>{const whiteList=['/login'] // 白名单let token=store.getters.GET_TOKEN;if(token){next();}else{if(whiteList.includes(to.path)){next();}else{next("/login")}}
})

main.js添加代码
在这里插入图片描述

自动跳转到login页面

动态路由实现

我们vue路由信息,需要通过后端查询的menuList,动态设置到router里面去;
layout index.vue 加下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

修改permission.js

import router from "@/router/index"
import store from "@/store"router.beforeEach((to,from,next)=>{const whiteList=['/login'] // 白名单let token=store.getters.GET_TOKEN;let hasRoutes=store.state.hasRoutes;let menuList=store.getters.GET_MENULIST;if(token){if(!hasRoutes){bindRoute(menuList);store.commit("SET_ROUTES_STATE",true);}next();}else{if(whiteList.includes(to.path)){next();}else{next("/login")}}
})// 动态绑定路由
const bindRoute=(menuList)=>{let newRoutes=router.options.routes;menuList.forEach(menu=>{if(menu.children){menu.children.forEach(m=>{let route=menuToRoute(m,menu.name);if(route){newRoutes[0].children.push(route);}})}})// 重新添加到路由newRoutes.forEach(route=>{router.addRoute(route)})
}// 菜单对象转成路由对象
const menuToRoute=(menu,parentName)=>{if(!menu.component){return null;}else{let route={name:menu.name,path:menu.path,meta:{parentName:parentName}}route.component=()=>import('@/views/'+menu.component+'.vue');return route;}
}

修改router index.js
在这里插入图片描述

import { createRouter, createWebHashHistory } from 'vue-router'const routes = [{path: '/',component: () => import('../layout'),redirect:'/index',children:[{path: '/index',name: '首页',component: () => import('../views/index/index')},{path: '/userCenter',name: '个人中心',component: () => import('../views/userCenter/index')},]},{path: '/login',name: 'login',component: () => import('../views/Login.vue')}
]const router = createRouter({history: createWebHashHistory(),routes
})export default router

显示成功
在这里插入图片描述

动态标签实现

store添加默认tabs数组,以及默认选中的tab值,以及添加和充值tab
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
menu index.vue
在这里插入图片描述
在这里插入图片描述
修改tabs index.vue

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
显示成功
在这里插入图片描述
修改代码tabs index.vue

<template><el-tabsv-model="editableTabsValue"type="card"class="demo-tabs"closable@tab-remove="removeTab"@tab-click="clickTab"><el-tab-panev-for="item in editableTabs":key="item.name":label="item.title":name="item.name">{{ item.content }}</el-tab-pane></el-tabs>
</template>
<script  setup>
import { ref,watch } from 'vue'
import  store from '@/store'import {useRouter} from 'vue-router'
const router=useRouter();const editableTabsValue = ref(store.state.editableTabsValue)
const editableTabs = ref(store.state.editableTabs)const addTab = (targetName) => {const newTabName = `${++tabIndex}`editableTabs.value.push({title: 'New Tab',name: newTabName,content: 'New Tab content',})editableTabsValue.value = newTabName
}const removeTab = (targetName) => {const tabs = editableTabs.valuelet activeName = editableTabsValue.valueif(activeName==='/index'){return}if (activeName === targetName) {tabs.forEach((tab, index) => {if (tab.name === targetName) {const nextTab = tabs[index + 1] || tabs[index - 1]if (nextTab) {activeName = nextTab.name}}})}editableTabsValue.value = activeNameeditableTabs.value = tabs.filter((tab) => tab.name !== targetName)store.state.editableTabsValue=editableTabsValue.value;store.state.editableTabs=editableTabs.value;router.push({path:activeName})
}
const clickTab=(target)=>{console.log("target.props.label="+target.props.label)router.push({name:target.props.label})
}const refreshTabs=()=>{editableTabsValue.value=store.state.editableTabsValue;editableTabs.value=store.state.editableTabs;
}watch(store.state,()=>{refreshTabs();
},{deep:true,immediate:true})
</script>
<style>
.demo-tabs > .el-tabs__content {padding: 32px;color: #6b778c;font-size: 32px;font-weight: 600;
}.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{background-color: lightgray;
}</style>

动态面包屑实现

breadcrumb.vue

<template><el-icon><HomeFilled /></el-icon><el-breadcrumb separator="/"><el-breadcrumb-item v-for="(item,index) in breadcrumbList"><span class="root" v-if="parentName && index>0">{{parentName}}&nbsp;&nbsp;/&nbsp;&nbsp;</span><span v-if="index==breadcrumbList.length-1">{{item.name}}</span><span class="root" v-else>{{item.name}}</span></el-breadcrumb-item></el-breadcrumb>
</template><script setup>
import {HomeFilled} from '@element-plus/icons-vue'
import {useRoute} from 'vue-router'
import { ref ,watch} from 'vue'
import store from "@/store";const route=useRoute();
const breadcrumbList=ref([])
const parentName=ref("")const initBreadcrumbList=()=>{breadcrumbList.value=route.matched;parentName.value=route.meta.parentName;
}watch(route,()=>{initBreadcrumbList();
},{deep:true,immediate:true})</script><style lang="scss" scoped>.root{color:#666;font-weight:600;
}
</style>

显示成功
在这里插入图片描述

个人中心功能实现

路由与导航动态绑定实现

App.vue加上下面这个 监控route,动态添加标签

import store from '@/store'
import { ref ,watch} from 'vue'
import { useRoute,useRouter } from 'vue-router'
const route=useRoute();
const router=useRouter();
const whitePath=['/login','/index','/']watch(route,(to,from)=>{console.log("to"+to.name)console.log(to.path)if (whitePath.indexOf(to.path)===-1) {console.log("to.path="+to.path)let obj = {name: to.name,path: to.path}store.commit("ADD_TABS", obj)}},{deep:true,immediate:true})

src/layout/menu/index.vue里修改代码
在这里插入图片描述

const activeIndex=ref("/index")
watch(store.state,()=>{console.log("editableTabsValue="+store.state.editableTabsValue)activeIndex.value=store.state.editableTabsValue
},{deep:true,immediate:true})

同步在这里插入图片描述

avatar.vue加下router-link
在这里插入图片描述

个人中心页面构建实现

<template><div class="app-container"><el-row :gutter="20"><el-col :span="6"><el-card class="box-card"><template v-slot:header><div class="clearfix"><span>个人信息</span></div></template><div><div class="text-center">修改头像</div><ul class="list-group list-group-striped"><li class="list-group-item"><svg-icon icon="user" />&nbsp;&nbsp;用户名称<div class="pull-right">{{currentUser.username}}</div></li><li class="list-group-item"><svg-icon icon="phone" />&nbsp;&nbsp;手机号码<div class="pull-right">{{currentUser.phonenumber}}</div></li><li class="list-group-item"><svg-icon icon="email" />&nbsp;&nbsp;用户邮箱<div class="pull-right">{{currentUser.email}}</div></li><li class="list-group-item"><svg-icon icon="peoples" />&nbsp;&nbsp;所属角色<div class="pull-right">{{currentUser.roles}}</div></li><li class="list-group-item"><svg-icon icon="date" />&nbsp;&nbsp;创建日期<div class="pull-right">{{formatDate(currentUser.loginDate)}}</div></li></ul></div></el-card></el-col><el-col :span="18"><el-card><template v-slot:header><div class="clearfix"><span>基本资料</span></div></template><el-tabs v-model="activeTab"><el-tab-pane label="基本资料" name="userinfo">基本资料</el-tab-pane><el-tab-pane label="修改密码" name="resetPwd">修改密码</el-tab-pane></el-tabs></el-card></el-col></el-row></div>
</template>
<script setup>
import {ref} from 'vue'
import store from '@/store'
import { formatDate } from '@/util/formatDate.js'
const currentUser = ref(store.getters.GET_USERINFO);const activeTab = ref("userinfo");
</script>
<style lang="scss" scoped>
.list-group-striped>.list-group-item {border-left: 0;border-right: 0;border-radius: 0;padding-left: 0;padding-right: 0;
}
.list-group-item {border-bottom: 1px solid #e7eaec;border-top: 1px solid #e7eaec;margin-bottom: -1px;padding: 11px 0;font-size: 13px;
}
.pull-right{float: right!important;
}
::v-deep .el-card__body{height:230px;
}
::v-deep .box-card{height:450px;
}
</style>

在这里插入图片描述

个人中心页面数据显示

新建 avatar.vue resetPwd.vue userInfo.vue
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
SysUser添加role属性

/**
* 所属角色
*/
@TableField(exist = false)
private String roles;

LoginSuccessHandler.java加上

currentUser.setRoles(roleList.stream().map(SysRole::getName).collect(Collectors.
joining(",")));

重新登录
在这里插入图片描述

基本资料修改功能实现

userInfo.vue

<template>
<el-form ref="userRef" :model="form" :rules="rules" label-width="100px" ><el-form-item label="手机号码:" prop="phonenumber"><el-input v-model="form.phonenumber" maxlength="11" /></el-form-item><el-form-item label="用户邮箱:" prop="email"><el-input v-model="form.email" maxlength="50" /></el-form-item><el-form-item><el-button type="primary" @click="handleSubmit">保存</el-button></el-form-item></el-form>
</template><script setup>
import {defineProps, ref} from "vue";
import requestUtil from "@/util/request";
import { ElMessage } from 'element-plus'
import store from "@/store";const props=defineProps({user:{type:Object,default:()=>{},required:true}}
)const form=ref({id:-1,phonenumber:'',email:''
})const userRef=ref(null)const rules = ref({email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
});form.value=props.user;const handleSubmit=()=>{userRef.value.validate(async (valid)=>{if(valid) {let result = await requestUtil.post("sys/user/save", form.value);let data = result.data;if (data.code == 200) {ElMessage.success("执行成功!")store.commit("SET_USERINFO", form.value)}}})
}</script><style lang="scss" scoped></style>

父页面传参
在这里插入图片描述
新建SysUserController

 package com.java.controller;import com.java.entity.R;
import com.java.entity.SysUser;
import com.java.service.SysUserService;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** 用户Controller控制器*/
@RestController
@RequestMapping("/sys/user")
public class SysUserController {@Autowiredprivate SysUserService sysUserService;/*** 添加或者修改* @param sysUser* @return*/@PostMapping("/save")@PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user:edit')")public R save(@RequestBody SysUser sysUser){if(sysUser.getId()==null || sysUser.getId()==-1){}else{sysUser.setUpdateTime(new Date());sysUserService.updateById(sysUser);}return R.ok();}}

修改成功
在这里插入图片描述

修改密码功能实现

resetPwd.vue

 <template>
<el-form ref="pwdRef" :model="form" :rules="rules" label-width="80px"><el-form-item label="旧密码" prop="oldPassword"><el-input v-model="form.oldPassword" placeholder="请输入旧密码" type="password" show-password /></el-form-item><el-form-item label="新密码" prop="newPassword"><el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" show-password /></el-form-item><el-form-item label="确认密码" prop="confirmPassword"><el-input v-model="form.confirmPassword" placeholder="请确认密码" type="password" show-password/></el-form-item><el-form-item><el-button type="primary" @click="handleSubmit">保存</el-button></el-form-item></el-form>
</template><script setup>
import {defineProps, ref} from "vue";
import requestUtil from "@/util/request";
import { ElMessage } from 'element-plus'
import store from "@/store";const props=defineProps({user:{type:Object,default:()=>{},required:true}}
)const form=ref({id:-1,oldPassword:'',newPassword:'',confirmPassword:''
})const pwdRef=ref(null)form.value=props.user;const equalToPassword = (rule, value, callback) => {if (form.value.newPassword !== value) {callback(new Error("两次输入的密码不一致"));} else {callback();}
};const rules = ref({oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }],confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
});const handleSubmit=()=>{pwdRef.value.validate(async (valid)=>{if(valid) {let result = await requestUtil.post("sys/user/updateUserPwd", form.value);let data = result.data;if (data.code == 200) {ElMessage.success("密码修改成功,下一次登录生效!")store.commit("SET_USERINFO", form.value)}else{ElMessage.error(data.msg)}}})
}</script><style lang="scss" scoped></style>
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;

SysUser

 /**
* 确认新密码
*/
@TableField(exist = false)
private String newPassword;
/**
* 旧密码(前端传来的)
*/
@TableField(exist = false)
private String oldPassword;

后端SysUserController.java

 /*** 修改密码
* @param sysUser
* @return
*/
@PostMapping("/updateUserPwd")
@PreAuthorize("hasAuthority('system:user:edit')")
public R updateUserPwd(@RequestBody SysUser sysUser){
SysUser currentUser = sysUserService.getById(sysUser.getId());
if(bCryptPasswordEncoder.matches(sysUser.getOldPassword(),currentUser.getPasswo
rd())){
currentUser.setPassword(bCryptPasswordEncoder.encode(sysUser.getNewPassword()))
;
currentUser.setUpdateTime(new Date());
sysUserService.updateById(currentUser);
}else{
return R.error("输入旧密码错误!");
}
return R.ok();
}

头像更换功能实现

avatar.vue

 <template><el-formref="formRef":model="form"label-width="100px"style="text-align: center;padding-bottom:10px"><el-upload:headers="headers"class="avatar-uploader":action="getServerUrl()+'sys/user/uploadImage'":show-file-list="false":on-success="handleAvatarSuccess":before-upload="beforeAvatarUpload"><img v-if="imageUrl" :src="imageUrl" class="avatar" /><el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon></el-upload><el-button @click="handleConfirm" >确认更换</el-button></el-form></template><script setup>import {defineProps, ref} from "vue";
import requestUtil,{getServerUrl} from "@/util/request";
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import store from "@/store";const props=defineProps({user:{type:Object,default:()=>{},required:true}}
)const headers=ref({token:store.getters.GET_TOKEN
})const form=ref({id:-1,avatar:''
})const formRef=ref(null)const imageUrl=ref("")form.value=props.user;
imageUrl.value=getServerUrl()+'image/userAvatar/'+form.value.avatarconst handleAvatarSuccess=(res)=>{imageUrl.value=getServerUrl()+res.data.srcform.value.avatar=res.data.title;
}const beforeAvatarUpload = (file) => {const isJPG = file.type === 'image/jpeg'const isLt2M = file.size / 1024 / 1024 < 2if (!isJPG) {ElMessage.error('图片必须是jpg格式')}if (!isLt2M) {ElMessage.error('图片大小不能超过2M!')}return isJPG && isLt2M
}const handleConfirm=async()=>{let result=await requestUtil.post("sys/user/updateAvatar",form.value);let data=result.data;if(data.code==200){ElMessage.success("执行成功!")store.commit("SET_USERINFO",form.value)}else{ElMessage.error(data.msg);}}</script><style>.avatar-uploader .el-upload {border: 1px dashed #d9d9d9;border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;
}
.avatar-uploader .el-upload:hover {border-color: #409eff;
}
.el-icon.avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 178px;height: 178px;text-align: center;
}
.avatar {width: 120px;height: 120px;display: block;
}</style>
avatarImagesFilePath: D://java1234-admin2/userAvatar/
@Value("${avatarImagesFilePath}")
private String avatarImagesFilePath;
/*** 上传用户头像图片* @param file* @return* @throws Exception*/@RequestMapping("/uploadImage")@PreAuthorize("hasAuthority('system:user:edit')")public Map<String,Object> uploadImage(MultipartFile file)throws Exception{Map<String,Object> resultMap=new HashMap<>();if(!file.isEmpty()){// 获取文件名String originalFilename = file.getOriginalFilename();String suffixName=originalFilename.substring(originalFilename.lastIndexOf("."));String newFileName= DateUtil.getCurrentDateStr()+suffixName;System.out.println("上传文件名:"+originalFilename);System.out.println(" newFileName:"+newFileName);System.out.println(" avatarImagesFilePath:"+avatarImagesFilePath);FileUtils.copyInputStreamToFile(file.getInputStream(),new File(avatarImagesFilePath+newFileName));resultMap.put("code",0);resultMap.put("msg","上传成功");Map<String,Object> dataMap=new HashMap<>();dataMap.put("title",newFileName);dataMap.put("src","image/userAvatar/"+newFileName);resultMap.put("data",dataMap);}return resultMap;}/*** 修改用户头像* @param sysUser* @return*/@RequestMapping("/updateAvatar")@PreAuthorize("hasAuthority('system:user:edit')")public R updateAvatar(@RequestBody SysUser sysUser){SysUser currentUser = sysUserService.getById(sysUser.getId());currentUser.setUpdateTime(new Date());currentUser.setAvatar(sysUser.getAvatar());sysUserService.updateById(currentUser);return R.ok();}

修改成功
在这里插入图片描述

相关文章:

  • OPENCV图形计算面积、弧长API讲解(1)
  • DBAPI如何优雅的获取单条数据
  • JavaScript 数据类型详解
  • 基于深度强化学习的智能机器人导航系统
  • 骨盆-x光参数
  • Linux多线程-进阶
  • 湖北理元理律师事务所视角:企业债务优化的三维平衡之道
  • 在uniCloud云对象中定义dbJQL的便捷方法
  • 免杀对抗--PE文件结构
  • 大实验:基于赛灵思csg324100T,pmodMAXsonar的危险距离警报
  • NumPy数组访问
  • MySQL从入门到DBA深度学习指南
  • 算法-数论
  • 每日八股文6.8
  • 通过Cline使用智能体
  • WebFuture 升级提示“不能同时包含聚集KEY和大字段””的处理办法
  • DDR供电设计中的VTT与VREF作用和区别
  • 深究二分查找算法:从普通到进阶
  • 【AIGC】RAGAS评估原理及实践
  • 可可·香奈儿 活出自己
  • 潍坊市网站建设/b站黄页推广
  • 一个工厂做网站有用吗/免费网络推广软件有哪些
  • 三鼎网络网站建设/软文营销经典案例
  • 招才猫网站多少钱做的/快排seo
  • 邢台做网站推广价格/南宁seo外包靠谱吗
  • 衡水seo_衡水网站建设-燕丰收/免费seo免费培训