SpringSecurity+vue通用权限系统

发布于:2025-06-09 ⋅ 阅读:(22) ⋅ 点赞:(0)

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: 80
  servlet:
    context-path: /

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_admin3?serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  redis: # redis配置
    host: 127.0.0.1 # IP
    port: 6379  # 端口
    password:  # 密码
    connect-timeout: 10s  # 连接超时时间
    lettuce: # lettuce redis客户端配置
      pool: # 连接池配置
        max-active: 8  # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-wait: 200s  # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-idle: 8 # 连接池中的最大空闲连接 默认 8
        min-idle: 0 # 连接池中的最小空闲连接 默认 0


mybatis-plus:
  global-config:
    db-config:
      id-type: auto
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-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 {

    @Autowired
    private 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 {
        //小明失效 10s
        String 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_TOKEN
    return 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 {

    @Override
    public 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-input

            type="text"
            size="large"
            auto-complete="off"
            placeholder="账号"
        >

        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input

            type="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-button
            size="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/**"
    } ;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    protected 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 {

    @Override
    public 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 {


    @Override
    public 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.authorization
        store.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 {

    @Autowired
    SysUserService sysUserService;

    @Override
    public 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

    @Override
    public 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 {

    @Autowired
    SysUserService sysUserService;

    @Override
    public 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.authorization
        store.commit('SET_TOKEN',token);
        router.replace("/")
      }else{
        ElMessage.error(data.msg)
      }
    }else{
      console.log("验证失败")
    }
  })
}

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

  @Bean
    JwtAuthenticationFilter 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 {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private MyUserDetailsServiceImpl myUserDetailsService;

    private static final String URL_WHITELIST[] ={
            "/login",
            "/logout",
            "/captcha",
            "/password",
            "/image/**"
    } ;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected 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 {

    @Override
    public 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里添加配置

  @Autowired
    private 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 {
    @Override
    public 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添加配置

 @Autowired
    private 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>{

	@Override
	public 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:edit
        String 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{


    @Autowired
    SysRoleMapper sysRoleMapper;

    @Autowired
    SysMenuMapper sysMenuMapper;
    @Override
    public SysUser getByUsername(String username) {
        return getOne(new QueryWrapper<SysUser>().eq("username",username));
    }

    @Override
    public 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里添加

  @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysRoleService sysRoleService;

    @Autowired
    private SysMenuService sysMenuService;


  @Override
    public 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里新建方法
在这里插入图片描述
对应实现

    @Override
    public 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-menu
        active-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类:

  @Override
    public 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-tabs
    v-model="editableTabsValue"
    type="card"
    class="demo-tabs"
    closable
    @tab-remove="removeTab"
    @tab-click="clickTab"
  >
    <el-tab-pane
      v-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.value
  let activeName = editableTabsValue.value

  if(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 = activeName
  editableTabs.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 {

    @Autowired
    private 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-form
        ref="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.avatar

const handleAvatarSuccess=(res)=>{
  imageUrl.value=getServerUrl()+res.data.src
  form.value.avatar=res.data.title;
}


const beforeAvatarUpload = (file) => {
  const isJPG = file.type === 'image/jpeg'
  const isLt2M = file.size / 1024 / 1024 < 2

  if (!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();
    }

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


网站公告

今日签到

点亮在社区的每一天
去签到