前后端分离 权限系统[SpringBoot2+SpringSecurity+Vue+ElementPlus]

前言:创建一个根目录管理前端和后端的项目 比如 E:\javaStudy\SOFAB2

gitee仓库地址[项目完整代码都在里面了]:SOFAB: 权限管理系统

搭建后端系统架构

1.创建jd-admin项目

2.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>
    <groupId>com.jd</groupId>
    <artifactId>jd-admin</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jd-admin</name>
    <description>jd-admin</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.2.RELEASE</spring-boot.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.12</version>
        </dependency>

        <!--mp启动器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</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>4.3.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>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--redis缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--lettuce poll 缓存连接池-->
        <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>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

 3.在resources目录下创建application.yml文件

server:
  port: 81
  servlet:
    context-path: /

spring:
  datasource: #mysql数据连接池配置
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/db_admin?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    username: root
    password: yr
  redis: #redis配置
    host: 192.168.200.130 #ip
    port: 6379 #端口
    password: yr #密码
    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 #置自动映射行为为 full:会自动映射所有的结果集,无论是否有嵌套结果集映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志输出
  mapper-locations: classpath:mapper/*.xml #XXXMapper.xml文件存放路径

 4.注意redis要启动 且ip是192.168.200.130[我是把redis安装在了linux虚拟机上 可以使用ip address查看ip]

 5.创建数据库和表

# 新建数据库 db_admin
CREATE DATABASE jd_admin;
USE jd_admin;

# 新建用户表
CREATE TABLE `sys_user` (
`id` BIGINT(20) NOT NULL PRIMARY KEY 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 '备注'
)ENGINE=INNODB CHARSET = utf8;

#测试数据

INSERT INTO
`sys_user`(`id`,`username`,`password`,`avatar`,`email`,`phonenumber`,`login_date`,`status`,`create_time`,`update_time`,`remark`)
 VALUES
(1,'tom','$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');
 

 

6.使用MybatisX插件快速生成 entity、mapper、service

 

 

 此时便可以看到该库下的表和字段了

 

 

 

好啦生成完毕!

 7.创建封装返回json的数据的通用类

package com.yr.java123admin.entity;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @author java大帝
 * @ClassName R
 * @description 封装返回参数的类,用于统一处理应用程序的响应结果。
 *              该类包含了消息、状态码和扩展数据等属性,以及一系列静态方法用于创建不同状态的响应对象。
 * @date 2025年01月13日
 * @version 1.0
 */
@Data
public class R {
    // 消息
    private String msg;
    // 状态码
    private Integer code;
    // 数据
    private Map<String, Object> data = new HashMap<>();

    /**
     * 创建一个表示成功的R对象,默认消息为 "success",状态码为 200。
     *
     * @return 表示成功的R对象。
     */
    public static R success() {
        R r = new R();
        r.setMsg("success");
        r.setCode(200);
        return r;
    }

    /**
     * 创建一个表示成功的R对象,自定义成功消息,状态码为 200。
     *
     * @param msg 自定义的成功消息。
     * @return 表示成功的R对象。
     */
    public static R success(String msg) {
        R r = new R();
        r.setMsg(msg);
        r.setCode(200);
        return r;
    }

    /**
     * 创建一个表示成功的R对象,带有扩展数据,默认消息为 "success",状态码为 200。
     *
     * @param extend 包含扩展数据的Map。
     * @return 表示成功的R对象。
     */
    public static R success(Map<String, Object> extend) {
        R r = new R();
        r.setMsg("success");
        r.setCode(200);
        r.setData(extend);
        return r;
    }

    /**
     * 创建一个表示失败的R对象,默认消息为 "未知异常 请联系管理员!",状态码为 500。
     *
     * @return 表示失败的R对象。
     */
    public static R fail() {
        R r = new R();
        r.setMsg("未知异常 请联系管理员!");
        r.setCode(500);
        return r;
    }

    /**
     * 创建一个表示失败的R对象,自定义失败消息,状态码为 500。
     *
     * @param msg 自定义的失败消息。
     * @return 表示失败的R对象。
     */
    public static R fail(String msg) {
        R r = new R();
        r.setMsg(msg);
        r.setCode(500);
        return r;
    }

    public static R fail(Integer code,String msg) {
        R r = new R();
        r.setMsg(msg);
        r.setCode(code);
        return r;
    }

    public R put(String key, Object val) {
        this.data.put(key, val);
        return this;
    }
}

8.创建controlelr.TestController 

package com.yr.java123admin.controller;

import com.yr.java123admin.entity.R;
import com.yr.java123admin.entity.SysUser;
import com.yr.java123admin.service.SysUserService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;

/**
 * @author java大帝
 * @ClassName TestController
 * @description: 测试包
 * @date 2025年 01月 13日
 * @version: 1.0
 */
@RestController
@RequestMapping("/test")
public class TestController {
    @Resource
    private SysUserService sysUserService;

    @RequestMapping("list")
    public R userList(){
        HashMap<String, Object> resultMap = new HashMap<>();
        List<SysUser> list = sysUserService.list();
        resultMap.put("userList",list);
        return R.success(resultMap);
    }

}

9.修改一下启动类并使用浏览器测试

package com.jd.jdadmin;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.jd.jdadmin.mapper") //扫描XXXMapper类
public class JdAdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(JdAdminApplication.class, args);
    }

}

搭建前端系统架构

1.使用命令创建vue3

 

 2.使用idea打开SOFAB2

 3.启动前端项目

 

使用浏览器访问

4.安装axios和elementPlus依赖

5.测试elementPlus是否安装成功

        在main.ts中引入

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)

        在App.vue中使用elementPlus的按钮组件

<template>
  <div>
    <el-button type="danger">猜猜我是谁</el-button>
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped>

</style>

         浏览器访问

JWT前后端交互

        Json Web Token(jwt),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)

        JWT就是一段字符串,用来进行用户身份认证的凭证,该字符串分成三段[头部、载荷、凭证]

           1.创建common.constant包 并创建JwtConstant类

       

package com.jd.jdadmin.common.constant;

/**
 * @author java大帝
 * @ClassName JwtConstant
 * @description: 常量类
 * @date 2025年 01月 13日
 * @version: 1.0
 */
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 = "1ba@842as45*74s?1p41-15a*s/dj;"; // 密钥
    public static final long JWT_TTL = 24* 60 * 60 * 1000; //  24小时
}

2.在entity包下创建CheckResult类

package com.jd.jdadmin.entity;

import io.jsonwebtoken.Claims;

/**
 * @author java大帝
 * @ClassName CheckResult
 * @description: 认证结果
 * @date 2025年 01月 13日
 * @version: 1.0
 */
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;
    }
}

3.创建util包 并创建JwtUtils类 

package com.jd.jdadmin.util;

import cn.hutool.core.codec.Base64;
import com.jd.jdadmin.common.constant.JwtConstant;
import com.jd.jdadmin.entity.CheckResult;
import io.jsonwebtoken.*;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;

/**
 * @author java大帝
 * @ClassName JwtUtils
 * @description: Jwt工具类
 * @date 2025年 01月 13日
 * @version: 1.0
 */
public class JwtUtils {
    // 生成JWT
    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 jwtBuilder = Jwts.builder()
                .setId(id)
                .setSubject(subject) // 主体
                .setIssuer("yr") // 签发者
                .setIssuedAt(now) // 签发时间
                .signWith(signatureAlgorithm, secretKey);// 签名算法以及密钥
        if (ttlMillis >= 0){
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            jwtBuilder.setExpiration(expDate); //设置过期时间
        }
        return  jwtBuilder.compact();
    }

    // 生成jwt token
    public static String genJwtToken(String username){return createJWT(username,username,60 * 60 * 1000);}

    // 验证JWT
    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
    public static SecretKey generalKey(){
        byte[] encodeKey = Base64.decode(JwtConstant.JWT_SECERT);
        SecretKeySpec key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES");
        return key;
    }

    // 解析JWT字符串
    public static Claims parseJWT(String jwt){
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

    public static void main(String[] args) {
        // 小明失效 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());

        System.out.println(validateJWT(sc).getClaims());

    }
}

测试一下

 3.发请求测试login登录返回token

        3.1后端:创建测试类controller.TestController

@RestController
@RequestMapping("/test")
public class TestController {
    @Resource
    private SysUserService sysUserService;

    @RequestMapping("list")
    public R userList(@RequestHeader(required = false) String token){
        if (StrUtil.isNotEmpty(token)){
            HashMap<String, Object> resultMap = new HashMap<>();
            List<SysUser> list = sysUserService.list();
            resultMap.put("userList",list);
            return R.success(resultMap);
        }
        return R.fail(401,"无权访问");
    }

    @RequestMapping("login")
    public R login(){
        String token = JwtUtils.genJwtToken("java123456");
        return R.success().put("token",token);
    }

}

        3.2 后端:创建util.StrUtil字符串的工具类

package com.jd.jdadmin.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author java大帝
 * @ClassName StrUtil
 * @description: 字符串工具类
 * @date 2025年 01月 13日
 * @version: 1.0
 */
public class StrUtil {
    /**
     * 判断是否是空
     * @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();
    }


}

 3.3前端:二次封装axios

// 引入axios
import axios from 'axios'
import {ElMessage} from "element-plus"
import useUserStore from "@/stores/modules/user"
//基础路径
export let baseUrl = 'http://localhost:81/'

const request = axios.create({
    baseURL:baseUrl, // 基础路径
    timeout:5000 // 超时时间
})

// 请求拦截器
request.interceptors.request.use(config =>  {
    let userStore = useUserStore()
    // 在发送请求之前做些什么 比如携带token请求头
    config.headers.token = userStore.token
    return config
},err => {
    return Promise.reject(err)
})

// 响应拦截器
request.interceptors.response.use(resp => {
    console.log(resp)
    return resp.data
},err => {
    //错误处理
    let message = ''
    let status = err.response.status
    switch (status){
        case 401:
            message = 'token过期'
            break
        case 403:
            message = '无权访问'
            break
        case 404:
            message='请求地址有误'
            break
        case 500:
            message = '服务器出错'
            break
        default:
            message = '网络错误'
            break
    }
    ElMessage({
        type:'error',
        message
    })
})
export default request

 3.4前端:创建用户相关小仓库

 大仓库

import {createPinia} from "pinia"

let pinia = createPinia()

export default pinia

         在main.ts中引入大仓库并使用

import pinia from "@/stores"
app.use(pinia) 

        小仓库

import { defineStore } from "pinia"
import request from "@/util/request"
import {GET_TOKEN, SET_TOKEN} from "@/util/token";


const useUserStore = defineStore('User',{
    state(){
        return {
            token:GET_TOKEN()
        }
    },
    actions:{
        async userLogin(){
            let res = await request.get('test/login')
            if (res.code == 200){
                SET_TOKEN(res.data.token)
                return 'ok'
            } else {
                return Promise.reject(res.msg)
            }
        }
    }
})

export default useUserStore

3.5 创建token工具类

export const SET_TOKEN = (token:string)=> {
    localStorage.setItem("TOKEN",token)
}

export const GET_TOKEN = ()=> {
    return localStorage.getItem("TOKEN")
}

export const REMOVE_TOKEN = ()=> {
    return localStorage.removeItem("TOKEN")
}

3.6App.vue中写相关测试代码

<template>
  <div>
    <el-button type="danger" @click="handleLogin">测试登录</el-button>
    <el-button type="primary" @click="handleUserList">测试获取用户请求</el-button>
  </div>
</template>

<script setup lang="ts">
import useUserStore from "@/stores/modules/user"
import request from "@/util/request"


let userStore = useUserStore()
const handleLogin = async () => {
  await userStore.userLogin()
}

const handleUserList = async () => {
  let res = await request.get('test/list')
  if (res.code == 200) {
    console.log('userlist',res)
  }
}
</script>

<style scoped>

</style>

 3.7浏览器访问出现跨域问题

 3.8创建config.WebAppConfigurer解决跨域

        

package com.jd.jdadmin.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author java大帝
 * @ClassName WebAppConfigurer
 * @description: 跨域解决
 * @date 2025年 01月 13日
 * @version: 1.0
 */
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET","HEAD","POST","DELETE","OPTIONS")
                .maxAge(3600);
    }
}

 测试成功

 登录功能实现

        前端静态登录页面实现

                1.准备两个样式文件

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

/* http://meyerweb.com/eric/tools/css/reset/
   v2.0 | 20110126
   License: none (public domain)
*/

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
    display: block;
}
html{
    font-size: 12px;
}
body {
    line-height: 1;
}
ol, ul {
    list-style: none;
}
blockquote, q {
    quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}

 2.准备好背景图片

 网址:The night sky over a mountain range with stars in the sky photo – Free Chamonix Image on Unsplash

3. 在main.ts中引入两个样式文件

// 引入样式文件
import '@/assets/styles/border.css'
import '@/assets/styles/reset.css'

4. 修改router/index.ts

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

export default router

5.创建Login.vue

<script setup>

const handleLogin = () => {

}
</script>

<template>
<div class="login">

    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">java大帝 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"
            show-password
        >

        </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">java大帝.vip</a> 版权所有.</span>
    </div>
  </div>
</template>


<style lang="scss" scoped>
a{
  color:white
}
.login {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/bg.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>

6.安装scss

npm install sass sass-loader --save-dev   

7.修改App.vue 

<template>
  <router-view />
</template>

<script setup lang="ts">

</script>

<style>
html,body,#app {
  height: 100%;
}

.app-container {
  padding: 20px;
}
</style>

8.浏览器访问 

 全局注册Element-Plus的图标组件
 

1.安装element-plus icon

npm install @element-plus/icons-vue

2.将其注册为全局组件

import * as ElementPlusIconsVue from '@element-plus/icons-vue'

export  default {
    install(app:any){
        //@ts-ignore
        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
            app.component(key, component)
        }
    }
}

3.在main.ts中引入钩子并使用 

// 引入钩子
import globalComponent from '@/components/index'
app.use(globalComponent)

4.在Login.vue的表单项中使用

<el-form-item prop="username">

  <el-input
      prefix-icon="User"
      type="text"
      size="large"
      auto-complete="off"
      placeholder="账号"
  >

  </el-input>
</el-form-item>
<el-form-item prop="password">
  <el-input
      prefix-icon="Lock"
      type="password"
      size="large"
      auto-complete="off"
      placeholder="密码"
      @keyup.enter="handleLogin"
      show-password
  >

  </el-input>
</el-form-item>

SpringSecurity执行原理概述

        SpringSecurity简单原理

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安全上下文容器将第三步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(...)方法 设置到其中,可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager,而SpringSecurity支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的,web表单对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取,最终AuthenticationProvider将UserDetails填充至Authentication

授权管理

访问资源(即授权管理),访问url时,会通过FilterSecurityInterceptor拦截器拦截,其中会调用SecurityMetadataSource的方法来获取被拦截url所需的全部权限,再调用授权管理器AccessDecisionManager,这个授权管理器会通过Spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的投票策略(有:一票否定、一票决定、少数服从多数等),如果权限足够,则决策通过,返回访问资源,请求放行,否则跳转到403页面、自定义页面。

详细执行流程

项目整合SpringSecurity 

 1.在pom.xml中加入SpringSecurity依赖

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

2.创建config.SecurityConfig类

package com.jd.jdadmin.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;

/**
 * @author java大帝
 * @ClassName SecurityConfig
 * @description: TODO
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用了基于方法的安全性控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String URI_WHITELIST[] = {
            "/login",
            "/logout",
            "captcha",
            "/password",
            "/image/**",
            "/test/**"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置
        http
                .cors()
                .and()
                .csrf()
                .disable()
                .formLogin()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
                .and()
                .authorizeRequests()
                .antMatchers(URI_WHITELIST)                             // 白名单
                .permitAll()
                .anyRequest()
                .authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth); // 使用默认的用户名和密码[内置]
    }
}

3.启动测试 此时浏览器访问http://localhost:81/test/list会报错 因为必须先登录

控制台会打印出密码

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

1.创建common.security包 和 LoginSuccessHandler类

package com.jd.jdadmin.common.security;

import cn.hutool.json.JSONUtil;
import com.jd.jdadmin.entity.R;
import com.jd.jdadmin.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;

/**
 * @author java大帝
 * @ClassName LoginSuccessHandler
 * @description: 登录成功后的处理器
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@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();

        // 目前使用SpringSecurity生成的user用户名
        String username = "user";
        String token = JwtUtils.genJwtToken(username);

        outputStream.write(JSONUtil.toJsonStr(R.success().put("authorization",token)).getBytes());

        outputStream.flush();
        outputStream.close();
    }
}

2.创建security.LoginFailureHandler类

package com.jd.jdadmin.common.security;

import cn.hutool.json.JSONUtil;
import com.jd.jdadmin.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;


/**
 * @author java大帝
 * @ClassName LoginSuccessHandler
 * @description: 登录失败后的处理器
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@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.fail(message)).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

3.在 SecurityConfig类中将登录成功和失败的处理器配置进去

package com.jd.jdadmin.config;

import com.jd.jdadmin.common.security.LoginFailureHandler;
import com.jd.jdadmin.common.security.LoginSuccessHandler;
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;

import javax.annotation.Resource;

/**
 * @author java大帝
 * @ClassName SecurityConfig
 * @description: TODO
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用了基于方法的安全性控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private LoginSuccessHandler loginSuccessHandler;

    @Resource
    private LoginFailureHandler loginFailureHandler;
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置
        http
                .cors()
                ...
                .formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                ...;
    }

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

4.前台:创建api统一管理项目接口

 创建user文件夹 存储用户相关API接口

在user文件夹下创建index.ts和type.ts

 index.ts

// 用户相关的API接口
import request from "@/util/request"
import type {UserFormData, UserLoginResponseData} from "@/api/user/type"
enum API {
    LOGIN_URL = 'login'
}

export const reqUserLogin = (data:UserFormData) => request.post<any,UserLoginResponseData>(API.LOGIN_URL + `?username=${data.username}&password=${data.password}`)

type.ts

export interface ResponseData {
    msg:string
    code:number
}

export interface UserFormData {
    username:string,
    password:string
}

export interface UserLoginResponseData extends ResponseData {
    data:{
        authorization:string
    }
}

 5.在仓库中发登录请求[存储token]

import { defineStore } from "pinia"
import {GET_TOKEN, SET_TOKEN} from "@/util/token";
import {reqUserLogin} from "@/api/user";
import type {UserFormData} from "@/api/user/type";


const useUserStore = defineStore('User',{
    state(){
        return {
            token:GET_TOKEN()
        }
    },
    actions:{
        async userLogin(data:UserFormData){
            let res = await reqUserLogin(data)
            if (res.code == 200){
                 this.token = res.data.authorization                  SET_TOKEN(res.data.authorization) // 存储token
                return 'ok'
            } else {
                return Promise.reject(res.msg)
            }
        }
    }
})

export default useUserStore

6. 修改Login.vue页面 测试登录请求

<script setup>
import { ref,reactive } from 'vue'
import useUserStore from "@/stores/modules/user"
import {ElMessage} from "element-plus";
import router from "@/router"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/util/jsencrypt"

let userStore = useUserStore()
// el-form组件实例
let loginRef = ref()

// 收集表单数据
let loginForm  = reactive({
  username:'',
  password:'',
  rememberMe:false
})

// 验证对象
let loginRules = reactive({
  username:[{required:true,trigger:'blur',message:'请输入您的账号'}],
  password:[{required:true,trigger:'blur',message:'请输入您的密码'}]
})

const handleLogin = async () => {
  await loginRef.value.validate()

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

  try {
    await userStore.userLogin(loginForm)
    console.log(1111)
    router.push('/')
  } catch (e) {
    ElMessage.error("登录失败!")
  }
}

function getCookie() {
  const username = Cookies.get("username");
  const password = Cookies.get("password");
  const rememberMe = Cookies.get("rememberMe");
  Object.assign(loginForm, {
    username: username === undefined ? loginForm.username : username,
    password: password === undefined ? loginForm.password : decrypt(password),
    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
  });
}
// 获取cookie
getCookie();
</script>

<template>
<div class="login">

    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">java大帝 Vue3 后台管理系统</h3>

      <el-form-item prop="username">

        <el-input
            v-model="loginForm.username"
            prefix-icon="User"
            type="text"
            size="large"
            auto-complete="off"
            placeholder="账号"
        >

        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
            v-model="loginForm.password"
            prefix-icon="Lock"
            type="password"
            size="large"
            auto-complete="off"
            placeholder="密码"
            @keyup.enter="handleLogin"
            show-password
        >

        </el-input>
      </el-form-item>

      <el-checkbox v-model="loginForm.rememberMe"  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="https://blog.csdn.net/2302_80480374/article/details/145109271?spm=1001.2014.3001.5502" target="_blank">java大帝.vip</a> 版权所有.</span>
    </div>
  </div>
</template>


<style lang="scss" scoped>
a{
  color:white
}
.login {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/bg.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>

7.浏览器测试

用户登录SpringSecurity查库实现

 1.创建security.MyUserDetailsServiceImpl类

package com.jd.jdadmin.common.security;

import com.jd.jdadmin.common.exception.UserCountLockException;
import com.jd.jdadmin.entity.SysUser;
import com.jd.jdadmin.service.SysUserService;
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 javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * @author java大帝
 * @ClassName MyUserDetailsServiceImpl
 * @description: 自定义UserDetails
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private 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<>();
    }
}

 2.创建common.exception.UserCountLockException异常类

package com.jd.jdadmin.common.exception;


import org.springframework.security.core.AuthenticationException;

/**
 * @author java大帝
 * @ClassName UserCountLockException
 * @description: 账号被禁用异常
 * @date 2025年 01月 14日
 * @version: 1.0
 */
public class UserCountLockException extends AuthenticationException {
    public UserCountLockException(String msg, Throwable t) {
        super(msg, t);
    }

    public UserCountLockException(String msg) {
        super(msg);
    }
}

 3.创建common.exception.GlobalExceptionHandler全局异常处理类

package com.jd.jdadmin.common.exception;

import com.jd.jdadmin.entity.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author java大帝
 * @ClassName GlobalException
 * @description: 全局异常处理
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = RuntimeException.class)
    public R handler(RuntimeException e){
        log.error("运行时异常:-----------------{}",e.getMessage());
        return R.fail(e.getMessage());
    }
}

4.在SysUserService中新增根据用户名查询SysUser方法

 impl实现

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

 5.在SecurityConfig中装配MyUserDetailsService类

@Resource
private MyUserDetailsServiceImpl myUserDetailsService;

// 加密因为密码就是使用的BCryptPasswordEncoder 加密存储的
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
    return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(myUserDetailsService);
}

6.重新启动 浏览器测试

实现JWT认证过滤器

1.创建JwtAuthenticationFilter

package com.jd.jdadmin.common.security;

import com.jd.jdadmin.common.constant.JwtConstant;
import com.jd.jdadmin.entity.CheckResult;
import com.jd.jdadmin.entity.SysUser;
import com.jd.jdadmin.service.SysUserService;
import com.jd.jdadmin.util.JwtUtils;
import com.jd.jdadmin.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
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.annotation.Resource;
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;

/**
 * @author java大帝
 * @ClassName JwtAuthenticationFilter
 * @description: Jwt认证
 * @date 2025年 01月 14日
 * @version: 1.0
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    @Resource
    private SysUserService sysUserService;

    @Resource
    private MyUserDetailsServiceImpl myUserDetailsService;
    private static final String URI_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 {
        // 1.获取token请求头
        String token = request.getHeader("token");
        System.out.println("请求url" + request.getRequestURI());

        // 2.判断token为空或uri在白名单里面 则放行
        if (StrUtil.isEmpty(token) || new ArrayList<String>(Arrays.asList(URI_WHITELIST)).contains(request.getRequestURI())) {
            chain.doFilter(request,response);
            return;
        }

        // 3.否则验证token
        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);// 将新创建的认证对象放入 SecurityContextHolder中,这样在整个请求链中都可以访问到当前认证的信息。

        chain.doFilter(request,response);
    }
}

2.在SecurityConfig类中装配

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
    return new JwtAuthenticationFilter(authenticationManager());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置
    http
            ...
            .and()
            .addFilter(jwtAuthenticationFilter()) // 过滤器
    ;
}

 实现JWT认证异常处理器

1.新建security.JwtAuthenticationEntryPoint类

package com.jd.jdadmin.common.security;

import cn.hutool.json.JSONUtil;

import com.jd.jdadmin.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;

/**
 * @author java大帝
 * @ClassName JwtAuthenticationEntryPoint
 * @description: JWT自定义认证失败后的处理
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@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.fail(HttpServletResponse.SC_UNAUTHORIZED,"认证失败请登录!")).getBytes());
        outputStream.flush();
        outputStream.close();
    }
}

2.在SecurityConfig中装配并设置

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

@Resource
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置、自定义过滤器配置
    http
            ...
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            ...
    ;
}

3.前端:修改一下Login.vue 实现登录成功后去往首页

3.1创建src/layout文件夹 并新建index.vue

<script setup lang="ts">
import request from "@/util/request"

const testHandler = async () => {
  let res = await request.get('test/list')
  console.log(res)
}
</script>

<template>
  <div>首页</div>
  <el-button type="danger" @click="testHandler">测试接口</el-button>
</template>

<style scoped>

</style>

3.2在router/index.ts中配置 
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: '首页',
      component: () => import('@/layout/index.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/Login.vue')
    }
  ]
})

export default router
3.3修改views/Login.vue页面
<script setup>
import { ref,reactive } from 'vue'
import useUserStore from "@/stores/modules/user"
import {ElMessage} from "element-plus";
import router from "@/router"

let userStore = useUserStore()
// el-form组件实例
let loginRef = ref()

// 收集表单数据
let loginForm  = reactive({
  username:'',
  password:''
})

// 验证对象
let loginRules = reactive({
  username:[{required:true,trigger:'blur',message:'请输入您的账号'}],
  password:[{required:true,trigger:'blur',message:'请输入您的密码'}]
})

const handleLogin = async () => {
  await loginRef.value.validate()
  try {
    await userStore.userLogin(loginForm)
    console.log(1111)
    router.push('/')
  } catch (e) {
    ElMessage.error("登录失败!")
  }
}
</script>

<template>
  <div class="login">

    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">Jd Vue3 后台管理系统</h3>

      <el-form-item prop="username">

        <el-input
            v-model="loginForm.username"
            prefix-icon="User"
            type="text"
            size="large"
            auto-complete="off"
            placeholder="账号"
        >

        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
            v-model="loginForm.password"
            prefix-icon="Lock"
            type="password"
            size="large"
            auto-complete="off"
            placeholder="密码"
            @keyup.enter="handleLogin"
            show-password
        >

        </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">jd.vip</a> 版权所有.</span>
    </div>
  </div>
</template>
<style lang="scss" scoped>
...
</style>

 4.浏览器测试

实现自定义logout处理 

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

1.新建security.JwtLogoutSuccessHandler

@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.success("登出成功")).getBytes());
        outputStream.flush();
        outputStream.close();
    }
}

2.在SecurityConfig中配置

@Resource
private JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 开启跨域支持、关闭csrf支持、登录相关处理、退出登录、禁用 session配置、异常配置
    http
            ...
            .and()
            .logout()
            .logoutSuccessHandler(jwtLogoutSuccessHandler)
            ...
    ;
}

3.浏览器测试

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

springsecurity鉴权需要获取用户的角色权限系统 包括前端也需要这些信息

首先新建角色表 sys_role、菜单权限表sys_menu、用户角色关联表 sys_user_role、角色菜单权限关联表sys_role_menu

1.创建数据库表

# 角色表
CREATE TABLE `sys_role`(
`id` BIGINT(20) NOT NULL PRIMARY KEY 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 '备注'
)ENGINE=INNODB CHARSET = utf8;

# 测试数据
INSERT INTO `sys_role`(`id`,`name`,`code`,`create_time`,`update_time`,`remark`)
VALUES (1,'超级管理员','admin','2024-07-04 14:40:44','2024-07-04 14:40:47','拥有系统最高权限'),
(2,'普通角色','common','2024-07-04 14:41:56','2024-07-04 14:41:58','普通角色'),
(3,'测试角色','test','2024-07-04 14:42:24','2024-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','2024-08-13 21:06:21','2024-08-13
13:06:27','eewew2'),(20,'ccc测试','test2','2024-08-29 17:10:33',NULL,'xxx'),
(21,'今天测试角色','todytest','2024-08-29 22:01:11',NULL,'ccc');

# 菜单权限表
CREATE TABLE `sys_menu` (
`id` BIGINT(20) NOT NULL PRIMARY KEY 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 '备注'
)ENGINE=INNODB CHARSET = utf8;

# 测试数据
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','','2024-07-04 14:56:29','2024-07-04 14:56:31','系统管理目录'),
(2,'业务管理','monitor',0,2,'/bsns','','M','','2024-07-04 14:59:43','2024-07-04 14:59:45','业务管理目录'),
(3,'用户管理','user',1,1,'/sys/user','sys/user/index','C','system:user:list','2024-07-04 15:20:51','2024-07-04 15:20:53','用户管理菜单'),
(4,'角色管理','peoples',1,2,'/sys/role','sys/role/index','C','system:role:list','2024-07-04 15:23:35','2024-07-04 15:23:39','角色管理菜单'),
(5,'菜单管理','tree- table',1,3,'/sys/menu','sys/menu/index','C','system:menu:list','2024-07-04 15:23:41','2024-07-04 15:23:43','菜单管理菜单'),
(6,'部门管理','tree',2,1,'/bsns/department','bsns/Department','C','','2024-07-04 15:24:40','2024-07-04 15:24:44','部门管理菜单'),
(7,'岗位管理','post',2,2,'/bsns/post','bsns/Post','C','','2024-07-04 15:24:42','2024-07-04 15:24:46','岗位管理菜单'),
(8,'用户新增','#',3,2,'','','F','system:user:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加用户按钮'),
(9,'用户修改','#',3,3,'','','F','system:user:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改用户按钮'),
(10,'用户删除','#',3,4,'','','F','system:user:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除用户按钮'),
(11,'分配角色','#',3,5,'','','F','system:user:role','2024-07-04 15:24:42','2024-07-04 15:24:46','分配角色按钮'),
(12,'重置密码','#',3,6,'','','F','system:user:resetPwd','2024-07-04 15:24:42','2024-07-04 15:24:46','重置密码按钮'),
(13,'角色新增','#',4,2,'','','F','system:role:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加用户按钮'),
(14,'角色修改','#',4,3,'','','F','system:role:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改用户按钮'),
(15,'角色删除','#',4,4,'',NULL,'F','system:role:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除用户按钮'),
(16,'分配权限','#',4,5,'','','F','system:role:menu','2024-07-04 15:24:42','2024-07-04 15:24:46','分配权限按钮'),
(17,'菜单新增','#',5,2,'',NULL,'F','system:menu:add','2024-07-04 15:24:42','2024-07-04 15:24:46','添加菜单按钮'),
(18,'菜单修改','#',5,3,'',NULL,'F','system:menu:edit','2024-07-04 15:24:42','2024-07-04 15:24:46','修改菜单按钮'),
(19,'菜单删除','#',5,4,'',NULL,'F','system:menu:delete','2024-07-04 15:24:42','2024-07-04 15:24:46','删除菜单按钮'),
(20,'用户查询','#',3,1,'',NULL,'F','system:user:query','2024-07-04 15:24:42','2024-07-04 15:24:46','用户查询按钮'),
(21,'角色查询','#',4,1,'',NULL,'F','system:role:query','2024-07-04 15:24:42','2024-07-04 15:24:46','角色查询按钮'),
(22,'菜单查询','#',5,1,'',NULL,'F','system:menu:query','2024-07-04 15:24:42','2024-07-04 15:24:46','菜单查询按钮'),
(33,'测速22','122',3,3,'','34','M','33','2024-08-19 03:11:20','2024-08-18 19:11:33',NULL);

# 用户角色关联表
CREATE TABLE `sys_user_role`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户角色主键id',
`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户id',
`role_id` BIGINT(20) DEFAULT NULL COMMENT '角色id'
)ENGINE=INNODB CHARSET = utf8;

# 测试数据
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);

# 角色菜单关联表
CREATE TABLE `sys_role_menu`(
`id` BIGINT(20) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '角色菜单主键id',
`role_id` BIGINT(20) DEFAULT NULL COMMENT '角色id',
`menu_id` BIGINT(20) DEFAULT NULL COMMENT '菜单id'
)ENGINE=INNODB CHARSET = utf8;

# 测试数据
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);

2.使用MybatisX快速生成entity、mapper、service

 

 3.完善MyUserDetailsServiceImpl的getUserAuthority方法

// 在调用getUserAuthority方法的地方也要做相应修改 比如JwtAuthenticationFilter的方法
public List<GrantedAuthority> getUserAuthority(Long userId) {
    String authority=sysUserService.getUserAuthorityInfo(userId);
    return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}

 4.在SysUserService中定义方法

String getUserAuthorityInfo(Long userId);

5.在SysUserServiceImpl中做相应实现

package com.jd.jdadmin.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jd.jdadmin.entity.SysMenu;
import com.jd.jdadmin.entity.SysRole;
import com.jd.jdadmin.entity.SysUser;
import com.jd.jdadmin.mapper.SysMenuMapper;
import com.jd.jdadmin.mapper.SysRoleMapper;
import com.jd.jdadmin.service.SysUserService;
import com.jd.jdadmin.mapper.SysUserMapper;
import com.jd.jdadmin.util.StrUtil;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author java大帝
* @description 针对表【sys_user】的数据库操作Service实现
* @createDate 2025-01-13 15:41:51
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>
    implements SysUserService{

    @Resource
    private SysRoleMapper sysRoleMapper;

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

    // 返回格式: ROLE_admin,ROLE_common,system:user:resetPwd,system:role:delete,system:user:list,system:menu:query,system:menu:list 的字符串
    @Override
    public String getUserAuthorityInfo(Long userId) {
        StringBuffer authority = new StringBuffer();
        // 1.根据用户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){ // 长度大于0
            String roleCodeStrs = roleList.stream().map(item -> "ROLE_" + item.getCode()).collect(Collectors.joining(","));
            authority.append(roleCodeStrs);
        }

        // 2.遍历所有角色获取所有菜单权限而且不重复
        // 使用set集合 避免重复
        Set<String> menuCodeSet = new HashSet<>();
        for (SysRole sysRole : roleList) {
            List<SysMenu> menuList = sysMenuMapper.selectList(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id = " + sysRole.getId()));
            // 遍历菜单列表 取出perms权限值
            for (SysMenu sysMenu : menuList) {
                String perms = sysMenu.getPerms();
                if (StrUtil.isNotEmpty(perms)){
                    // 添加到set集合中
                    menuCodeSet.add(perms);
                }
            }
        }
        if (menuCodeSet.size() > 1 ){ // 默认就是1所以长度必须大于1
            // 在角色字符串的后面加个,
            authority.append(",");
            String menuCodeStrs = menuCodeSet.stream().collect(Collectors.joining(","));
            authority.append(menuCodeStrs);
        }
        System.out.println("authority=" + authority);
        return authority.toString();
    }
}




6.在TestController方法上使用PreAuthorize注解做相应权限校验

@PreAuthorize("hasRole('admin')") // 在校验时可以不带上ROLE_因为会自动拼接上,当存入进去的role值必须带ROLE_
@RequestMapping("list")
public R userList(@RequestHeader(required = false) String token){
    if (StrUtil.isNotEmpty(token)){
        HashMap<String, Object> resultMap = new HashMap<>();
        List<SysUser> list = sysUserService.list();
        resultMap.put("userList",list);
        return R.success(resultMap);
    }
    return R.fail(401,"无权访问");
}

7.修改一下LoginSuccessHandler

package com.jd.jdadmin.common.security;

import cn.hutool.json.JSONUtil;
import com.jd.jdadmin.entity.R;
import com.jd.jdadmin.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;

/**
 * @author java大帝
 * @ClassName LoginSuccessHandler
 * @description: 登录成功后的处理器
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@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 = authentication.getName();
        String token = JwtUtils.genJwtToken(username);

        outputStream.write(JSONUtil.toJsonStr(R.success().put("authorization",token)).getBytes());

        outputStream.flush();
        outputStream.close();
    }
}

8.浏览器测试 登录之后再测试  

记住密码功能实现

记住密码 通过cookie实现 先安装'js-cookie'依赖

存储用户密码为了安全需要加密 获取密码解密 所以我们安装'jsencrypt'依赖

1.安装js-cookie和jsencrypt依赖

npm install js-cookie@3.0.1 jsencrypt@3.2.1

2.util下新建jsencrypt.ts

//@ts-ignore
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:string) {
    const encryptor = new JSEncrypt()

    encryptor.setPublicKey(publicKey)// 设置公钥

    return encryptor.encrypt(txt) // 对数据进行加密
}

// 解密
export function decrypt(txt:string) {
    const encryptor = new JSEncrypt()

    encryptor.setPrivateKey(privateKey) // 设置私钥

   return encryptor.decrypt(txt) // 对数据进行解密
}

3.修改登录页Login.vue

<script setup>
import { ref,reactive } from 'vue'
import useUserStore from "@/stores/modules/user"
import {ElMessage} from "element-plus"
import router from "@/router"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/util/jsencrypt"

...

// 验证对象
let loginRules = reactive({
  username:[{required:true,trigger:'blur',message:'请输入您的账号'}],
  password:[{required:true,trigger:'blur',message:'请输入您的密码'}]
})

const handleLogin = async () => {
  await loginRef.value.validate()

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

  try {
    await userStore.userLogin(loginForm)
    console.log(1111)
    router.push('/')
  } catch (e) {
    ElMessage.error("登录失败!")
  }
}

function getCookie() {
  const username = Cookies.get("username");
  const password = Cookies.get("password");
  const rememberMe = Cookies.get("rememberMe");
  Object.assign(loginForm, {
    username: username === undefined ? loginForm.username : username,
    password: password === undefined ? loginForm.password : decrypt(password),
    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
  });
}
// 获取cookie
getCookie();
</script>

<template>
  <div class="login">

    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      ...
      <el-checkbox  style="margin:0px 0px 25px 0px;" v-model="loginForm.rememberMe">记住密码</el-checkbox>
       ...
    </el-form>
    <!--  底部  -->
    <div class="el-login-footer">
      <span>Copyright © 2013-2022 <a href="http://www.java1234.vip" target="_blank">jd.vip</a> 版权所有.</span>
    </div>
  </div>
</template>
<style lang="scss" scoped>
...
</style>

主页面功能实现

 主页面布局实现

1.在layout目录下新建四个子目录[每个子目录下都要建index.vue] 分模块管理主页面 

 2.修改layout/index.vue

<script setup lang="ts">
import Menu from '@/layout/menu/index.vue'
import Header from '@/layout/header/index.vue'
import Tabs from '@/layout/tabs/index.vue'
import Footer from '@/layout/footer/index.vue'


</script>

<template>
  <div class="app-wrapper">
    <el-container>
      <el-aside class="sidebar-container" width="200px">
        <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>

<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: 0;
  padding-right: 0;
}

/*:deep深度选择器*/
:deep(ul,el-menu) {
  border-right-width: 0;
}
</style>

3.修改footer/index.vue 

<script setup>
</script>

<template>
  <div class="footer"> Copyright © 2012-2022 java大帝 版权所有&nbsp;&nbsp;
    <a href="https://blog.csdn.net/2302_80480374/article/details/145109271?spm=1001.2014.3001.5501" target="_blank">www.java大帝.vip</a>
  </div>
</template>

<style lang="scss" scoped>
.footer{
  padding: 20px; display: flex; align-items: center;
}
</style>

4.修改tabs/index.vue 

<script lang="ts" setup>
import { ref } from 'vue'

let tabIndex = 2
const editableTabsValue = ref('2')
const editableTabs = ref([
  {
    title: 'Tab 1',
    name: '1',
    content: 'Tab 1 content',
  },
  {
    title: 'Tab 2',
    name: '2',
    content: 'Tab 2 content',
  },
])

const addTab = (targetName: string) => {
  const newTabName = `${++tabIndex}`
  editableTabs.value.push({
    title: 'New Tab',
    name: newTabName,
    content: 'New Tab content',
  })
  editableTabsValue.value = newTabName
}
const removeTab = (targetName: string) => {
  const tabs = editableTabs.value
  let activeName = editableTabsValue.value
  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)
}
</script>

<template>
  <div style="margin-bottom: 20px">
    <el-button size="small" @click="addTab(editableTabsValue)">
      add tab
    </el-button>
  </div>
  <el-tabs
      v-model="editableTabsValue"
      type="card"
      class="demo-tabs"
      closable
      @tab-remove="removeTab"
  >
    <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>

<style>
.demo-tabs > .el-tabs__content {
  padding: 32px;
  color: #6b778c;
  font-size: 32px;
  font-weight: 600;
}
</style>

 左侧动态权限菜单实现

1.后端: LoginSuccessHandler在登录成功处理器中做处理

package com.jd.jdadmin.common.security;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.jd.jdadmin.entity.R;
import com.jd.jdadmin.entity.SysMenu;
import com.jd.jdadmin.entity.SysRole;
import com.jd.jdadmin.entity.SysUser;
import com.jd.jdadmin.service.SysMenuService;
import com.jd.jdadmin.service.SysRoleService;
import com.jd.jdadmin.service.SysUserService;
import com.jd.jdadmin.util.JwtUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @author java大帝
 * @ClassName LoginSuccessHandler
 * @description: 登录成功后的处理器
 * @date 2025年 01月 14日
 * @version: 1.0
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private SysUserService sysUserService;

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    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);

        StringBuffer authority = new StringBuffer();
        // 1.根据用户id获取所有的权限信息[通过用户角色关联表]
        List<SysRole> roleList = sysRoleService.list(new QueryWrapper<SysRole>().inSql("id", "SELECT role_id FROM sys_user_role WHERE user_id = " + currentUser.getId()));

        // 2.遍历所有角色获取所有菜单权限而且不重复
        // 使用set集合 避免重复
        Set<SysMenu> menuSet = new HashSet<>();
        for (SysRole sysRole : roleList) {
            List<SysMenu> menuList = sysMenuService.list(new QueryWrapper<SysMenu>().inSql("id", "SELECT menu_id FROM sys_role_menu WHERE role_id = " + sysRole.getId()));
            // 遍历菜单列表 取出perms权限值
            for (SysMenu sysMenu : menuList) {
                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.success().put("authorization",token).put("currentUser",currentUser).put("menuList",menuList)).getBytes());

        outputStream.flush();
        outputStream.close();
    }
}

2.后端:在SysMenuService中创建buildTreeMenu方法

List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList);

3.后端:在 SysMenuServiceImpl实现类中实现 处理成菜单树方法

package com.jd.jdadmin.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jd.jdadmin.entity.SysMenu;
import com.jd.jdadmin.service.SysMenuService;
import com.jd.jdadmin.mapper.SysMenuMapper;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
* @author java大帝
* @description 针对表【sys_menu】的数据库操作Service实现
* @createDate 2025-01-15 11:16:24
*/
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu>
    implements SysMenuService{

    @Override
    public List<SysMenu> buildTreeMenu(List<SysMenu> sysMenuList) {
        List<SysMenu> resultMenuList = new ArrayList<>();

        for (SysMenu sysMenu : sysMenuList) {
            for (SysMenu menu : sysMenuList) {
                if (menu.getParentId() == sysMenu.getId()){ // 寻找子节点
                    sysMenu.getChildren().add(menu);
                }
            }
            if (sysMenu.getParentId() == 0L ){ // 若是一级菜单
                resultMenuList.add(sysMenu); // 直接加入集合中
            }
        }
        return resultMenuList;
    }
}

4.后端:在SysMenu中新建字段存储子节点 

/**
 * 子节点
 * */
@TableField(exist = false)
private List<SysMenu> children = new ArrayList<>();

 5.前端:补全一下api/user/type.ts 类型

...
export interface UserLoginResponseData extends ResponseData {
    data:{
        authorization:string,
        menuList: MenuInfo[],
        currentUser:UserInfo
    }
}

export interface UserInfo {
    phonenumber: string,
    loginDate: number,
    updateTime: number,
    remark: string,
    avatar: string,
    password: string,
    createTime: number,
    id: number,
    email: string,
    username: string,
    status: string
}

export interface MenuInfo {
    icon: string,
    orderNum: number,
    updateTime:  number,
    remark: string,
    parentId: number,
    path: string,
    component: string,
    children: MenuInfo[]
}

6.前端:在util下新建menu.ts 菜单相关的方法 

import type { MenuInfo } from "@/api/user/type";

export const SET_MENU = (menuList:MenuInfo[])=> {
    localStorage.setItem("MENU",JSON.stringify(menuList))
}

export const GET_MENU = ()=> {
    return JSON.parse(localStorage.getItem("MENU") || "[]")
}

export const REMOVE_MENU = ()=> {
    return localStorage.removeItem("MENU")
}

7.在用户仓库 登录请求发送完毕后存储一下后端返回的menuList数据 

import { defineStore } from "pinia"
import {GET_TOKEN, SET_TOKEN} from "@/util/token";
import {reqUserLogin} from "@/api/user";
import type {UserFormData, UserLoginResponseData} from "@/api/user/type";
import {GET_MENU, SET_MENU} from "@/util/menu";


const useUserStore = defineStore('User',{
    state(){
        return {
            token: GET_TOKEN(),
            menuList: GET_MENU()
        }
    },
    actions:{
        async userLogin(data:UserFormData){
            let res:UserLoginResponseData = await reqUserLogin(data)
            if (res.code == 200){
                SET_TOKEN(res.data.authorization)
                SET_MENU(res.data.menuList)
                return 'ok'
            } else {
                return Promise.reject(res.msg)
            }
        }
    }
})

export default useUserStore

8.修改layout/menu/inedx.vue 动态渲染菜单

因为 只遍历两次所以不用害怕第三级按钮权限会显示出来、首页是每个用户都可以访问的所以直接写静态的了

<script setup lang="ts">
import { ref } from 'vue'
import useUserStore from "@/stores/modules/user"

let userStore = useUserStore()
</script>

<template>
  <el-menu
      active-text-color="#ffd04b"
      background-color="#2d3a4b"
      text-color="#fff"
      router
  >
        
    <el-menu-item index="/index">
      <el-icon>
        <component is="House"/>
      </el-icon>
      <span>首页</span>
    </el-menu-item>

    <el-sub-menu :index="menu.path" v-for="menu in userStore.menuList">
      <template #title>
        <el-icon>
          <component :is="menu.icon" />
        </el-icon>
        <span>{
  
  { menu.name }}</span>
      </template>
      <el-menu-item :index="item.path" v-for="item in menu.children">
        <el-icon>
          <component :is="item.icon" />
        </el-icon>
        <span>{
  
  { item.name }}</span>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<style scoped lang="scss">

</style>

9.浏览器访问 显示效果

 右上角用户头像显示及退出登录实现

1.后端:在config.WebAppConfigurer类中添加addResourceHandlers方法

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { // 虚拟路径映射
    registry.addResourceHandler("/image/userAvatar/**").addResourceLocations("file:D:\\java1234\\img\\userAvatar\\");
}

2.前端:在util下新建user.ts用户相关方法

import type { UserInfo} from "@/api/user/type";

export const SET_USER = ((user:UserInfo)=> {
    localStorage.setItem("USER",JSON.stringify(user))
})

export const GET_USER = ()=> {
    return JSON.parse(localStorage.getItem("USER") || "{}")
}

export const REMOVE_USER = ()=> {
    return localStorage.removeItem("USER")
}

3.前端:新建两个子组件 并在layout/header/index.vue中引入使用

<script setup lang="ts">
import Breadcrumb from './breadCrumb/index.vue'
import Avatar from './avator/index.vue'
</script>

<template>
  <div class="navbar">
    <Breadcrumb />
    <div class="navbar-right">
      <Avatar />
    </div>
  </div>
</template>

<style scoped lang="scss">
.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>

4.前端:在layout/header/avator/index.vue实现显示头像、退出登录

<script setup lang="ts">
import useUserStore from "@/stores/modules/user"
import { baseUrl } from '@/util/request'
import {ref} from "vue"
import user from "@/stores/modules/user"
import router from "@/router"

let userStore = useUserStore()
// 头像地址
let squareUrl = ref(baseUrl + 'image/userAvatar/' + userStore.userInfo.avatar)

const logout = async () => {
  await userStore.userLogout()
  router.replace('/login')
}

</script>

<template>
  <el-avatar shape="square" :size="40" :src="squareUrl" style="margin-right: 10px"/>
  <el-dropdown>
    <span class="el-dropdown-link">
      {
  
  { userStore.userInfo.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>

<style scoped>
.el-dropdown-link {
  cursor: pointer;
  color: var(--el-color-primary);
  display: flex;
  align-items: center;
}
</style>

 5.前端:在api/user/index.ts中新建相关api接口

enum API {
    ...
    LOGOUT_URL = 'logout'
}
export const reqUserLogout = () => request.get<any,any>(API.LOGOUT_URL)

6.前端:修改stores/modules/user.ts用户小仓库 

import { defineStore } from "pinia"
import {GET_TOKEN, REMOVE_TOKEN, SET_TOKEN} from "@/util/token";
import {reqUserLogin, reqUserLogout} from "@/api/user";
import type {UserFormData, UserLoginResponseData} from "@/api/user/type";
import {GET_MENU, REMOVE_MENU, SET_MENU} from "@/util/menu";
import {GET_USER, REMOVE_USER, SET_USER} from "@/util/user";


const useUserStore = defineStore('User',{
    state(){
        return {
            token: GET_TOKEN(),
            menuList: GET_MENU(),
            userInfo:GET_USER()
        }
    },
    actions:{
        async userLogin(data:UserFormData){
            let res:UserLoginResponseData = await reqUserLogin(data)
            if (res.code == 200){              
                this.token = res.data.authorization
                SET_TOKEN(res.data.authorization) // 存储token
                SET_MENU(res.data.menuList) // 存储菜单数据
                SET_USER(res.data.currentUser) // 存储用户信息
                return 'ok'
            } else {
                return Promise.reject(res.msg)
            }
        },
        async userLogout(){ // 退出登录
            let res:any = await reqUserLogout()
            if (res.code == 200){
                // 清空本地存储
                window.localStorage.clear()
                return 'ok'
            } else {
                return Promise.reject(res.msg)
            }
        }
    }
})

export default useUserStore

7.浏览器测试

路由守卫功能实现

前端如果没有登录过 也就没有token 则自动跳转到登录页面 这个就是路由守卫

我们通过 router.beforeEach((to,from,next) => {}) 实现

1.router目录下新建permission.ts

import router from "@/router/index"
import useUserStore from "@/stores/modules/user"

router.beforeEach((to,from,next) => {
    // 用户相关小仓库
    const userStore = useUserStore()

    // 白名单
    const whiteList = ['/login']
    if (userStore.token){
        next()
    } else {
        if (whiteList.includes(to.path)){
            next()
        } else {
            next('/login')
        }
    }
})

2.在main.ts中引入使用

// 引入路由鉴权文件
import '@/router/permission' 

3.此时若是未登录则不能直接访问除/login之外的路由

 动态路由实现 

我们vue路由信息,需要通过后端查询的menuList 动态设置到router里面去

1.layout/index.vue加下<router-view/>

<template>
  <div class="app-wrapper">
    <el-container>
      ...
        <el-main>
          <Tabs />
          <router-view />
        </el-main>
        ...
    </el-container>
  </div>
</template>

2.在src/views下创建对应文件夹和文件

 

3.修改src/router/index.ts

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'layout',
      component: () => import('../layout/index.vue'),
      redirect:'/index',
      children:[ // 为动态添加路由做准备
        {
          path: '/index',
          name: '首页',
          component: () => import('../views/index/index.vue')
        },
        {
          path: '/userCenter',
          name: '用户中心',
          component: () => import('../views/userCenter/index.vue')
        }
      ]
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/Login.vue')
    }
  ]
})

export default router

4.在用户仓库中添加属性 

...
const useUserStore = defineStore('User',{
    state(){
        return {
            ...
            hasRoutes:false //标志着是否已经动态添加过路由了 只需动态添加一次就好了
        }
    },
    actions:{
       ...
    }
})

export default useUserStore

5.修改src/router/permission.ts 

import router from "@/router/index"
import useUserStore from "@/stores/modules/user"
import type {MenuInfo} from "@/api/user/type"

// 使用 import.meta.glob 预加载所有可能的组件
const components = import.meta.glob('../views/**/**/*.vue');

router.beforeEach((to,from,next) => {
    // 用户相关小仓库
    const userStore = useUserStore()

    let menuList = userStore.menuList
    // 白名单
    const whiteList = ['/login']

    if (userStore.token){
    
        next()

    } else {
        if (whiteList.includes(to.path)){
            next()
        } else {
            next('/login')
        }
    }
})

// 动态绑定路由
const bindRoute = (menuList:MenuInfo[]) => {
    let newRoutes = router.options.routes
    menuList.forEach(menu => {
        if (menu.children){ //是否有子组件
            menu.children.forEach(m => {
                let route = menuToRoute(m,menu.name) //将子菜单对象转成路由对象
                console.log(route)
                if (route){ // 若为空 则不动态添加
                    //@ts-ignore
                    newRoutes[0].children.push(route) //将路由都添加到第一个路由的子路由中
                }
            })
        }
    })
    // 重新添加到路由
    newRoutes.forEach(route => {
        router.addRoute(route)
    })
}

// 菜单对象转成路由对象
const menuToRoute = (menu:MenuInfo,parentName:string) => {
    if (!menu.component){ // 若没有组件信息字段 则直接返回null
        return null
    } else {
        const componentPath = `../views/${menu.component}.vue`;
        const component = components[componentPath];
        let route ={
          name:menu.name,
          path:menu.path,
          component,
          meta:{
              parentName:parentName
          }
      }
      return route
    }
}

async function initializeRoutes() {
    const userStore = useUserStore(pinia);
    if (!userStore.hasRoutes && userStore.token) {
        await bindRoute(userStore.menuList);
        userStore.hasRoutes = true;
    }
}

initializeRoutes(); //为了避免重复添加路由,可以考虑在应用初始化时就执行一次bindRoute,而不是放在beforeEach守卫里。这样可以保证路由仅被添加一次,而不会因为页面刷新或其他原因重复添加

6.路由显示结果如下 都是在/下

动态标签页的实现

1.创建tabsStore 用于管理标签的相关数据[因为涉及到菜单组件和标签选项卡组件的通信]

import {defineStore} from "pinia"

const useTabsStore = defineStore('Tabs',{
    state(){
        return {
            editableTabsValue:'/index', // 存储当前选中的可编辑选项卡的值
            editableTabs:[ // 存储可编辑选项卡的列表
                {
                    title:'首页',// 显示的标签名字
                    name:'/index' // 选项卡的唯一标识
                }
            ]
        }
    },
    actions:{
        addTabs(tab:any){
            if (this.editableTabs.findIndex(e => e.name === tab.path) === -1) {
                this.editableTabs.push({
                    title: tab.name,
                    name: tab.path
                })
            }
            this.editableTabsValue = tab.path
        },
        resetTabs(){
            this.editableTabsValue = '/index'
            this.editableTabs = [
                {
                    title:'首页',// 显示的标签名字
                    name:'/index' //
                }
            ]
        }
    }
})

export default useTabsStore

2. 修改src/layout/menu/index.vue 实现点击左侧菜单项 显示对应 标签选项卡

<script setup lang="ts">
...
import type {MenuInfo} from "@/api/user/type";
import useTabsStore from "@/stores/modules/tabs";

let tabsStore = useTabsStore()
let userStore = useUserStore()

const showTabs = (menu:MenuInfo) => {
  tabsStore.addTabs(menu)
}
</script>

<template>
  <el-menu
      active-text-color="#ffd04b"
      background-color="#2d3a4b"
      text-color="#fff"
      router
  >

    <el-menu-item index="/index">
      <el-icon>
        <component is="House"/>
      </el-icon>
      <span>首页</span>
    </el-menu-item>

    <el-sub-menu :index="menu.path" v-for="menu in userStore.menuList">
      <template #title>
        <el-icon>
          <component :is="menu.icon" />
        </el-icon>
        <span>{
  
  { menu.name }}</span>
      </template>
      <el-menu-item :index="item.path" v-for="item in menu.children" @click="showTabs(item)">
        <el-icon>
          <component :is="item.icon" />
        </el-icon>
        <span>{
  
  { item.name }}</span>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<style scoped lang="scss">

</style>

 3.修改src/layout/header/avator/index.vue 实现退出登录后将数据复原

import useTabsStore from "@/stores/modules/tabs"

let tabsStore = useTabsStore()
const logout = async () => {
  await userStore.userLogout()
  // 重置tab
  tabsStore.resetTabs()
  // 将控制是否动态加载路由的值复原
  userStore.hasRoutes = false

  router.replace('/login')
}

4.修改src/layout/tabs/index.vue 实现点击标签浏览器地址栏显示对应路径 选中对应菜单项 

<script lang="ts" setup>
import { ref,watch } from 'vue'
import useTabsStore from "@/stores/modules/tabs"
import {useRouter} from "vue-router"
import router from "@/router";

let tabsStore = useTabsStore()
let $router = useRouter()

// 点击标签页
const tabClick = (target:any) => {
  // 路由跳转
  $router.push({name:target.props.label})
}
const removeTab = (targetName: string) => {
  let tabs = tabsStore.editableTabs
  let activeName = tabsStore.editableTabsValue

  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
        }
      }
    })
  }

  // 过滤出除当前点击的标签页生成的数组
  tabsStore.editableTabs = tabs.filter(i => i.name !== targetName)

  router.push({path:activeName})
}

</script>

<template>
  <el-tabs
      v-model="tabsStore.editableTabsValue"
      type="card"
      class="demo-tabs"
      closable
      @tab-remove="removeTab"
      @tab-click="tabClick"
  >
    <el-tab-pane
        v-for="item in tabsStore.editableTabs"
        :key="item.name"
        :label="item.title"
        :name="item.name"
    >
    </el-tab-pane>
  </el-tabs>
</template>

<style>
.demo-tabs > .el-tabs__content {
  padding: 32px;
  color: #6b778c;
  font-size: 32px;
  font-weight: 600;
}

</style>

5.浏览器测试

动态面包屑实现

 1.修改src/layout/header/breadCrumb/index.vue

<script setup lang="ts">
import {ref, watch} from "vue"
import {useRoute} from "vue-router"


let $route = useRoute()
let breadcrumbList = ref([])
let parentName = ref('')

const initBreadcrumbList = () => {
  // 获取当前匹配的路由
  //@ts-ignore
  breadcrumbList.value = $route.matched
  //@ts-ignore
  parentName.value = $route.meta.parentName
}
// 深度监听
watch($route, ()=>{
  initBreadcrumbList()
},{deep:true,immediate:true})
</script>

<template>
  <el-icon>
    <component is="House"/>
  </el-icon>
  <el-breadcrumb separator="/">
    <el-breadcrumb-item v-for="(item,index) in breadcrumbList" :key="index">
      <span v-if="parentName && index > 0">{
  
  {parentName}}&nbsp;&nbsp;/&nbsp;&nbsp;</span>
      <span>{
  
  { item.name }}</span>
    </el-breadcrumb-item>
  </el-breadcrumb>
</template>

<style scoped>

</style>

 2.在views/index/index.vue中随便写点东西

<template>
  <div class="home">
    欢迎使用,java大帝 通用权限系统 !
  </div>
</template>
<script>
export default { name: "index" };
</script>

<style lang="scss" scoped>
</style>

3.浏览器测试

4.menu菜单默认激活菜单项 

<script setup lang="ts">
...
import {useRoute} from "vue-router"

...
let $route = useRoute()

...
</script>

<template>
  <el-menu
      active-text-color="#ffd04b"
      background-color="#2d3a4b"
      text-color="#fff"
      router
      :default-active="$route.path"
  >
        ...
  </el-menu>
</template>

<style scoped lang="scss">

</style>

个人中心功能实现 

路由与导航动态绑定实现

1.修改App.vue

<script setup lang="ts">
import { ref ,watch} from 'vue'
import { useRoute,useRouter }  from 'vue-router'
import useTabsStore from "@/stores/modules/tabs"

let tabsStore = useTabsStore()
const route=useRoute()
const router=useRouter()

const whitePath=['/login','/index','/']

watch(() => route.path, (newPath) => {
  if (whitePath.indexOf(newPath) === -1) {
    let obj = {
      name: route.name as string,
      path: newPath
    };
    tabsStore.addTabs(obj);
  }
}, { immediate: true });
</script>

2.修改 src/layout/header/avator/index.vue

<script setup lang="ts">
...

</script>

<template>
  <el-avatar shape="square" :size="40" :src="squareUrl" style="margin-right: 10px"/>
  <el-dropdown>
   ...
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item>
          <router-link :to="{name:'个人中心'}" >个人中心</router-link>
        </el-dropdown-item>
        <el-dropdown-item @click="logout">安全退出</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<style scoped>
.el-dropdown-link {
...
</style>

 个人中心页面构建实现

1.修改src/views/userCenter/index.vue

<script setup lang="ts">
import useUserStore from "@/stores/modules/user"
import {formatDate} from "@/util/formatDate"
import {ref} from "vue";

let userStore = useUserStore()
// 默认选中的标签页
let activeTab = ref('userinfo')

</script>

<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">
                <el-icon>
                  <component is="User"/>
                </el-icon>&nbsp;&nbsp;用户名称
                <div class="pull-right">
                  {
  
  {userStore.userInfo.username}}
                </div>
              </li>
              <li class="list-group-item">
                <el-icon>
                  <component is="Histogram"/>
                </el-icon>&nbsp;&nbsp;手机号码
                <div class="pull-right">
                  {
  
  {userStore.userInfo.phonenumber}}
                </div>
              </li>
              <li class="list-group-item">
                <el-icon>
                  <component is="Tickets"/>
                </el-icon>&nbsp;&nbsp;用户邮箱
                <div class="pull-right">
                  {
  
  {userStore.userInfo.email}}
                </div>
              </li>
              <li class="list-group-item">
                <el-icon>
                  <component is="Avatar"/>
                </el-icon>&nbsp;&nbsp;所属角色
                <div class="pull-right">
                  {
  
  {userStore.userInfo.roles}}
                </div>
              </li>
              <li class="list-group-item">
                <el-icon>
                  <component is="Timer"/>
                </el-icon>&nbsp;&nbsp;创建日期
                <div class="pull-right">
                  {
  
  {formatDate(userStore.userInfo.loginDate)}}
                </div>
              </li>
            </ul>
          </div>
        </el-card>
      </el-col>
      <el-col :span="18">
        <el-card class="box-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>

<style scoped lang="scss">
.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>

2.创建格式化日期的工具方法 

export function formatDate(val:string){
    let date = new Date(Number(val))
    let Y = date.getFullYear() + '-'
    let M = (date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1) + "-"
    let D = date.getDate() + " "
    let h = date.getHours() + ':'
    let m = date.getMinutes() + ':'
    let s =  (date.getSeconds() < 10 ? "0" + (date.getSeconds()) : date.getSeconds())
    return Y + M + D + h + m + s
}

3.浏览器显示效果 

个人中心页面数据显示 

1.抽取出来三个子组件 分别处理更换头像、重置密码、用户基本信息的业务

2.在src/views/userCenter/index.vue中引入三个子组件并使用

<script setup lang="ts">
...
import Avatar from './avatar/index.vue'
import userInfo from './userInfo/index.vue'
import resetPwd from './resetPwd/index.vue'

...
</script>

<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">
            <avatar />
          </div>
          <ul class="list-group list-group-striped">
           ...
         </ul>
        </div>
      </el-card>
    </el-col>
    <el-col :span="18">
      <el-card class="box-card">
        <template v-slot:header>
          <div class="clearfix">
            <span>
              基本资料
            </span>
          </div>
        </template>
       <el-tabs v-model="activeTab">
         <el-tab-pane label="基本资料" name="userinfo">
           <user-info/>
         </el-tab-pane>
         <el-tab-pane label="修改密码" name="resetPwd">
           <reset-pwd/>
         </el-tab-pane>
       </el-tabs>
      </el-card>
    </el-col>
  </el-row>
</div>
</template>

<style scoped lang="scss">
....
</style>

 3.后端:修改SysUser实体类 添加roles属性 存储拥有的角色信息

/**
 * 角色信息
 * 使用逗号分割的字符串
 * */
@TableField(exist = false)
private String roles;

4.后端:在LoginSuccessHandler中设置角色信息 

...
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Resource
    private SysUserService sysUserService;

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private SysMenuService sysMenuService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        ...
        for (SysRole sysRole : roleList) {
           ...
        }

        // 使用stream流式计算将角色列表处理成使用逗号间隔的字符串
        String roleStr = roleList.stream().map(SysRole::getName).collect(Collectors.joining(","));

        // 设置给当前用户
        currentUser.setRoles(roleStr);
        ...
    }
}

 5.浏览器测试

基本资料修改功能实现

1.在父组件中将用户信息传递进去

<script setup lang="ts">
...
</script>

<template>
  <div class="app-container">
    <el-row :gutter="20">
      ...
    
      <el-col :span="18">
        <el-card class="box-card">
         ...
          <el-tabs v-model="activeTab">
            <el-tab-pane label="基本资料" name="userinfo">
              <user-info :user="userStore.userInfo"/>
            </el-tab-pane>
            ...
          </el-tabs>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<style scoped lang="scss">
...
</style>

2.补齐views/userCenter/userInfo/index.vue

<script setup lang="ts">
import {ref} from "vue"
import {ElMessage} from "element-plus";
import {reqAddOrUpdate} from "@/api/user";
import useUserStore from "@/stores/modules/user";

let userStore = useUserStore()
let props = defineProps({
  user:{
    type:Object,
    default:()=> {},
    required:true
  }
})

let form = ref({
  id:-1,
  phonenumber:'',
  email:''
})

let userRef= ref()

let 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= async ()=>{
  await userRef.value.validate()
  try {
    let res = await reqAddOrUpdate(form.value)
    if (res.code == 200){
      ElMessage.success(form.value.id ? '修改成功' : '添加成功')
      userStore.userInfo = form.value
    }else {
      ElMessage.error(form.value.id ? '修改失败' : '添加失败')
    }
  } catch (e) {
    ElMessage.error((e as Error).message)
  }
}

</script>

<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>

<style scoped lang="scss">

</style>

 3.在src/api/user/index.ts中新增相关API接口

// 用户相关的API接口
import request from "@/util/request"
import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type"
enum API {
    ...
    ADD_OR_UPDATE_URL = 'sys/user/saveOrUpdate'
}

...
export const reqAddOrUpdate = (data:UserInfo) => request.post<any,any>(API.ADD_OR_UPDATE_URL,data)
// 修改用户信息接口
export interface UserInfo {
    phonenumber: string,
    loginDate?: number,
    updateTime?: number,
    remark?: string,
    avatar?: string,
    password?: string,
    createTime?: number,
    id?: number,
    email: string,
    username?: string,
    status?: string,
    roles?:string
}

4.后端:新增controller/SysUserController.java

package com.jd.jdadmin.controller;

import com.jd.jdadmin.entity.R;
import com.jd.jdadmin.entity.SysUser;
import com.jd.jdadmin.service.SysUserService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;

/**
 * @author java大帝
 * @ClassName SysUserController
 * @description: 用户Controller控制器
 * @date 2025年 01月 16日
 * @version: 1.0
 */
@RestController
@RequestMapping("/sys/user")
public class SysUserController {
    @Resource
    private SysUserService sysUserService;


    /**
    * 添加或修改
    * */
    @RequestMapping("saveOrUpdate")
    @PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user :edit')")
    public R saveOrUpdate(@RequestBody SysUser sysUser){
        if (sysUser.getId() == null || sysUser.getId() == -1){

        }else {
            sysUser.setUpdateTime(new Date());
            sysUserService.updateById(sysUser);
        }

        return R.success();
    }
}

5.浏览器测试

 

修改密码功能实现

1.在父组件中将用户信息传递进去

<script setup lang="ts">
...
</script>

<template>
<div class="app-container">
  <el-row :gutter="20">
    ...
    <el-col :span="18">
      <el-card class="box-card">
       ...
       <el-tabs v-model="activeTab">
         ...
         <el-tab-pane label="修改密码" name="resetPwd">
           <reset-pwd :user="userStore.userInfo"/>
         </el-tab-pane>
       </el-tabs>
      </el-card>
    </el-col>
  </el-row>
</div>
</template>

<style scoped lang="scss">
...
</style>

2.修改src/views/userCenter/resetPwd.vue

<script setup lang="ts">
import {defineProps, ref} from "vue"; import requestUtil from "@/util/request";
import { ElMessage } from 'element-plus'
import useUserStore from "@/stores/modules/user"
import {reqUpdatePwd} from "@/api/user";

let userStore = useUserStore()
const props = defineProps( {
  user:{
    type:Object,
    default:()=>{},
    required:true
  }
})
const form = ref({
  id:-1,
  oldPassword:'',
  newPassword:'',
  confirmPassword:''
})
const pwdRef= ref()

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 = async ()=> {
  await pwdRef.value.validate()
  let result = await reqUpdatePwd(form.value)
  if (result.code == 200) {
    ElMessage.success("密码修改成功,下一次登录生效!")
    userStore.userInfo = form.value
  }else{
    ElMessage.error(result.msg)
  }
}
</script>

<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>

<style scoped lang="scss">

</style>

3.在src/api/user/index.ts中新增相关API接口

// 用户相关的API接口
import request from "@/util/request"
import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type"
enum API {
    ...
    UPDATE_PWD_URL = 'sys/user/updateUserPwds'
}
...
export const reqUpdatePwd = (data:UserInfo) => request.post<any,any>(API.UPDATE_PWD_URL,data)
// 修改用户信息接口
export interface UserInfo {
    ...
    oldPassword?:string,
    newPassword?:string
}

4.后端:修改SysUser.java 添加两个oldPassword和newPassword字段 

/**
 * 旧密码
 * */
@TableField(exist = false)
private String oldPassword;

/**
 * 确认新密码
 * */
@TableField(exist = false)
private String newPassword;

5.在SysUserController中新增修改密码的接口

@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
 * 修改密码
 * */
@RequestMapping("updateUserPwds")
@PreAuthorize("hasAuthority('system:user:edit')")
public R updateUserPwds(@RequestBody SysUser sysUser){

    SysUser currentUser = sysUserService.getById(sysUser.getId());
    // 比较旧密码是否与数据库中的一致
    if (bCryptPasswordEncoder.matches(sysUser.getOldPassword(),currentUser.getPassword())){

        currentUser.setPassword(bCryptPasswordEncoder.encode(sysUser.getNewPassword()));

        currentUser.setUpdateTime(new Date());

        sysUserService.updateById(currentUser);

        return R.success();
    }else{
        return R.fail("输入旧密码错误");
    }
}

6.浏览器测试

 

头像更换功能实现

1.在父组件中将用户信息传递进去

<script setup lang="ts">
...

</script>

<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">
            <avatar :user="userStore.userInfo"/>
          </div>
          <ul class="list-group list-group-striped">
           ...
         </ul>
        </div>
      </el-card>
    </el-col>
    ...
  </el-row>
</div>
</template>

<style scoped lang="scss">
...
</style>

2.修改src/views/userCenter/resetPwd.vue

<script setup lang="ts">
import {defineProps, ref} from "vue";
import { ElMessage } from 'element-plus'
import useUserStore from "@/stores/modules/user"
import {baseUrl} from "@/util/request"
import {reqUpdateAvatar} from "@/api/user";

let userStore = useUserStore()
const props=defineProps( {
  user:{
    type:Object,
    default:()=>{},
    required:true
  }
})
const headers= ref({ token:userStore.token })
const form=ref({ id:-1, avatar:'' })
const formRef=ref(null)
const imageUrl=ref("")

form.value = props.user;

imageUrl.value= baseUrl+'image/userAvatar/'+form.value.avatar
const handleAvatarSuccess= (res:any) =>{
  console.log(res)
  imageUrl.value= baseUrl+res.data.src
  form.value.avatar=res.data.title;
}
const beforeAvatarUpload = (file:any) => {
  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 reqUpdateAvatar(form.value)

  if(result.code==200){
    ElMessage.success("执行成功!")
  }else{
    ElMessage.error(result.msg);
  }
}
</script>

<template>
  <el-form ref="formRef" :model="form" label-width="100px" style="text-align: center;padding-bottom:10px" >
    <!--
        :action="baseUrl+'sys/user/uploadImage'" 图片上传
    -->
    <el-upload
        :headers="headers" class="avatar-uploader"
        :action="baseUrl+'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">
        <component is="Plus"/>
      </el-icon>
    </el-upload>
    <el-button @click="handleConfirm" >确认更换</el-button>
  </el-form>
</template>

<style scoped lang="scss">
.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>

3.在src/api/user/index.ts中新增相关API接口

// 用户相关的API接口
import request from "@/util/request"
import type {UserFormData, UserInfo, UserLoginResponseData} from "@/api/user/type"
enum API {
    ...
    UPDATE_AVATAR_URL = 'sys/user/updateAvatar'
}

...
export const reqUpdateAvatar = (data:any) => request.post<any,any>(API.UPDATE_AVATAR_URL,data)

4.后端: 在SysUserController中新增上传头像和修改用户头像的接口

@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;

        // 将文件拷贝到对应位置
        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.success();
}

5.在application.yml中增加属性 

avatarImagesFilePath: D://jd/img/userAvatar/

6. 在util下新增DateUtil.java

package com.jd.jdadmin.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日期工具类
 * @author Administrator
 *
 */
public class DateUtil {

    /**
     * 日期对象转字符串
     * @param date
     * @param format
     * @return
     */
    public static String formatDate(Date date,String format){
       String result="";
       SimpleDateFormat sdf=new SimpleDateFormat(format);
       if(date!=null){
          result=sdf.format(date);
       }
       return result;
    }

    /**
     * 字符串转日期对象
     * @param str
     * @param format
     * @return
     * @throws Exception
     */
    public static Date formatString(String str,String format) throws Exception{
       if(StrUtil.isEmpty(str)){
          return null;
       }
       SimpleDateFormat sdf=new SimpleDateFormat(format);
       return sdf.parse(str);
    }

    public static String getCurrentDateStr(){
       Date date=new Date();
       SimpleDateFormat sdf=new SimpleDateFormat("yyyyMMddhhmmssSSSSSSSSS");
       return sdf.format(date);
    }

    public static String getCurrentDatePath()throws Exception{
       Date date=new Date();
       SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd/");
       return sdf.format(date);
    }

    public static void main(String[] args) {
       try {
          System.out.println(getCurrentDateStr());
       } catch (Exception e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
       }
    }
}

7.浏览器测试

 用户管理实现

列表分页显示

1.创建entity/PageBean.java封装分页参数

package com.jd.jdadmin.entity;

/**
 * @author java大帝
 * @ClassName PageBean
 * @description: 封装分页数据
 * @date 2025年 01月 17日
 * @version: 1.0
 */
public class PageBean {
    // 第几页
    private Integer pageNum;
    // 每页记录数
    private Integer pageSize;
    // 起始页
    private Integer start;
    // 查询参数
    private String query;

    public PageBean() {
    }

    public PageBean(Integer pageNum, Integer pageSize, String query) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
        this.query = query;
    }

    public Integer getStart() {
        // 当前页码减一*一页显示多少条 比如 1 - 1 * 5 = 5 所以起始页是5
        return (pageNum - 1) * pageSize;
    }

    public void setStart(Integer start) {
        this.start = start;
    }

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }


    public String getQuery() {
        return query;
    }

    public void setQuery(String query) {
        this.query = query;
    }
}

2.装配分页拦截器

package com.jd.jdadmin.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author java大帝
 * @ClassName MybatisPlusConfig
 * @description: 开启mp的分页功能
 * @date 2025年 01月 17日
 * @version: 1.0
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

3.在SysUserController中新增分页查询接口

/**
 * 根据条件分页查询用户信息
 */
@PostMapping("/list")
@PreAuthorize("hasAuthority('system:user:query')")
public R list(@RequestBody PageBean pageBean){
    Page<SysUser> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize());
    Page<SysUser> pageResult = sysUserService.page(sysUserPage);
    // 数据集合
    List<SysUser> userList = pageResult.getRecords();
    Map<String,Object> resultMap = new HashMap<>();
    resultMap.put("userList",userList);
    resultMap.put("total",pageResult.getTotal());
    return R.success(resultMap);
}

4.前端: 在src/api/user下创建相关接口和ts类型

src/api/user/index.ts

// 用户相关的API接口
...
enum API {
    ...
    QUERY_URL = 'sys/user/list'
}

...
export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data)

 src/api/user/type.ts

...
export interface QueryPageParams {
    pageNum:number,
    pageSize:number,
    start?:number,
    query?:string
}
export interface QueryResponse extends ResponseData {
    data:{
        total:number,
        userList:UserInfo[]
    }
}

5.前端:修改src/views/sys/user/index.vue

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type";
import {reqQueryList} from "@/api/user";

let tableData = ref<UserInfo[]>([])
let queryForm = reactive<QueryPageParams>({
  pageNum:1,
  pageSize:3,
  query:''
})
let total = ref(0)

const initUserList = async (pager=1)=>{
  queryForm.pageNum = pager
  let res:QueryResponse = await reqQueryList(queryForm)
  if (res.code == 200){
    tableData.value = res.data.userList
    total.value = res.data.total
  }
}


const handleSizeChange = () => {
  initUserList()
}

onMounted(()=>{
  initUserList()
})
</script>

<template>
<div class="app-container">
  <el-table border :data="tableData" stripe style="width: 100%">
    <el-table-column prop="username" label="用户名" width="180" />
  </el-table>
  <el-pagination
      v-model:current-page="queryForm.pageNum"
      v-model:page-size="queryForm.pageSize"
      :page-sizes="[3, 5, 7, 10]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="initUserList"
  />
</div>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

6.element-plu国际化配置

main.ts 

...

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
...

app.use(ElementPlus, {
    locale: zhCn,
})

...

7.浏览器显示效果

搜索功能实现

1.在SysUser中新增sysRoleList 存储角色列表[方便前端遍历显示]

/**
 * 所有角色集合
 * */
@TableField(exist = false)
public List<SysRole> sysRoleList;

2.修改SysUserController的list方法

 
 
@Resource
private SysRoleService roleService;
/**
 * 根据条件分页查询用户信息
 */
@PostMapping("/list")
@PreAuthorize("hasAuthority('system:user:query')")
public R list(@RequestBody PageBean pageBean){
    String query = pageBean.getQuery().trim();
    LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
    if (StrUtil.isNotEmpty(query)){
        queryWrapper.like(SysUser::getUsername,query);
    }
    Page<SysUser> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize());
    Page<SysUser> pageResult = sysUserService.page(sysUserPage,queryWrapper);
    // 数据集合
    List<SysUser> userList = pageResult.getRecords();
    for (SysUser sysUser : userList) {
        List<SysRole> roleList = roleService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + sysUser.getId()));
        sysUser.setSysRoleList(roleList);
    }
    Map<String,Object> resultMap = new HashMap<>();
    resultMap.put("userList",userList);
    resultMap.put("total",pageResult.getTotal());
    return R.success(resultMap);
}

3.前端: 在src/api/user下新增ts类型

src/api/user/type.ts
export interface UserInfo {
    phonenumber: string,
    loginDate?: number,
    updateTime?: number,
    remark?: string,
    avatar?: string,
    password?: string,
    createTime?: number,
    id?: number,
    email: string,
    username?: string,
    status?: string,
    roles?:string,
    oldPassword?:string,
    newPassword?:string,
    sysRoleList?:RoleInfo[]
}

export interface RoleInfo {
    id: number,
    createTime: string,
    updateTime: string,
    remark: string,
    name: string,
    code: string
}

 4.前端:修改src/views/sys/user/index.vue

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type";
import {reqQueryList} from "@/api/user";
import {baseUrl} from "@/util/request";

let tableData = ref<UserInfo[]>([])
let queryForm = reactive<QueryPageParams>({
  pageNum:1,
  pageSize:3,
  query:''
})
let total = ref(0)

const initUserList = async (pager=1)=>{
  queryForm.pageNum = pager
  let res:QueryResponse = await reqQueryList(queryForm)
  if (res.code == 200){
    tableData.value = res.data.userList
    total.value = res.data.total
  }
}


const handleSizeChange = () => {
  initUserList(queryForm.pageNum)
}

onMounted(()=>{
  initUserList()
})
</script>

<template>
<div class="app-container">
  <el-row :gutter="20" class="header">
    <el-col :span="7">
      <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input>
    </el-col>
    <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button>
  </el-row>
  <el-table border :data="tableData" stripe style="width: 100%">
    <el-table-column type="selection" width="55" />
    <el-table-column prop="avatar" label="头像" width="80" align="center">
      <template v-slot="scope">
        <img :src="baseUrl+'image/userAvatar/'+scope.row.avatar" width="50" height="50"/>
      </template>
    </el-table-column>
    <el-table-column prop="username" label="用户名" width="100" align="center"/>
    <el-table-column prop="roles" label="拥有角色" width="200" align="center">
      <template v-slot="scope">
        <el-tag size="small" type="warning" v-for="item in scope.row.sysRoleList">
          {
  
  {item.name}}
        </el-tag>
      </template>
    </el-table-column>
    <el-table-column prop="email" label="邮箱" width="200" align="center"/>
    <el-table-column prop="phonenumber" label="手机号" width="120" align="center"/>
    <el-table-column prop="status" label="状态?" width="200" align="center">
      <template v-slot="{row}" >
        <el-switch v-model="row.status"
                   @change="statusChangeHandle(row)"
                   active-text="正常"
                   inactive-text="禁用"
                   active-value="0"
                   inactive-value="1">
        </el-switch>
      </template>
    </el-table-column>
    <el-table-column prop="createTime" label="创建时间" width="200" align="center"/>
    <el-table-column prop="loginDate" label="最后登录时间" width="200" align="center"/>
    <el-table-column prop="remark" label="备注" />
    <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
      <template v-slot="scope" >
        <el-button type="primary" icon="Tools" >分配角色</el-button>
      </template>
    </el-table-column>
  </el-table>
  <el-pagination
      v-model:current-page="queryForm.pageNum"
      v-model:page-size="queryForm.pageSize"
      :page-sizes="[3, 5, 7, 10]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="initUserList"
  />
</div>
</template>

<style lang="scss" scoped>
...
</style>

5.浏览器显示效果

 添加修改功能实现

1.在SysUserController修改saveOrUpdate接口和新增两个新的接口

/**
* 添加或修改
* */
@RequestMapping("saveOrUpdate")
@PreAuthorize("hasAuthority('system:user:add')"+"||"+"hasAuthority('system:user :edit')")
public R saveOrUpdate(@RequestBody SysUser sysUser){
    if (sysUser.getId() == null || sysUser.getId() == -1){
        sysUser.setCreateTime(new Date());
        sysUserService.save(sysUser);
    }else {
        sysUser.setUpdateTime(new Date());
        sysUserService.updateById(sysUser);
    }

    return R.success();
}
/**
 * 根据id查询
 * */
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('system:user:query')")
public R findById(@PathVariable("id") Integer id){
    SysUser sysUser = sysUserService.getById(id);
    Map<String, Object> map = new HashMap<>();
    map.put("sysUser",sysUser);
    return R.success(map);
}

/**
 * 验证用户名是否已经被占用了
 * */
@PostMapping("/checkUserName")
@PreAuthorize("hasAuthority('system:user:query')")
public R checkUserName(@RequestBody SysUser sysUser){
    if (sysUserService.getByUsername(sysUser.getUsername()) == null){
        // 用户名没有被占用 返回成功
        return R.success();
    }
    return R.fail();
}

2. 前端:在src/api/user下新增两个接口

// 用户相关的API接口
...
enum API {
    ...
    FIND_BY_ID_URL = 'sys/user/',
    CHECK_USERNAME_URL = 'sys/user/checkUserName'
}

...
export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id)
export const reqCheckUserName = (data:UserInfo) => request.post<any,any>(API.CHECK_USERNAME_URL,data)

3.前端:在src/views/sys/user下新建子组件 用于显示添加/修改的对话框,并完成添加/修改用户

<script setup lang="ts">
import {defineEmits, defineProps,ref,watch } from "vue"
import { ElMessage } from 'element-plus'
import {reqAddOrUpdate, reqCheckUserName, reqFindById} from "@/api/user";
import type {UserInfo} from "@/api/user/type";


let props = defineProps( {
  id:{
    type:Number,
    default:-1,
    required:true
  },
  dialogTitle:{
    type:String,
    default:'',
    required:true
  },
  dialogVisible:{
    type:Boolean,
    default:false,
    required:true
  }
})

let formRef = ref()

let form = ref<UserInfo>({
  id:-1,
  username:"",
  password:"123456",
  status:"0",
  phonenumber:"",
  email:"",
  remark:""
})
const checkUsername = async (rule, value, callback) => {
  if(form.value.id == -1){ // 新增用户
    const res= await reqCheckUserName(form.value)
    if (res.code==500) {
      callback(new Error("用户名已存在!"));
    } else {
      callback();
    }
  }else{
    callback();
  }
}

let rules = ref({
  username:[
    { required: true, message: '请输入用户名'},
    { required: true, validator: checkUsername, trigger: "blur" }
  ],
  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" }
  ],
})
const initFormData = async (id:number) =>{
  const res = await reqFindById(id)
  form.value = res.data.sysUser;
}

watch(() => props.dialogVisible, () => {
  if (formRef.value){ // 首次打开对话框时formRef暂未初始化
    formRef.value.clearValidate('phonenumber')
    formRef.value.clearValidate('email')
    formRef.value.clearValidate('username')
  }
  let id = props.id;
  if(id!=-1){
    initFormData(id);
  }else{
    form.value= {
      id:-1,
      username:"",
      password:"123456",
      status:"0",
      phonenumber:"",
      email:"",
      remark:""
    }
  }
})
const emits = defineEmits(['update:modelValue','initUserList'])
const handleClose = ()=>{ emits('update:modelValue',false) }
const handleConfirm = async ()=>{
  await formRef.value.validate()
  let res = await reqAddOrUpdate(form.value)

  if(res.code==200){
    ElMessage.success("执行成功!")
    formRef.value.resetFields();
    emits("initUserList")
    handleClose();
  }else{
    ElMessage.error(res.msg);
  }

}

</script>

<template>
  <!--不要在 dialogVisible 上使用 v-model,因为它是一个 prop 需要使用v-on 和 v-bind[绑定dialogVisible]-->
  <el-dialog :visible="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" >
      <el-form-item label="用户名" prop="username">
        <el-input v-model="form.username" :disabled="form.id==-1? false:'disabled'" />
        <el-alert v-if="form.id==-1" title="默认初始密码:123456" :closable="false" style="line-height: 10px;" type="success" > </el-alert>
      </el-form-item>
      <el-form-item label="手机号" prop="phonenumber">
        <el-input v-model="form.phonenumber" />
      </el-form-item>
      <el-form-item label="邮箱" prop="email">
        <el-input v-model="form.email" />
      </el-form-item>
      <el-form-item label="状态" prop="status">
        <el-radio-group v-model="form.status">
          <el-radio :label="'0'">正常</el-radio>
          <el-radio :label="'1'">禁用</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input v-model="form.remark" type="textarea" :rows="4"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose" >取消</el-button >
      </span>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">

</style>

4.前端:src/views/sys/user/index.vue中引入对话框子组件并显示添加修改按钮

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, UserInfo} from "@/api/user/type"
import {reqQueryList} from "@/api/user"
import {baseUrl} from "@/util/request"
import Dialog from './components/index.vue'

let tableData = ref<UserInfo[]>([])
let queryForm = reactive<QueryPageParams>({
  pageNum:1,
  pageSize:3,
  query:''
})
let total = ref(0)

let dialogVisible = ref(false)
let dialogTitle = ref("")
let id = ref(-1)

const initUserList = async (pager=1)=>{
  queryForm.pageNum = pager
  let res:QueryResponse = await reqQueryList(queryForm)
  if (res.code == 200){
    tableData.value = res.data.userList
    total.value = res.data.total
  }
}


const handleSizeChange = () => {
  initUserList(queryForm.pageNum)
}

const handleDialogValue = (userId:number) => {
  if(userId){
    id.value = userId
    dialogTitle.value = '修改用户'
  } else {
    id.value = -1
    dialogTitle.value = '新增用户'
  }
  dialogVisible.value = true
}

onMounted(()=>{
  initUserList()
})
</script>

<template>
<div class="app-container">
  <el-row :gutter="20" class="header">
    <el-col :span="7">
      <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input>
    </el-col>
    <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button>
    <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
  </el-row>
  <el-table border :data="tableData" stripe style="width: 100%">
    <el-table-column type="selection" width="55" />
    <el-table-column prop="avatar" label="头像" width="80" align="center">
      <template v-slot="scope">
        <img :src="baseUrl+'image/userAvatar/'+scope.row.avatar" width="50" height="50"/>
      </template>
    </el-table-column>
    <el-table-column prop="username" label="用户名" width="100" align="center"/>
    <el-table-column prop="roles" label="拥有角色" width="200" align="center">
      <template v-slot="scope">
        <el-tag size="small" type="warning" v-for="item in scope.row.sysRoleList">
          {
  
  {item.name}}
        </el-tag>
      </template>
    </el-table-column>
    <el-table-column prop="email" label="邮箱" width="200" align="center"/>
    <el-table-column prop="phonenumber" label="手机号" width="120" align="center"/>
    <el-table-column prop="status" label="状态?" width="200" align="center">
      <template v-slot="{row}" >
        <el-switch v-model="row.status"
                   @change="statusChangeHandle(row)"
                   active-text="正常"
                   inactive-text="禁用"
                   active-value="0"
                   inactive-value="1">
        </el-switch>
      </template>
    </el-table-column>
    <el-table-column prop="createTime" label="创建时间" width="200" align="center"/>
    <el-table-column prop="loginDate" label="最后登录时间" width="200" align="center"/>
    <el-table-column prop="remark" label="备注" />
    <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
      <template v-slot="scope" >
        <el-button type="primary" icon="Tools" >分配角色</el-button>
        <el-button v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)">修改用户</el-button>
      </template>
    </el-table-column>
  </el-table>
  <el-pagination
      v-model:current-page="queryForm.pageNum"
      v-model:page-size="queryForm.pageSize"
      :page-sizes="[3, 5, 7, 10]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="initUserList"
  />
</div>
  <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initUserList="initUserList"/>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

5.浏览器测试

 

删除或批量删除

1.在SysUserController新增删除接口

@Resource
private SysUserRoleService sysUserRoleService;
/**
 * 删除或批量删除
 * 需要删除两个表的数据
 * */
@Transactional
@PostMapping("/delete")
@PreAuthorize("hasAuthority('system:user:delete')")
public R delete(@RequestBody Long[] ids){
    sysUserService.removeByIds(Arrays.asList(ids));
    sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("user_id",ids));
    return R.success();
}

2. 前端:在src/api/user下新增一个接口

// 用户相关的API接口
...
enum API {
    ...
    REMOVE_URL = 'sys/user/delete'

}

...
export const reqRemove = (ids:number[]) => request.post<any,any>(API.REMOVE_URL,ids)

3.修改src/views/sys/user/index.vue显示批量删除和单挑删除按钮及业务逻辑

<script setup lang="ts">
...
import {ElMessage} from "element-plus";

....
let multipleSelection = ref([])

...

const handleSelectionChange = (selection:any) => {
  console.log(selection)
  multipleSelection.value = selection
  delBtnStatus.value = selection.length == 0
}

const handleDelete = async (id:any) =>{
  let ids = []
  if(id){
    ids.push(id)
  } else {
    multipleSelection.value.forEach( row =>ids.push(row.id))
  }
  const res = await reqRemove(ids)
  if(res.code==200){
    ElMessage({ type: 'success', message: '执行成功!' })
    initUserList();
  }else{
    ElMessage({ type: 'error', message: res.msg })
  }
}

</script>

<template>
<div class="app-container">
  <el-row :gutter="20" class="header">
    <el-col :span="7">
      <el-input placeholder="请输入用户名..." v-model="queryForm.query" clearable ></el-input>
    </el-col>
    <el-button type="primary" icon="Search" @click="initUserList(1)">搜索</el-button>
    <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
    <el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)">
      <template #reference>
        <el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button>
      </template>
    </el-popconfirm>
  </el-row>
  <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
    ...
    <el-table-column prop="action" label="操作" width="250" fixed="right" align="center">
      <template v-slot="scope" >
        <el-button size="small" type="primary" icon="Tools" >分配角色</el-button>
        <el-button size="small" v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)">修改用户</el-button>
        <el-popconfirm v-if="scope.row.username!='tom'" title="您确定要删除这条记录 吗?" @confirm="handleDelete(scope.row.id)">
          <template #reference>
            <el-button size="small" type="danger" icon="Delete">删除用户</el-button>
          </template>
        </el-popconfirm>
      </template>
    </el-table-column>
  </el-table>
 ...
</template>

<style lang="scss" scoped>
...
</style>

4.浏览器测试 

 重置密码和修改状态

1.在SysUserController新增重置密码和修改状态的接口

/**
 * 重置密码
 * */
@GetMapping("/resetPassword/{id}")
@PreAuthorize("hasAuthority('system:user:edit')")
public R resetPassword(@PathVariable("id")Integer id){
    SysUser sysUser = sysUserService.getById(id);
    sysUser.setPassword(bCryptPasswordEncoder.encode(Constant.DEFAULT_PASSWORD));
    sysUser.setUpdateTime(new Date());
    sysUserService.updateById(sysUser);
    return R.success();
}

/**
 * 更新status状态
 * */
@GetMapping("/updateStatus/{id}/status/{status}")
@PreAuthorize("hasAuthority('system:user:edit')")
public R updateStatus(@PathVariable("id")Integer id,@PathVariable("status")String status){
    SysUser sysUser = sysUserService.getById(id);
    sysUser.setStatus(status);
    sysUserService.saveOrUpdate(sysUser);
    return R.success();

}

2.在common/constant/Constant.java存储默认密码

public class Constant {
    // 默认密码
    public final static String DEFAULT_PASSWORD = "123456";
}

3. 前端:在src/api/user下新增两个接口

// 用户相关的API接口
...
enum API {
    ...
    RESET_PWD_URL = 'sys/user/resetPassword/',
    UPDATE_STATUS_URL = 'sys/user/updateStatus/'
}

...

export const reqResetPwd = (id:number) => request.get<any,any>(API.RESET_PWD_URL + id)
export const reqUpdateStatus = (id:number,status:string) => request.get<any,any>(API.UPDATE_STATUS_URL + `${id}/status/${status}`)

4. 前端:修改src/views/sys/user/index.vue

<script setup lang="ts">
...
const handleResetPwd = async (id:number) => {
  let res = await reqResetPwd(id)
  if (res.code == 200){
    ElMessage({
      type:'success',
      message:'执行成功'
    })
  } else {
    ElMessage({
      type:'error',
      message:res.msg
    })
  }
}

const statusChangeHandle = async (row:any) => {
  let res = await reqUpdateStatus(row.id,row.status)
  if (res.code == 200){
    ElMessage({
      type:'success',
      message:'执行成功'
    })
  } else {
    ElMessage({
      type:'error',
      message:res.msg
    })
  }
}

onMounted(()=>{
  initUserList()
})
</script>

<template>
  <div class="app-container">
    ...
    <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
      ...
      <el-table-column prop="status" label="状态?" width="200" align="center">
        <template v-slot="{row}" >
          <el-switch v-model="row.status"
                     @change="statusChangeHandle(row)"
                     active-text="正常"
                     inactive-text="禁用"
                     active-value="0"
                     inactive-value="1">
          </el-switch>
        </template>
      </el-table-column>
      ...
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button size="small" type="primary" icon="Tools" >分配角色</el-button>
          <el-button size="small" v-if="scope.row.username != 'tom'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button>
          <el-popconfirm v-if="scope.row.username!='tom'" :title="`您确定要删除【${scope.row.username}】吗?`" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button size="small" type="danger" icon="Delete"></el-button>
            </template>
          </el-popconfirm>
          <el-popconfirm v-if="scope.row.username!='tom'" :title="`您确定要对这个用户重置密码吗?`" @confirm="handleResetPwd(scope.row.id)">
            <template #reference>
              <el-button size="small" type="warning" icon="WarnTriangleFilled">重置密码</el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    ...
</template>

<style lang="scss" scoped>
...
</style>

5.浏览器测试 

 

给用户分配角色

1.后端:在SysUserController新增分配角色接口

/**
 * 分配角色
 * */
@Transactional
@PostMapping("/grantRole/{userId}")
@PreAuthorize("hasAuthority('system:user:role')")
public R grantRole(@PathVariable("userId")Long userId,@RequestBody Long[] roleIds){
    List<SysUserRole> userRoleList = new ArrayList<>();

    Arrays.stream(roleIds).forEach(r -> {
        SysUserRole sysUserRole = new SysUserRole();
        sysUserRole.setRoleId(r);
        sysUserRole.setUserId(userId);
        userRoleList.add(sysUserRole);
    });
    // 先将当前用户关联的角色删除
    sysUserRoleService.remove(new QueryWrapper<SysUserRole>().eq("user_id",userId));
    // 再重新把新的管理的用户角色id设置进去
    sysUserRoleService.saveBatch(userRoleList);
    return R.success();
}

2.后端:新建controller/SysRoleController.java

package com.jd.jdadmin.controller;

import com.jd.jdadmin.entity.R;
import com.jd.jdadmin.entity.SysRole;
import com.jd.jdadmin.service.SysRoleService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author 叶蕊
 * @ClassName SysRoleController
 * @description: TODO
 * @date 2025年 01月 17日
 * @version: 1.0
 */
@RestController
@RequestMapping("/sys/role")
public class SysRoleController {
    @Resource
    private SysRoleService sysRoleService;

    /*
    * 查询所有角色信息
    * */
    @GetMapping("/listAll")
    @PreAuthorize("hasAuthority('system:role:query')")
    public R listAll(){
        Map<String,Object> resultMap = new HashMap<>();
        List<SysRole> roleList = sysRoleService.list();
        resultMap.put("roleList",roleList);
        return R.success(resultMap);
    }
}

3.前端: 在src/api/user下新增两个接口

// 用户相关的API接口
...
...
enum API {
    ...
    GRANT_ROLE_URL = 'sys/user/grantRole/',
    ROLE_LIST_URL = 'sys/role/listAll'
}

...

export const reqGrantRole = (userId:number,roleIds:number[]) => request.post<any,any>(API.GRANT_ROLE_URL + userId,roleIds )
export const reqRoleList = () => request.get<any,any>(API.ROLE_LIST_URL)

4.前端:在src/views/sys/user/components下新增roleDialog.vue 显示分配角色的对话框

<script setup lang="ts">
import {defineEmits, defineProps, ref, watch} from "vue"
import { ElMessage } from 'element-plus'
import {reqGrantRole, reqRoleList} from "@/api/user";

const props = defineProps( {
    id :{
      type:Number,
      default:-1,
      required:true
    },
  roleDialogVisible:{
      type:Boolean,
      default:false,
      required:true
  },
  sysRoleList :{
      type:Array,
      default:[],
      required:true
  }
})
const form=ref({
  id:-1,
  roleList:[],
  checkedRoles:[]
})
const formRef=ref()
const initFormData=async(id:number)=>{
  const res= await reqRoleList()
  console.log(res)
  form.value.roleList=res.data.roleList;
  form.value.id=id;
}
watch(()=>props.roleDialogVisible, ()=>{
  let id=props.id;
  console.log("id="+id)
  if(id!=-1){
    form.value.checkedRoles=[]
    props.sysRoleList.forEach(item => form.value.checkedRoles.push(item.id))
    initFormData(id)
  }
})
const emits=defineEmits(['update:modelValue','initUserList'])
const handleClose=()=>{ emits('update:modelValue',false) }
const handleConfirm = async ()=>{
  await formRef.value.validate()
  let res = await reqGrantRole(form.value.id,form.value.checkedRoles)
  if(res.code==200){
    ElMessage.success("执行成功!")
    emits("initUserList")
    handleClose();
  }else{
    ElMessage.error(res.msg);
  }
}
</script>

<template>
  <el-dialog :visible="roleDialogVisible" title="分配角色" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" label-width="100px" >
      <el-checkbox-group v-model="form.checkedRoles">
        <el-checkbox v-for="role in form.roleList" :id="role.id" :key="role.id" :label="role.id" name="checkedRoles" >
          {
  
  {role.name}}
        </el-checkbox>
      </el-checkbox-group>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose">取消</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">

</style>

5.前端:在src/views/sys/user/index.vue引入RoleDialog子组件 实现点击分配角色显示对话框

<script setup lang="ts">
...

const handleRoleDialogValue = (userId:number,roleList:any) => {
  console.log(userId)
  id.value = userId
  sysRoleList.value = roleList
  roleDialogVisible.value = true
}

...
</script>

<template>
  <div class="app-container">
    ...
    <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
      ...
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button size="small" type="primary" icon="Tools" @click="handleRoleDialogValue(scope.row.id,scope.row.sysRoleList)">分配角色</el-button>
          ...
      </el-table-column>
    </el-table>
    ...
  <RoleDialog v-model="roleDialogVisible" :sysRoleList="sysRoleList" :roleDialogVisible="roleDialogVisible" :id="id" @initUserList="initUserList"/>
</template>

<style lang="scss" scoped>
...
</style>

6.浏览器测试

角色管理实现

列表分页显示[根据用户管理模块做相应修改即可]

1.前端:复制user下的文件及文件夹到role中

 2.后端:复制SysUserController的分页查询接口到SysRoleController中 做相应处理即可

/**
 * 根据条件分页查询角色信息
 */
@PostMapping("/list")
@PreAuthorize("hasAuthority('system:role:query')")
public R list(@RequestBody PageBean pageBean){
    String query = pageBean.getQuery().trim();
    LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>();
    if (StrUtil.isNotEmpty(query)){
        queryWrapper.like(SysRole::getName,query);
    }
    Page<SysRole> sysUserPage = new Page<>(pageBean.getPageNum(), pageBean.getPageSize());
    Page<SysRole> pageResult = sysRoleService.page(sysUserPage,queryWrapper);
    // 数据集合
    List<SysRole> roleList = pageResult.getRecords();

    Map<String,Object> resultMap = new HashMap<>();
    resultMap.put("roleList",roleList);
    resultMap.put("total",pageResult.getTotal());
    return R.success(resultMap);
}

3.前端:在src/api下创建role文件夹及index.ts和type.ts

index.ts

import request from "@/util/request"; import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type";

enum API {
    QUERY_URL = 'sys/role/list'
}
export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data)


type.ts

export interface ResponseData {
    msg:string
    code:number
}
export interface RoleInfo {
    id?: number,
    createTime?: string,
    updateTime?: string,
    remark?: string,
    name: string,
    code: string
}
export interface QueryPageParams {
    pageNum:number,
    pageSize:number,
    start?:number,
    query?:string
}
export interface QueryResponse extends ResponseData {
    data:{
        total:number,
        roleList:RoleInfo[]
    }
}

4.浏览器测试

添加或修改[根据用户管理模块做相应修改即可]

1.后端:复制SysUserController的添加或修改接口及根据id查询角色信息接口到SysRoleController中 做相应处理即可

/**
 * 添加或修改
 * */
@RequestMapping("saveOrUpdate")
@PreAuthorize("hasAuthority('system:role:add')"+"||"+"hasAuthority('system:role:edit')")
public R saveOrUpdate(@RequestBody SysRole sysRole){
    if (sysRole.getId() == null || sysRole.getId() == -1){
        sysRole.setCreateTime(new Date());
        sysRoleService.save(sysRole);
    }else {
        sysRole.setUpdateTime(new Date());
        sysRoleService.updateById(sysRole);
    }

    return R.success();
}

/**
 * 根据id查询
 * */
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('system:role:query')")
public R findById(@PathVariable("id") Integer id){
    SysRole sysRole = sysRoleService.getById(id);
    Map<String, Object> map = new HashMap<>();
    map.put("sysRole",sysRole);
    return R.success(map);
}

2.前端:在src/api/role/index.ts中新增接口 

...
enum API {
    ...
    ADD_OR_UPDATE_URL = 'sys/role/saveOrUpdate',
    FIND_BY_ID_URL = 'sys/role/'
}
...
export const reqQueryList = (data:QueryPageParams) => request.post<any,QueryResponse>(API.QUERY_URL,data)
export const reqRemove = (ids:number[]) => request.post<any,any>(API.REMOVE_URL,ids)

 3.前端:修改src/views/role/components/index.vue

<script setup lang="ts">
import {defineEmits, defineProps,ref,watch } from "vue"
import { ElMessage } from 'element-plus'
import {reqAddOrUpdate,reqFindById} from "@/api/role";
import type {RoleInfo} from "@/api/role/type";


let props = defineProps( {
  id:{
    type:Number,
    default:-1,
    required:true
  },
  dialogTitle:{
    type:String,
    default:'',
    required:true
  },
  dialogVisible:{
    type:Boolean,
    default:false,
    required:true
  }
})

let formRef = ref()

let form = ref<RoleInfo>({
  id:-1,
  name:"",
  remark:"",
  code:''
})


let rules = ref({
  name:[ { required: true, message: '请输入角色名称'} ],
  code:[ { required: true, message: '请输入权限字符'} ]
})
const initFormData = async (id:number) =>{
  const res = await reqFindById(id)
  form.value = res.data.sysRole;
}

watch(() => props.dialogVisible, () => {
  if (formRef.value){ // 首次打开对话框时formRef暂未初始化
    formRef.value.clearValidate('code')
    formRef.value.clearValidate('name')
  }
  let id = props.id;
  if(id!=-1){
    initFormData(id);
  }else{
    form.value= {
      id:-1,
      name:"",
      remark:"",
      code:''
    }
  }
})
const emits = defineEmits(['update:modelValue','initRoleList'])
const handleClose = ()=>{ emits('update:modelValue',false) }
const handleConfirm = async ()=>{
  await formRef.value.validate()
  let res = await reqAddOrUpdate(form.value)

  if(res.code==200){
    ElMessage.success("执行成功!")
    formRef.value.resetFields();
    emits("initRoleList")
    handleClose();
  }else{
    ElMessage.error(res.msg);
  }

}

</script>

<template>
  <!--不要在 dialogVisible 上使用 v-model,因为它是一个 prop 需要使用v-on 和 v-bind[绑定dialogVisible]-->
  <el-dialog :visible="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" >
      <el-form-item label="角色名称" prop="name">
        <el-input v-model="form.name" :disabled="form.id==-1? false:'disabled'" />
      </el-form-item>
      <el-form-item label="权限字符" prop="code">
        <el-input v-model="form.code" />
      </el-form-item>
      <el-form-item label="备注" prop="remark">
        <el-input v-model="form.remark" type="textarea" :rows="4"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose" >取消</el-button >
      </span>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">

</style>

4.浏览器测试

 

删除或批量删除[根据用户管理模块做相应修改即可]

1.后端:复制SysUserController的删除或批量删除接口到SysRoleController中 做相应处理即可

@Resource
private SysUserRoleService sysUserRoleService;
/**
 * 删除或批量删除
 * 需要删除两个表的数据
 * */
@Transactional
@PostMapping("/delete")
@PreAuthorize("hasAuthority('system:role:delete')")
public R delete(@RequestBody Long[] ids){
    sysRoleService.removeByIds(Arrays.asList(ids));
    sysUserRoleService.remove(new QueryWrapper<SysUserRole>().in("role_id",ids));
    return R.success();
}

2.前端:在src/api/role/index.ts中新增接口 

...
enum API {
    ...
    REMOVE_URL = 'sys/role/delete'
}
...
export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id)

3.前端:src/views/role/index.vue

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type"
import {reqQueryList, reqRemove} from "@/api/role"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import RoleDialog from "@/views/sys/role/components/roleDialog.vue"

let tableData = ref<RoleInfo[]>([])
let queryForm = reactive<QueryPageParams>({
  pageNum:1,
  pageSize:3,
  query:''
})
let total = ref(0)
let dialogVisible = ref(false)
let dialogTitle = ref("")
let id = ref(-1)
let delBtnStatus = ref(true)
let multipleSelection = ref([])
let sysRoleList=ref([])
let roleDialogVisible=ref(false)

const initRoleList = async (pager=1)=>{
  queryForm.pageNum = pager
  let res:QueryResponse = await reqQueryList(queryForm)
  if (res.code == 200){
    tableData.value = res.data.roleList
    total.value = res.data.total
  }
}


const handleSizeChange = () => {
  initRoleList(queryForm.pageNum)
}

const handleDialogValue = (roleId:number) => {
  if(roleId){
    id.value = roleId
    dialogTitle.value = '修改角色'
  } else {
    id.value = -1
    dialogTitle.value = '新增角色'
  }
  dialogVisible.value = true
}

const handleSelectionChange = (selection:any) => {
  console.log(selection)
  multipleSelection.value = selection
  delBtnStatus.value = selection.length == 0
}

const handleDelete = async (id:any) =>{
  let ids = []
  if(id){
    ids.push(id)
  } else {
    multipleSelection.value.forEach( row => ids.push(row.id))
  }
  const res = await reqRemove(ids)
  if(res.code==200){
    ElMessage({ type: 'success', message: '执行成功!' })
    initRoleList();
  }else{
    ElMessage({ type: 'error', message: res.msg })
  }
}

const handleRoleDialogValue = (roleId:number,roleList:any) => {
  console.log(roleId)
  id.value = roleId
  sysRoleList.value = roleList
  roleDialogVisible.value = true
}

onMounted(()=>{
  initRoleList()
})
</script>

<template>
  <div class="app-container">
    <el-row :gutter="20" class="header">
      <el-col :span="7">
        <el-input placeholder="请输入角色名..." v-model="queryForm.query" clearable ></el-input>
      </el-col>
      <el-button type="primary" icon="Search" @click="initRoleList(1)">搜索</el-button>
      <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
      <el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)">
        <template #reference>
          <el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button>
        </template>
      </el-popconfirm>
    </el-row>
    <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" />
      <el-table-column prop="name" label="角色名" width="100" align="center"/>
      <el-table-column prop="code" label="权限字符" width="200" align="center"/>
      <el-table-column prop="createTime" label="创建时间" width="200" align="center"/>
      <el-table-column prop="remark" label="备注" />
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button size="small" type="primary" icon="Tools" @click="handleRoleDialogValue(scope.row.id,scope.row.sysRoleList)">分配权限</el-button>
          <el-button size="small" v-if="scope.row.code != 'admin'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button>
          <el-popconfirm v-if="scope.row.code!='admin'" :title="`您确定要删除【${scope.row.name}】吗?`" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button size="small" type="danger" icon="Delete"></el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
        v-model:current-page="queryForm.pageNum"
        v-model:page-size="queryForm.pageSize"
        :page-sizes="[3, 5, 7, 10]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="initRoleList"
    />
  </div>
  <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initRoleList="initRoleList"/>
  <RoleDialog v-model="roleDialogVisible" :sysRoleList="sysRoleList" :roleDialogVisible="roleDialogVisible" :id="id" @initRoleList="initRoleList"/>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

 4.浏览器测试

 显示权限菜单树

1.后端:在controller下创建SysMenuController控制器并提供返回所有的菜单树的接口

@RestController
@RequestMapping("/sys/menu")
public class SysMenuController {
    @Resource
    private SysMenuService sysMenuService;

    /**
     * 返回所有的菜单树
     * */
    @RequestMapping("/treeList")
    @PreAuthorize("hasAuthority('system:menu:query')")
    public R treeList(){
        List<SysMenu> menuList = sysMenuService.list(new QueryWrapper<SysMenu>().orderByAsc("order_num"));
        List<SysMenu> treeMenu = sysMenuService.buildTreeMenu(menuList);
        return R.success().put("treeMenu",sysMenuService.buildTreeMenu(treeMenu));
    }
}

2.前端:在src/api/role 新增接口和接口类型

index.ts
...

enum API {
    ...
    TREE_MENU_URL = 'sys/menu/treeList'
}

...
export const reqTreeMenu = () => request.get<any,TreeMenuData>(API.TREE_MENU_URL)
type.ts
export interface MenuInfo {
    id:number,
    name:string,
    icon: string,
    orderNum: number,
    updateTime:  number,
    remark: string,
    parentId: number,
    path: string,
    component: string,
    children: MenuInfo[]
}
export interface TreeMenuData extends ResponseData {
    data: {
        treeMenu:MenuInfo[]
    }
}

 3.前端:修改src/views/role/index.vue

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import type {QueryPageParams, QueryResponse, RoleInfo} from "@/api/role/type"
import {reqQueryList, reqRemove} from "@/api/role"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import MenuDialog from "@/views/sys/role/components/menuDialog.vue"

let tableData = ref<RoleInfo[]>([])
let queryForm = reactive<QueryPageParams>({
  pageNum:1,
  pageSize:3,
  query:''
})
let total = ref(0)
let dialogVisible = ref(false)
let dialogTitle = ref("")
let id = ref(-1)
let delBtnStatus = ref(true)
let multipleSelection = ref([])
let menuDialogVisible=ref(false)

const initRoleList = async (pager=1)=>{
  queryForm.pageNum = pager
  let res:QueryResponse = await reqQueryList(queryForm)
  if (res.code == 200){
    tableData.value = res.data.roleList
    total.value = res.data.total
  }
}


const handleSizeChange = () => {
  initRoleList(queryForm.pageNum)
}

const handleDialogValue = (roleId:number) => {
  if(roleId){
    id.value = roleId
    dialogTitle.value = '修改角色'
  } else {
    id.value = -1
    dialogTitle.value = '新增角色'
  }
  dialogVisible.value = true
}

const handleSelectionChange = (selection:any) => {
  console.log(selection)
  multipleSelection.value = selection
  delBtnStatus.value = selection.length == 0
}

const handleDelete = async (id:any) =>{
  let ids = []
  if(id){
    ids.push(id)
  } else {
    multipleSelection.value.forEach( row => ids.push(row.id))
  }
  const res = await reqRemove(ids)
  if(res.code==200){
    ElMessage({ type: 'success', message: '执行成功!' })
    initRoleList();
  }else{
    ElMessage({ type: 'error', message: res.msg })
  }
}

const handleMenuDialogValue = (userId:number) => {
  console.log(userId)
  id.value = userId
  menuDialogVisible.value = true
}

onMounted(()=>{
  initRoleList()
})
</script>

<template>
  <div class="app-container">
    <el-row :gutter="20" class="header">
      <el-col :span="7">
        <el-input placeholder="请输入角色名..." v-model="queryForm.query" clearable ></el-input>
      </el-col>
      <el-button type="primary" icon="Search" @click="initRoleList(1)">搜索</el-button>
      <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
      <el-popconfirm title="您确定批量删除这些记录吗?" @confirm="handleDelete(null)">
        <template #reference>
          <el-button type="danger" :disabled="delBtnStatus" icon="Delete" >批量删除</el-button>
        </template>
      </el-popconfirm>
    </el-row>
    <el-table border :data="tableData" stripe style="width: 100%" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" />
      <el-table-column prop="name" label="角色名" width="100" align="center"/>
      <el-table-column prop="code" label="权限字符" width="200" align="center"/>
      <el-table-column prop="createTime" label="创建时间" width="200" align="center"/>
      <el-table-column prop="remark" label="备注" />
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button size="small" type="primary" icon="Tools" @click="handleMenuDialogValue(scope.row.id)">分配权限</el-button>
          <el-button size="small" v-if="scope.row.code != 'admin'" type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)"></el-button>
          <el-popconfirm v-if="scope.row.code!='admin'" :title="`您确定要删除【${scope.row.name}】吗?`" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button size="small" type="danger" icon="Delete"></el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
        v-model:current-page="queryForm.pageNum"
        v-model:page-size="queryForm.pageSize"
        :page-sizes="[3, 5, 7, 10]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="initRoleList"
    />
  </div>
  <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initRoleList="initRoleList"/>
  <MenuDialog v-model="menuDialogVisible"  :menuDialogVisible="menuDialogVisible" :id="id" @initRoleList="initRoleList"/>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

4.前端:修改src/views/role/components/menuDialog.vue

<script setup lang="ts">
import {defineEmits, defineProps, ref, watch} from "vue"
import { ElMessage } from 'element-plus'
import {reqTreeMenu} from "@/api/role";


let props = defineProps( {
  id :{
    type:Number,
    default:-1,
    required:true
  },
  menuDialogVisible:{
    type:Boolean,
    default:false,
    required:true
  }
})
let form=ref({
  id:-1
})
let formRef=ref()
let treeData=ref([])
let defaultProps = {
  children: 'children',
  label: 'name'
}
let treeRef = ref()

const initFormData=async(id:number)=>{
  const res= await reqTreeMenu()
  console.log(res)
  treeData.value = res.data.treeMenu
  form.value.id=id;
}
watch(()=>props.menuDialogVisible, ()=>{
  let id=props.id;
  console.log("id="+id)
  if(id!=-1){
    initFormData(id)
  }
})
const emits=defineEmits(['update:modelValue','initRoleList'])
const handleClose=()=>{ emits('update:modelValue',false) }
const handleConfirm = async ()=>{
  await formRef.value.validate()
  // let res = await reqGrantMenu(form.value.id,form.value.checkedMenus)
  // if(res.code==200){
  //   ElMessage.success("执行成功!")
  //   emits("initRoleList")
  //   handleClose();
  // }else{
  //   ElMessage.error(res.msg);
  // }
}
</script>

<template>
  <el-dialog :visible="menuDialogVisible" title="分配权限" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" label-width="100px" >
      <el-tree ref="treeRef" :data="treeData" :props="defaultProps" show-checkbox :default-expand-all="true" node-key="id" :check-strictly="true" />
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose">取消</el-button>
      </span>
    </template>
  </el-dialog>
</template>
<style scoped lang="scss">

</style>

5.浏览器测试

*分配权限的实现

1.后端:在SysMenuController提供根据id查询菜单权限列表的接口和给角色分配权限的接口

@Resource
private SysRoleMenuService sysRoleMenuService;
/**
* 获取当前角色的权限菜单id集合[即当前角色拥有的权限]
* */
@GetMapping("/menus/{id}")
@PreAuthorize("hasAuthority('system:role:menu')")
public R menus(@PathVariable("id") Long id){
    List<SysRoleMenu> roleMenuList = sysRoleMenuService.list(new QueryWrapper<SysRoleMenu>().eq("role_id", id));
    List<Long> menuIds = roleMenuList.stream().map(i -> i.getMenuId()).collect(Collectors.toList());
    return R.success().put("menuIds",menuIds);
}

/**
 * 给角色分配权限
 * */
@Transactional
@PostMapping("/update/menus/{id}")
@PreAuthorize("hasAuthority('system:role:menu')")
public R updateMenus(@PathVariable("id")Long id,@RequestBody Long[] menuIds){
    sysRoleMenuService.remove(new QueryWrapper<SysRoleMenu>().eq("role_id",id));
    List<SysRoleMenu> sysRoleMenus = new ArrayList<>();
    Arrays.stream(menuIds).forEach(menuId -> {
        SysRoleMenu sysRoleMenu = new SysRoleMenu();
        sysRoleMenu.setRoleId(id);
        sysRoleMenu.setMenuId(menuId);
        sysRoleMenus.add(sysRoleMenu);
    });

    sysRoleMenuService.saveBatch(sysRoleMenus);
    return R.success();
}

2.前端:在src/api/role/index.ts 新增接口

...

enum API {
    ...
    HAS_MENU_URL = 'sys/role/menus/',
    GRANT_MENU_URL = 'sys/role/update/menus/'
}

...
export const reqHasMenu = (id:number) => request.get<any,any>(API.HAS_MENU_URL + id)
export const reqGrantMenu = (id:number,menuIds:number[]) => request.post<any,any>(API.GRANT_MENU_URL + id,menuIds)

3.前端: 修改src/views/role/components/menuDialog.vue

<script setup lang="ts">
import {defineEmits, defineProps, ref, watch} from "vue"
import { ElMessage } from 'element-plus'
import {reqGrantMenu, reqHasMenu, reqTreeMenu} from "@/api/role";


let props = defineProps( {
    id :{
      type:Number,
      default:-1,
      required:true
    },
    menuDialogVisible:{
        type:Boolean,
        default:false,
        required:true
    }
})
let form=ref({
  id:-1
})
let formRef=ref()
let treeData=ref([])
let defaultProps = {
  children: 'children',
  label: 'name'
}
let treeRef = ref()

const initFormData= async(id:number)=>{
  let res= await reqTreeMenu()
  treeData.value = res.data.treeMenu
  form.value.id=id
  let res2 = await reqHasMenu(id)
  console.log(res2)
  // 通过 key 设置某个节点的当前选中状态,使用此方法必须设置 node-key  属性
  treeRef.value.setCheckedKeys(res2.data.menuIds)
}
watch(()=>props.menuDialogVisible, ()=>{
  let id=props.id;
  console.log("id="+id)
  if(id!=-1){
    initFormData(id)
  }
})
const emits=defineEmits(['update:modelValue','initRoleList'])
const handleClose=()=>{ emits('update:modelValue',false) }
const handleConfirm = async ()=>{
  await formRef.value.validate()
  // 获取所有选中的节点的key[权限id]
  let menuIds = treeRef.value.getCheckedKeys()
  // 分配权限
  let res = await reqGrantMenu(form.value.id,menuIds)

  if(res.code==200){
    ElMessage.success("执行成功!")
    emits("initRoleList")
    handleClose();
  }else{
    ElMessage.error(res.msg);
  }
}
</script>

<template>
  <el-dialog :visible="menuDialogVisible" title="分配权限" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" label-width="100px" >
      <el-tree ref="treeRef" :data="treeData" :props="defaultProps" show-checkbox :default-expand-all="true" node-key="id" :check-strictly="true" />
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose">取消</el-button>
      </span>
    </template>
  </el-dialog>
</template>
<style scoped lang="scss">

</style>

4.浏览器测试

 菜单管理实现

树形表格菜单信息显示

1.前端:将src/views/sys/user下的文件夹及文件拷贝到menu中 将roleDialog.vue删除

2.前端: 修改src/views/sys/menu/index.ts

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import {reqTreeMenu} from "@/api/role";

let tableData=ref([])
let dialogVisible=ref(false)
let dialogTitle=ref("")
let id=ref(-1)

const initMenuList= async()=>{
  const res= await reqTreeMenu()
  console.log()
  tableData.value = res.data.treeMenu

}
const handleDialogValue=(userId:number)=>{
  if(userId){
    id.value=userId;
    dialogTitle.value="用户修改"
  }else{
    id.value=-1;
    dialogTitle.value="用户添加"
  }
  dialogVisible.value=true
}
const handleDelete=async (id:number)=>{
  /*let ids = []
  if(id){
    ids.push(id)
  }else{
    multipleSelection.value.forEach(row=>{
      ids.push(row.id)
    })
  }
  const res=await requestUtil.post("sys/user/delete",ids)
  if(res.data.code==200){
    ElMessage({
      type: 'success',
      message: '执行成功!'
    })
    initMenuList();
  }else{
    ElMessage({
      type: 'error',
      message: res.data.msg
    })
  }*/
}

onMounted(()=>{
  initMenuList()
})
</script>

<template>
  <div class="app-container">
    <el-row class="header">
      <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
    </el-row>
    <el-table :data="tableData" row-key="id" stripe
              style="width: 100%; margin-bottom: 20px;"
              border default-expand-all
              :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
      <el-table-column prop="name" label="菜单名称" width="200"/>
      <el-table-column prop="icon" label="图标" width="70" align="center">
        <template v-slot="scope">
          <el-icon v-if="scope.row.icon != '#'">
            <component :is="scope.row.icon"/>
          </el-icon>
        </template>
      </el-table-column>
      <el-table-column prop="orderNum" label="排序" width="70" align="center"/>
      <el-table-column prop="perms" label="权限标识" width="200"/>
      <el-table-column prop="path" label="组件路径" width="180"/>
      <el-table-column prop="menuType" label="菜单类型" width="120" align="center">
        <template v-slot="scope">
          <el-tag size="small" v-if="scope.row.menuType === 'M'" type="danger" effect="dark">目录</el-tag>
          <el-tag size="small" v-else-if="scope.row.menuType === 'C'" type="success" effect="dark">菜单</el-tag>
          <el-tag size="small" v-else-if="scope.row.menuType === 'F'" type="warning" effect="dark">按钮</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间" align="center"/>
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" />
          <el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button type="danger" icon="Delete"/>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
  </div>
  <Dialog v-model="dialogVisible" :dialog-visible="dialogVisible" :id="id" :dialog-title="dialogTitle" @initMenuList="initMenuList"/>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

3.浏览器测试[因为后台已经写好了所以只需要改前端页面] 

 修改和添加功能

1.后端:在SysMenuController提供添加或修改接口及根据id查询菜单接口

/**
 * 添加或修改
 * */
@RequestMapping("saveOrUpdate")
@PreAuthorize("hasAuthority('system:menu:add')"+"||"+"hasAuthority('system:menu:edit')")
public R saveOrUpdate(@RequestBody SysMenu sysMenu){
    if (sysMenu.getId() == null || sysMenu.getId() == -1){
        sysMenu.setCreateTime(new Date());
        sysMenuService.save(sysMenu);
    }else {
        sysMenu.setUpdateTime(new Date());
        sysMenuService.updateById(sysMenu);
    }

    return R.success();
}

/**
 * 根据id查询
 * */
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('system:menu:query')")
public R findById(@PathVariable("id") Integer id){
    SysMenu sysMenu = sysMenuService.getById(id);
    Map<String, Object> map = new HashMap<>();
    map.put("sysMenu",sysMenu);
    return R.success(map);
}


2.前端: 在src/api/menu/index.ts 新增接口

index.ts
import request from "@/util/request";
import type {MenuInfo} from "@/api/menu/type"

enum API {
    ADD_OR_UPDATE_URL = 'sys/menu/saveOrUpdate',
    FIND_BY_ID_URL = 'sys/menu/'
}
export const reqAddOrUpdate = (data:MenuInfo) => request.post<any,any>(API.ADD_OR_UPDATE_URL,data)
export const reqRemove = (id:number) => request.get<any,any>(API.REMOVE_URL + id)

type.ts
export interface MenuInfo {
    id:number,
    name:string,
    icon: string,
    orderNum: number,
    updateTime:  number,
    remark: string,
    parentId: number,
    path: string,
    component: string,
    children: MenuInfo[]
}

3.前端: 修改src/views/sys/menu/components/index.vue

<script setup>
import {defineEmits, defineProps, ref, watch} from "vue"
import { ElMessage } from 'element-plus'
import {reqAddOrUpdate, reqFindById} from "@/api/menu";
const tableData=ref([])
const props= defineProps( {
  id:{
    type:Number,
    default:-1,
    required:true
  },
  dialogTitle:{
    type:String,
    default:'',
    required:true
  },
  dialogVisible:{
    type:Boolean,
    default:false,
    required:true
  },
  tableData:{
    type:Array,
    default:[],
    required:true
  }
})
const form=ref({ id:-1, parentId:'', menuType:"M", icon:'', name:'', perms:'', component:'', orderNum:1 })
const rules=ref({
  parentId:[{ required: true, message: '请选择上级菜单'}],
  name: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }]
})
const formRef=ref(null)
const initFormData = async(id)=>{
  const res = await reqFindById(id)
  form.value = res.data.sysMenu
}
watch(()=>props.dialogVisible, ()=>{
  let id=props.id
  tableData.value = props.tableData
  if(id!=-1){
    initFormData(id)
  }else{
    form.value = {
      id:-1,
      parentId:'',
      menuType:"M",
      icon:'',
      name:'',
      perms:'',
      component:'',
      orderNum:1
    }
  }
})
const emits=defineEmits(['update:modelValue','initMenuList'])
const handleClose= ()=>{
  emits('update:modelValue',false)
}
const handleConfirm=()=>{
  formRef.value.validate(async(valid)=>{
    if(valid){
      let result=await reqAddOrUpdate(form.value)
      if(result.code==200){
        ElMessage.success("执行成功!")
        formRef.value.resetFields()
        emits("initMenuList")
        handleClose()
      }else {
        ElMessage.error(result.msg);
      }
    }else{
      console.log("fail")
    }
  })
}
</script>

<template>
  <el-dialog model-value="dialogVisible" :title="dialogTitle" width="30%" @close="handleClose" >
    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
      <el-form-item label="上级菜单" prop="parentId">
        <el-select v-model="form.parentId" placeholder="请选择上级菜单">
          <template v-for="item in tableData">
            <el-option :label="item.name" :value="item.id"></el-option>
            <template v-for="child in item.children">
              <el-option :label="child.name" :value="child.id">
                <span>{
  
  { " -- " + child.name }}</span>
              </el-option>
            </template>
          </template>
        </el-select>
      </el-form-item>
      <el-form-item label="菜单类型" prop="menuType" label-width="100px">
        <el-radio-group v-model="form.menuType">
          <el-radio :label="'M'">目录</el-radio>
          <el-radio :label="'C'">菜单</el-radio>
          <el-radio :label="'F'">按钮</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="菜单图标" prop="icon">
        <el-input v-model="form.icon" />
      </el-form-item>
      <el-form-item label="菜单名称" prop="name">
        <el-input v-model="form.name" />
      </el-form-item>
      <el-form-item label="权限标识" prop="perms">
        <el-input v-model="form.perms" /> </el-form-item>
      <el-form-item label="组件路径" prop="component">
        <el-input v-model="form.component" />
      </el-form-item>
      <el-form-item label="显示顺序" prop="orderNum">
        <el-input-number v-model="form.orderNum" :min="1" label="显示顺序"></el-input-number>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button type="primary" @click="handleConfirm">确认</el-button>
        <el-button @click="handleClose">取消</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<style scoped> </style>

4.前端:修改 src/views/sys/menu/index.vue

<script setup lang="ts">
import {ref, onMounted, reactive} from "vue"
import Dialog from './components/index.vue'
import {ElMessage} from "element-plus"
import {reqTreeMenu} from "@/api/role";
import {reqRemove} from "@/api/menu";

let tableData=ref([])
let dialogVisible=ref(false)
let dialogTitle=ref("")
let id=ref(-1)

const initMenuList= async()=>{
  const res= await reqTreeMenu()
  console.log()
  tableData.value = res.data.treeMenu

}
const handleDialogValue=(userId:number)=>{
  if(userId){
    id.value=userId;
    dialogTitle.value="用户修改"
  }else{
    id.value=-1;
    dialogTitle.value="用户添加"
  }
  dialogVisible.value=true
}
const handleDelete=async (id:number)=>{

  ...
}

onMounted(()=>{
  initMenuList()
})
</script>

<template>
  <div class="app-container">
    <el-row class="header">
      <el-button type="success" icon="Plus" @click="handleDialogValue(0)">新增</el-button>
    </el-row>
    <el-table :data="tableData" row-key="id" stripe
              style="width: 100%; margin-bottom: 20px;"
              border default-expand-all
              :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
      <el-table-column prop="name" label="菜单名称" width="200"/>
      <el-table-column prop="icon" label="图标" width="70" align="center">
        <template v-slot="scope">
          <el-icon v-if="scope.row.icon != '#'">
            <component :is="scope.row.icon"/>
          </el-icon>
        </template>
      </el-table-column>
      <el-table-column prop="orderNum" label="排序" width="70" align="center"/>
      <el-table-column prop="perms" label="权限标识" width="200"/>
      <el-table-column prop="path" label="组件路径" width="180"/>
      <el-table-column prop="menuType" label="菜单类型" width="120" align="center">
        <template v-slot="scope">
          <el-tag size="small" v-if="scope.row.menuType === 'M'" type="danger" effect="dark">目录</el-tag>
          <el-tag size="small" v-else-if="scope.row.menuType === 'C'" type="success" effect="dark">菜单</el-tag>
          <el-tag size="small" v-else-if="scope.row.menuType === 'F'" type="warning" effect="dark">按钮</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间" align="center"/>
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" />
          <el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button type="danger" icon="Delete"/>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
  </div>
  <Dialog v-model="dialogVisible" :tableData="tableData" :dialogVisible="dialogVisible" :id="id" :dialogTitle="dialogTitle" @initMenuList="initMenuList"/>
</template>

<style lang="scss" scoped>
.header{
  padding-bottom: 16px;
  box-sizing: border-box;
}
.el-pagination{
  float: right;
  padding: 20px;
  box-sizing: border-box;
}
::v-deep th.el-table__cell{
  word-break: break-word;
  background-color: #f8f8f9 !important;
  color: #515a6e;
  height: 40px;
  font-size: 13px;
}
.el-tag--small {
  margin-left: 5px;
}
</style>

5.浏览器测试 

 删除功能

1.后端:在SysMenuController提供删除菜单接口

/**
 * 删除
 * 需要删除两个表的数据
 * */
@GetMapping("/delete/{id}")
@PreAuthorize("hasAuthority('system:menu:delete')")
public R delete(@PathVariable("id") Long id){
    int count = sysMenuService.count(new QueryWrapper<SysMenu>().eq("parent_id", id));
    if (count > 0){// 若有子菜单则不能直接删除
        return R.fail("请先删除子菜单");
    }
    sysMenuService.removeById(id);
    return R.success();
}

2.前端:在src/api/menu/index.ts 新增接口

...

enum API {
 
    REMOVE_URL = 'sys/menu/delete/'
}
...
export const reqFindById = (id:number) => request.get<any,any>(API.FIND_BY_ID_URL + id)

 3.前端: 修改src/views/sys/menu/index.vue

<script setup lang="ts">
...
const handleDelete=async (id:number)=>{

  const res=await reqRemove(id)
  if(res.code==200){
    ElMessage({
      type: 'success',
      message: '执行成功!'
    })
    initMenuList();
  }else{
    ElMessage({
      type: 'error',
      message: res.msg
    })
  }
}

onMounted(()=>{
  initMenuList()
})
</script>

<template>
  <div class="app-container">
    ...
    <el-table :data="tableData" row-key="id" stripe
              style="width: 100%; margin-bottom: 20px;"
              border default-expand-all
              :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
      ...
      <el-table-column prop="action" label="操作" width="400" fixed="right" align="center">
        <template v-slot="scope" >
          <el-button type="primary" icon="Edit" @click="handleDialogValue(scope.row.id)" />
          <el-popconfirm title="您确定要删除这条记录吗?" @confirm="handleDelete(scope.row.id)">
            <template #reference>
              <el-button type="danger" icon="Delete"/>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
  </div>
  <Dialog v-model="dialogVisible" :tableData="tableData" :dialogVisible="dialogVisible" :id="id" :dialogTitle="dialogTitle" @initMenuList="initMenuList"/>
</template>

<style lang="scss" scoped>
...
</style>

4.浏览器测试

### 回答1: 在 Spring Boot 中配置数据权限,需要以下几步: 1. 引入依赖:在项目的 pom.xml 文件中引入相应的依赖,比如使用 MyBatis 时可以引入 mybatis-spring-boot-starter 依赖。 2. 配置数据源:在 application.properties 文件中配置数据源,包括数据库地址、用户名、密码等信息。 3. 在实体类中添加数据权限字段:在实体类中添加数据权限相关的字段,比如创建者、所属部门等。 4. 在 DAO 层中添加数据权限过滤:在 DAO 层的查询方法中添加数据权限的过滤条件,比如使用 MyBatis 的时候可以使用 if 语句来判断并拼接过滤条件。 5. 在 Service 层中处理数据权限:在 Service 层中获取当前登录用户的数据权限信息,并将其传递给 DAO 层进行过滤。 6. 在控制层中获取数据权限:在控制层中获取当前登录用户的数据权限信息,并将其传递给 Service 层进行处理。 这是一种常见的配置数据权限的方式,具体实现还可以根据具体的需要进行调整。 ### 回答2: 在Spring Boot框架中配置数据权限需要遵循以下步骤: 1. 引入所需的依赖:在项目的pom.xml文件中添加相关的依赖,例如Spring Security和Spring Data JPA。 2. 配置Spring Security:在项目的配置文件(比如application.properties或application.yml)中配置Spring Security,开启权限验证。 3. 编写自定义的数据权限规则:创建一个实现了org.springframework.security.access.PermissionEvaluator接口的类,该类用于定义数据权限规则。在这个类中,必须实现evaluate()方法以及对应的hasPermission()方法,用于判断用户是否有权限操作某个资源。 4. 在业务逻辑中应用数据权限规则:根据具体的业务需求,在Service层或Controller层的方法中,调用数据权限规则类中的方法来判断用户是否有权限进行某些操作。 5. 配置数据过滤器:为了确保数据权限规则生效,在项目中需要配置数据过滤器。这可以通过自定义过滤器或使用已有的框架来实现。例如,可以使用Spring Data JPA中的Specification和QueryDSL语法来进行数据的过滤和筛选。 6. 测试和调试:最后,针对不同的角色权限编写相应的测试用例,并进行测试和调试,确保数据权限配置的正确性和完整性。 通过以上步骤,我们可以在Spring Boot框架中配置数据权限,确保用户只能访问、操作其具有权限的数据,提升系统的安全性和数据的保密性。 ### 回答3: 在Spring Boot框架中,配置数据权限可以通过以下步骤进行: 1. 首先,需要在项目的pom.xml文件中添加相应的依赖,例如添加Spring Security依赖。 2. 接下来,在项目的配置文件(application.properties或application.yml)中配置数据库连接信息、权限配置等。 3.Spring Boot启动类中添加@EnableGlobalMethodSecurity注解,开启全局方法权限验证。 4. 创建自定义的权限控制类,实现自定义的权限验证逻辑。可以继承AbstractSecurityInterceptor类,并重写相关方法。 5. 在自定义权限控制类中,使用@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter等注解进行权限控制的配置。例如使用@PreAuthorize注解来控制方法的访问权限。 6. 配置数据库表结构,创建用于存储用户角色权限等信息的表。 7. 定义用户角色表和角色权限表的关联关系,并进行适当的角色权限的分配。 8. 编写Controller层的接口方法,使用上述注解来进行权限控制。 9.登录页面进行用户认证,验证用户的账号和密码是否正确,之后可以从数据库中获取该用户拥有的角色权限。 10. 在接口方法的实现中,根据用户角色权限对数据进行过滤或限制。 这样,当用户访问接口时,系统会根据用户角色权限进行验证和授权,只有满足条件的用户才能够正常访问和操作相应的数据。 以上是关于在SpringBoot框架中配置数据权限的一般步骤,具体的配置和实现方式可以根据实际需求和项目情况进行适当调整和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值