权限通用项目

Maven-聚合工程

  • guigu-auth-parent: 根目录,管理子模块

    • common:公共类父模块
      • common-log: 系统操作日志模块
      • common-util: 核心工具类
      • service-util: service 模块工具类
      • spring-security 业务模块
    • model: 实体类模块
    • service-system: 系统权限模块
    目录结构

Knife4j

  • 文档

    https://doc.xiaominfo.com/

  • 是什么

    Knife4j 是一个集Swagger2OpenAPI3 为一体的增强解决方案

  • 快速开始

    1. 第一步:创建Spring Boot 项目,并且在pom.xml 中引入Knife4j 的依赖包,Maven 坐标如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-spring-boot-starter</artifactId>
      </dependency>
      <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.10.5</version>
      <scope>compile</scope>
      </dependency>
    2. 创建Swagger 配置依赖,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      package com.example.system.config;


      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import springfox.documentation.builders.ApiInfoBuilder;
      import springfox.documentation.builders.ParameterBuilder;
      import springfox.documentation.builders.PathSelectors;
      import springfox.documentation.builders.RequestHandlerSelectors;
      import springfox.documentation.schema.ModelRef;
      import springfox.documentation.service.ApiInfo;
      import springfox.documentation.service.Contact;
      import springfox.documentation.service.Parameter;
      import springfox.documentation.spi.DocumentationType;
      import springfox.documentation.spring.web.plugins.Docket;
      import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

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

      /**
      * knife4j 配置信息
      */
      @Configuration
      @EnableSwagger2WebMvc
      public class Knife4jConfig {

      @Bean
      public Docket adminApiConfig() {
      List<Parameter> pars = new ArrayList<>();
      ParameterBuilder tokenPar = new ParameterBuilder();
      tokenPar.name("token")
      // 描述字段支持 Markdown 语法
      .description("用户token")
      .defaultValue("")
      .modelRef(new ModelRef("string"))
      .parameterType("header")
      .required(false)
      .build();
      pars.add(tokenPar.build());
      // 指定使用 Swagger2 规范
      Docket adminApi = new Docket(DocumentationType.SWAGGER_2)
      // 分组名称
      .groupName("adminApi")
      .apiInfo(adminApiInfo())
      .select()
      // 这里指定 Controller 扫描包路径
      .apis(RequestHandlerSelectors.basePackage("com.example"))
      .paths(PathSelectors.regex("/admin/.*"))
      .build()
      .globalOperationParameters(pars);
      return adminApi;
      }

      private ApiInfo adminApiInfo() {
      return new ApiInfoBuilder()
      .title("后台管理系统-API文档")
      .description("本文档描述了后台管理系统微服务接口定义")
      .version("1.0")
      .contact(new Contact("coder-itl", "http://coderitl.github.io", "3327511395@qq.com"))
      .build();
      }
      }
    3. Controller 添加注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      package com.example.system.controller;

      import com.example.model.system.SysRole;
      import com.example.system.service.SysRoleService;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.*;

      import java.util.List;

      @Api(tags = "角色管理")
      @Slf4j
      @CrossOrigin
      @RestController
      @RequestMapping("/admin/system/sysRole")
      public class SysRoleController {
      @Autowired
      private SysRoleService sysRoleService;

      /**
      * 查询所有
      *
      * @return
      */
      @ApiOperation(value = "获取全部角色列表")
      @GetMapping("/findAll")
      public List<SysRole> findAll() {
      log.info("findAll:..............................");
      List<SysRole> list = sysRoleService.list();
      return list;
      }

      /**
      * 逻辑删除
      *
      * @param id
      * @return
      */
      @DeleteMapping("/remove/{id}")
      public boolean removeRole(@PathVariable("id") Long id) {
      log.info("PathVariable Id value:{}", id);
      return sysRoleService.removeById(id);
      }

      }

    4. 启动,访问

      http://localhost:8080/doc.html(该接口项目的端口)

定义统一返回结果对象

  • 创建统一返回结果状态信息类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.example.common.result;


    import lombok.Getter;

    /**
    * 统一返回结果状态信息类
    */
    @Getter
    public enum ResultCodeEnum {
    SUCCESS(200, "成功"),
    FAIL(201, "失败"),
    SERVICE_ERROR(2012, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    ILLEGAL_REQUEST(205, "非法请求"),
    REPEAT_SUBMIT(206, "重复提交"),
    ARGUMENT_VALID_ERROR(210, "参数校验异常"),

    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限"),
    ACCOUNT_ERROR(214, "账号不正确"),
    PASSWORD_ERROR(215, "密码不正确"),
    LOGIN_MOBLE_ERROR(216, "账号不正确"),
    ACCOUNT_STOP(217, "账号已停用"),
    NODE_ERROR(218, "该节点下有子节点,不可以删除");

    private Integer code;

    private String message;

    private ResultCodeEnum(Integer code, String message) {
    this.code = code;
    this.message = message;
    }
    }
  • 全局统一返回结果类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    package com.example.common.result;


    import lombok.Data;

    /**
    * 全局统一返回结果类
    */
    @Data
    public class Result<T> {

    //返回码
    private Integer code;

    //返回消息
    private String message;

    //返回数据
    private T data;

    public Result() {
    }

    // 返回数据
    protected static <T> Result<T> build(T data) {
    Result<T> result = new Result<T>();
    if (data != null)
    result.setData(data);
    return result;
    }

    public static <T> Result<T> build(T body, Integer code, String message) {
    Result<T> result = build(body);
    result.setCode(code);
    result.setMessage(message);
    return result;
    }

    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
    Result<T> result = build(body);
    result.setCode(resultCodeEnum.getCode());
    result.setMessage(resultCodeEnum.getMessage());
    return result;
    }

    public static <T> Result<T> ok() {
    return Result.ok(null);
    }

    /**
    * 操作成功
    *
    * @param data baseCategory1List
    * @param <T>
    * @return
    */
    public static <T> Result<T> ok(T data) {
    Result<T> result = build(data);
    return build(data, ResultCodeEnum.SUCCESS);
    }

    public static <T> Result<T> fail() {
    return Result.fail(null);
    }

    /**
    * 操作失败
    *
    * @param data
    * @param <T>
    * @return
    */
    public static <T> Result<T> fail(T data) {
    Result<T> result = build(data);
    return build(data, ResultCodeEnum.FAIL);
    }

    public Result<T> message(String msg) {
    this.setMessage(msg);
    return this;
    }

    public Result<T> code(Integer code) {
    this.setCode(code);
    return this;
    }
    }
  • 修改控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    package com.example.system.controller;

    import com.example.common.result.Result;
    import com.example.model.system.SysRole;
    import com.example.system.service.SysRoleService;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;

    @Api(tags = "角色管理")
    @Slf4j
    @CrossOrigin
    @RestController
    @RequestMapping("/admin/system/sysRole")
    public class SysRoleController {
    @Autowired
    private SysRoleService sysRoleService;

    /**
    * 查询所有
    *
    * @return
    */
    @ApiOperation(value = "获取全部角色列表")
    @GetMapping("/findAll")
    public Result findAll() {
    log.info("findAll:..............................");
    List<SysRole> list = sysRoleService.list();
    return Result.ok(list);
    }

    /**
    * 逻辑删除
    *
    * @param id
    * @return
    */
    @ApiOperation(value = "逻辑删除")
    @DeleteMapping("/remove/{id}")
    public Result removeRole(@PathVariable("id") Long id) {
    log.info("PathVariable Id value:{}", id);
    boolean removeById = sysRoleService.removeById(id);
    if (removeById) {
    return Result.ok();
    } else {
    return Result.fail();
    }
    }

    }

MybatisPlus-条件分页查询

  • 第一步: 配置分页插件,通过配置类实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.example.system.config;

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;

    @EnableTransactionManagement
    @Configuration
    @MapperScan("com.example.system.mapper")
    public class MybatisPlusConfig {

    /**
    * @return
    */
    @Bean
    public MybatisPlusInterceptor addPaginationInnerInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 向 Mybatis 过滤器链中添加分页拦截器
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }

    }
  • 第二步: 创建Controller 方法、创建Service 方法、创建Mapper 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // controller
    @ApiOperation("条件分页查询")
    @GetMapping("/{page}/{limit}")
    public Result findPageQueryRole(@PathVariable("page") Long page, @PathVariable("limit") Long limit, SysRoleQueryVo sysRoleQueryVo) {
    // 创建 page 对象
    Page<SysRole> pageparam = new Page<>(page, limit);
    IPage<SysRole> pageModel = sysRoleService.queryPage(pageparam, sysRoleQueryVo);
    return Result.ok(pageModel);
    }

    1
    2
    3
    4
    5
    6
    7
    // service && serviceImpl
    // 条件分页查询
    @Override
    public IPage<SysRole> queryPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo) {
    IPage<SysRole> pageModel = baseMapper.queryPage(pageparam, sysRoleQueryVo);
    return pageModel;
    }
    1
    2
    // mapper
    IPage<SysRole> queryPage(Page<SysRole> pageparam, @Param("vo") SysRoleQueryVo sysRoleQueryVo);
  • 第三步: 创建mapper xml 配置文件.编写sql 语句实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.system.mapper.SysRoleMapper">
    <!-- public IPage<SysRole> selectPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo); -->
    <resultMap id="roleMap" type="com.example.model.system.SysRole" autoMapping="true"></resultMap>

    <sql id="columns">
    id,role_name,role_code,description,create_time,update_time,is_deleted
    </sql>

    <select id="queryPage" resultMap="roleMap">
    select
    <include refid="columns"/>
    from sys_role
    <where>
    <if test="vo.roleName != null and vo.roleName != ''">
    and role_name like CONCAT('%',#{vo.roleName},'%')
    </if>
    and is_deleted = 0
    </where>
    order by id desc
    </select>

    </mapper>
  • 第四步: 修改配置文件关于mapper 路径映射

    1
    2
    3
    4
    mybatis-plus:
    configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    mapper-locations: classpath:com/example/mapper/*Mapper.xml

统一异常处理

全局异常处理
  • 只要出现异常,就执行这个处理

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @ControllerAdvice
    public class GlobalExceptionHandle {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Result error(Exception e) {
    e.printStackTrace();
    return Result.fail().message("执行了全局异常处理");
    }
    }
    全局异常捕获处理
特定异常处理
  • 针对特定异常处理

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.example.system.exception;

    import com.example.common.result.Result;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;

    @ControllerAdvice
    public class GlobalExceptionHandle {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Result error(Exception e) {
    e.printStackTrace();
    return Result.fail().message("执行了全局异常处理");
    }
    // 特定异常处理
    @ResponseBody
    @ExceptionHandler(ArithmeticException.class)
    public Result error(ArithmeticException e) {
    e.printStackTrace();
    return Result.fail().message("执行了特定异常");
    }
    }

    处理特定异常
自定义异常处理
  • 自己编写异常类,手动抛出异常

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class coderItlException extends RuntimeException {
    // 状态码
    private Integer code;
    // 消息
    private String message;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    package com.example.system.exception;

    import com.example.common.result.Result;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;

    @ControllerAdvice
    public class GlobalExceptionHandle {
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Result error(Exception e) {
    e.printStackTrace();
    return Result.fail().message("执行了全局异常处理");
    }

    @ResponseBody
    @ExceptionHandler(ArithmeticException.class)
    public Result error(ArithmeticException e) {
    e.printStackTrace();
    return Result.fail().message("执行了特定异常");
    }
    // 自定义异常处理
    @ResponseBody
    @ExceptionHandler(coderItlException.class)
    public Result error(coderItlException e) {
    return Result.fail().code(e.getCode()).message(e.getMessage());
    }
    }

    1
    2
    3
    4
    5
    6
    // 在需要的地方主动抛出异常
    try {
    System.out.println(1 / 0);
    } catch (Exception e) {
    throw new coderItlException(10010, "执行了自定义异常");
    }
    执行自定义异常

代码生成器使用

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- mybatis-plus 3.4.1 使用旧版本 -->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
    </dependency>

    <dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.0</version>
    </dependency>
  • 创建测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    package com.example.system;


    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.generator.AutoGenerator;
    import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
    import com.baomidou.mybatisplus.generator.config.GlobalConfig;
    import com.baomidou.mybatisplus.generator.config.PackageConfig;
    import com.baomidou.mybatisplus.generator.config.StrategyConfig;
    import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;

    public class CodeGet {

    public static void main(String[] args) {

    // 1、创建代码生成器
    AutoGenerator mpg = new AutoGenerator();

    // 2、全局配置
    // 全局配置
    GlobalConfig gc = new GlobalConfig();
    // 使用时需要修改
    gc.setOutputDir("E:\\auth-parent\\service-system" + "/src/main/java");

    gc.setServiceName("%sService"); //去掉Service接口的首字母I
    gc.setAuthor("coder-itl");
    gc.setOpen(false);
    mpg.setGlobalConfig(gc);

    // 3、数据源配置
    DataSourceConfig dsc = new DataSourceConfig();
    // 数据库名称需要修改
    dsc.setUrl("jdbc:mysql://localhost:3306/guigu-auth?serverTimezone=GMT%2B8&useSSL=false");
    dsc.setDriverName("com.mysql.cj.jdbc.Driver");
    dsc.setUsername("root");
    dsc.setPassword("root");
    dsc.setDbType(DbType.MYSQL);
    mpg.setDataSource(dsc);

    // 4、包配置
    PackageConfig pc = new PackageConfig();
    pc.setParent("com.example");
    pc.setModuleName("system"); // 模块名
    pc.setController("controller");
    pc.setService("service");
    pc.setMapper("mapper");
    mpg.setPackageInfo(pc);

    // 5、策略配置
    StrategyConfig strategy = new StrategyConfig();
    // 要生成的表
    strategy.setInclude("sys_user");

    strategy.setNaming(NamingStrategy.underline_to_camel);// 数据库表映射到实体的命名策略

    strategy.setColumnNaming(NamingStrategy.underline_to_camel);// 数据库表字段映射到实体的命名策略
    strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter 链式操作

    strategy.setRestControllerStyle(true); //restful api风 格控制器
    strategy.setControllerMappingHyphenStyle(true); // url中驼峰转连字符

    mpg.setStrategy(strategy);

    // 6、执行
    mpg.execute();
    }
    }

    实现效果(局部修改)

Token

  • 介绍

    JWT JSON Web Token 的缩写,即JSON Web 令牌,是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于JSON 的开放标准。

    JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

    JWT 最重要的作用就是对 token 信息的防伪作用。

  • 有效载荷

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    iss: jwt签发者
    sub: 主题
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

    // 除以上默认字段外,我们还可以自定义私有字段,如下例:
    {
    "name": "Helen",
    "role": "editor",
    "avatar": "helen.jpg"
    }

    // 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。JSON对象也使用Base64 URL算法转换为字符串保存。


  • 使用步骤

    • 添加依赖

      1
      2
      3
      4
      <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      </dependency>
    • 创建工具类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      package com.example.common.utils;

      import io.jsonwebtoken.*;
      import org.springframework.util.StringUtils;

      import java.util.Date;

      /**
      * 生成JSON Web令牌的工具类
      */
      public class JwtHelper {

      private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;
      private static String tokenSignKey = "123456";

      public static String createToken(Long userId, String username) {
      String token = Jwts.builder()
      .setSubject("AUTH-USER")
      .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
      .claim("userId", userId)
      .claim("username", username)
      .signWith(SignatureAlgorithm.HS512, tokenSignKey)
      .compressWith(CompressionCodecs.GZIP)
      .compact();
      return token;
      }

      public static String getUserId(String token) {
      try {
      if (StringUtils.isEmpty(token)) return null;

      Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
      Claims claims = claimsJws.getBody();
      String userId = (String) claims.get("userId");
      return userId;
      } catch (Exception e) {
      e.printStackTrace();
      return null;
      }
      }

      public static String getUsername(String token) {
      try {
      // StringUtils: 是 web 包下的
      if (StringUtils.isEmpty(token)) return "";

      Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
      Claims claims = claimsJws.getBody();
      return (String) claims.get("username");
      } catch (Exception e) {
      e.printStackTrace();
      return null;
      }
      }

      // 测试
      public static void main(String[] args) {
      String token = JwtHelper.createToken("1", "admin");
      System.out.println(token);
      System.out.println(JwtHelper.getUserId(token));
      System.out.println(JwtHelper.getUsername(token));
      }
      }
  • MD5 加密工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.example.common.utils;

    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;


    public final class MD5 {

    public static String encrypt(String strSrc) {
    try {
    char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
    '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    byte[] bytes = strSrc.getBytes();
    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(bytes);
    bytes = md.digest();
    int j = bytes.length;
    char[] chars = new char[j * 2];
    int k = 0;
    for (int i = 0; i < bytes.length; i++) {
    byte b = bytes[i];
    chars[k++] = hexChars[b >>> 4 & 0xf];
    chars[k++] = hexChars[b & 0xf];
    }
    return new String(chars);
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    throw new RuntimeException("MD5加密出错!!+" + e);
    }
    }
    }

前端

vue-admin-template
  • 下载

    https://github.com/PanJiaChen/vue-admin-template

  • 解压

  • 依赖下载

    1
    2
    3
    4
    5
    6
    # 配置镜像
    npm config set registry http://registry.npmmirror.com
    yarn config set registry http://registry.npmmirror.com

    # 切换回默认镜像源
    npm config set registryhttps://registry.npmjs.org
    1
    2
    3
    4
    # npm
    npm install
    # yarn
    yarn install
  • 启动

    1
    2
    3
    4
    # npm 启动
    npm run dev
    # yarn 启动
    yarn run dev
    通过package.json 查看scripts 确定启动命令
  • 访问

    访问首页
  • 依赖下载出错问题

    提示下载,直接下载无效
    • 解决方案

      • 先删除node_modules

      • 在下载如下依赖

        1
        npm install --save core-js
      • 再次执行

        1
        2
        # /: 或者
        npm install / yarn install
Vue-admin-template文件目录
  • 文件信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    |-dist 生产环境打包生成的打包项目
    |-mock 产生模拟数据
    |-public 包含会被自动打包到项目根路径的文件夹
    |-index.html 唯一的页面
    |-src
    |-api 包含接口请求函数模块
    |-table.js 表格列表mock数据接口的请求函数
    |-user.js 用户登陆相关mock数据接口的请求函数
    |-assets 组件中需要使用的公用资源
    |-404_images 404页面的图片
    |-components 非路由组件
    |-SvgIcon svg图标组件
    |-Breadcrumb 面包屑组件(头部水平方向的层级组件)
    |-Hamburger 用来点击切换左侧菜单导航的图标组件
    |-icons
    |-svg 包含一些svg图片文件
    |-index.js 全局注册SvgIcon组件,加载所有svg图片并暴露所有svg文件名的数组
    |-layout
    |-components 组成整体布局的一些子组件
    |-mixin 组件中可复用的代码
    |-index.vue 后台管理的整体界面布局组件
    |-router
    |-index.js 路由器
    |-store
    |-modules
    |-app.js 管理应用相关数据
    |-settings.js 管理设置相关数据
    |-user.js 管理后台登陆用户相关数据
    |-getters.js 提供子模块相关数据的getters计算属性
    |-index.js vuexstore
    |-styles
    |-xxx.scss 项目组件需要使用的一些样式(使用scss)
    |-utils 一些工具函数
    |-auth.js 操作登陆用户的token cookie
    |-get-page-title.js 得到要显示的网页title
    |-request.js axios二次封装的模块
    |-validate.js 检验相关工具函数
    |-index.js 日期和请求参数处理相关工具函数
    |-views 路由组件文件夹
    |-dashboard 首页
    |-login 登陆
    |-App.vue 应用根组件
    |-main.js 入口js
    |-permission.js 使用全局守卫实现路由权限控制的模块
    |-settings.js 包含应用设置信息的模块
    |-.env.development 指定了开发环境的代理服务器前缀路径
    |-.env.production 指定了生产环境的代理服务器前缀路径
    |-.eslintignore eslint的忽略配置
    |-.eslintrc.js eslint的检查配置
    |-.gitignore git的忽略配置
    |-.npmrc 指定npm的淘宝镜像和sass的下载地址
    |-babel.config.js babel的配置
    |-jsconfig.json 用于vscode引入路径提示的配置
    |-package.json 当前项目包信息
    |-package-lock.json 当前项目依赖的第三方包的精确信息
    |-vue.config.js webpack相关配置(如: 代理服务器)
登录退出改造
  • 通过network 查看到登录后发起的请求

    • login

      1
      2
      3
      4
      5
      6
      7
      // POST
      {
      "code":20000,
      "data":{
      "token":"admin-token"
      }
      }
    • info

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // GET
      {
      "code":20000,
      "data":{
      "roles":[
      "admin"
      ],
      "introduction":"I am a super administrator",
      "avatar":"https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
      "name":"Super Admin"
      }
      }
      分析
  • 通过上述分析,后端模拟实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package com.example.system.controller;

    import com.example.common.result.Result;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.*;

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

    @Api(tags = "用户登录接口")
    @Slf4j
    @CrossOrigin
    @RestController
    @RequestMapping("/admin/system/index")
    public class IndexController {
    @ApiOperation("用户登录")
    @PostMapping("/login")
    public Result login() {
    Map<String, Object> map = new HashMap<>();
    map.put("token", "admin-token");
    return Result.ok(map);
    }


    @ApiOperation("用户信息")
    @GetMapping("/info")
    public Result info() {
    Map<String, Object> map = new HashMap<>();
    map.put("roles", "[admin]");
    map.put("introduction", "I am a super administrator");
    map.put("avatar", "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
    map.put("name", "Super Admin");
    return Result.ok(map);
    }
    }

  • 配置后端路径IP+ 端口号

    • 修改vue.config.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // before: require('./mock/mock-server.js')
      proxy: {
      '/dev-api': { // 匹配所有以 '/dev-api'开头的请求路径
      target: 'http://localhost:8800',
      changeOrigin: true, // 支持跨域
      pathRewrite: { // 重写路径: 去掉路径中开头的'/dev-api'
      '^/dev-api': ''
      }
      }
      }
    • 修改src/utils 里面的request.js 文件

      1
      2
      3
      4
      5
      6
      7
      8
      9

      // if the custom code is not 20000, it is judged as an error.
      if (res.code !== 20000) {
      Message({
      message: res.message || 'Error',
      type: 'error',
      duration: 5 * 1000
      })
      // 20000 修改为自己所需要的值信息
  • 修改登录接口和提交方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // src/api/user.js

    export function login(data) {
    return request({
    // 后端提供的接口
    url: "/admin/system/index/login",
    method: "post",
    data,
    });
    }

    export function getInfo(token) {
    return request({
    url: "/admin/system/index/info",
    method: "get",
    params: { token },
    });
    }
  • 退出实现

    • 实现一: 后端实现,前端调用接口

      1
      2
      3
      4
      5
      @ApiOperation("用户退出")
      @PostMapping("/logout")
      public Result logout() {
      return Result.ok();
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // src/api/user

      export function logout() {
      return request({
      url: "/admin/system/index/logout",
      method: "post",
      });
      }

    • 实现二: 修改前端信息src/store/modules/user.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      login({ commit }, userInfo) {
      const { username, password } = userInfo
      return new Promise((resolve, reject) => {
      const { data } = response
      commit('SET_TOKEN', data.token)
      setToken(data.token)
      resolve()
      })
      },
角色列表实现
  • 实现步骤

    1. 添加角色管理路由

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24

      /* sysRole start */
      {
      path: "/system",
      component: Layout,
      redirect: "/system/sysUser",
      name: "System",
      meta: { title: "系统管理", icon: "el-icon-s-help" },
      children: [
      {
      path: "sysUser",
      name: "SysUser",
      component: () => import("@/views/system/sysUser/list"),
      meta: { title: "用户管理", icon: "tree" },
      },
      {
      path: "sysRole",
      name: "SysRole",
      component: () => import("@/views/system/sysRole/list"),
      meta: { title: "角色管理", icon: "table" },
      },
      ],
      },
      /* sysRole end */
    2. 创建路由对应的页面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <template>
      <div class="app-container">角色列表</div>
      </template>
      <script>
      export default {}
      </script>

      <style>
      </style>
      根据路由创建页面
    3. api 目录创建js 文件,定义角色管理接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      import request from "@/utils/request";
      const API_PATH = "/admin/system/sysRole/";
      export default {
      // 分页查询
      getPageList(page, limit, searchObj) {
      return request({
      url: `${API_PATH}/${page}/${limit}`,
      method: "get",
      params: searchObj,
      });
      },
      };
    4. 在具体功能页面调用api 定义的方法获取接口返回数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      <script>
      // 引入定义接口的 js 文件
      import api from '@/api/role/role'
      export default {
      data() {
      return {
      list: [], // 角色列表
      total: 0, // 总记录数
      page: 1, // 当前页
      limit: 3, // 每页显示记录数
      searchObj: {}, // 条件查询封装对象
      }
      },
      // 页面渲染之前执行
      created() {
      this.fetchData()
      },
      methods: {
      // 条件分页查询列表,pageNum=1 代表默认值
      async fetchData(pageNum = 1) {
      // 页数赋值
      this.page = pageNum
      // 发起 ajax 调用
      const { data: res } = await api.getPageList(
      this.page,
      this.limit,
      this.searchObj
      )
      console.log(res)
      // 角色列表赋值
      this.list = res.records
      this.total = res.total
      },
      },
      }
      </script>
      数据模型
    5. 把接口返回的数据进行处理,使用ElementUI 显示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      <template>
      <div class="app-container">
      <!-- 表格 -->
      <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%; margin-top: 10px"
      >
      <el-table-column label="序号" width="70" align="center">
      <template slot-scope="scope">
      {{ (page - 1) * limit + scope.$index + 1 }}
      </template>
      </el-table-column>

      <el-table-column prop="roleName" label="角色名称" />
      <el-table-column prop="roleCode" label="角色编码" />
      <el-table-column prop="createTime" label="创建时间" width="160" />
      <el-table-column label="操作" width="200" align="center">
      <template slot-scope="scope">
      <el-button
      type="primary"
      icon="el-icon-edit"
      size="mini"
      @click="edit(scope.row.id)"
      title="修改"
      />
      <el-button
      type="danger"
      icon="el-icon-delete"
      size="mini"
      @click="removeDataById(scope.row.id)"
      title="删除"
      />
      </template>
      </el-table-column>
      </el-table>
      </div>
      </template>
      <script>
      // 引入定义接口的 js 文件
      import api from '@/api/role/role'
      export default {
      data() {
      return {
      list: [], // 角色列表
      total: 0, // 总记录数
      page: 1, // 当前页
      limit: 3, // 每页显示记录数
      searchObj: {}, // 条件查询封装对象
      }
      },
      // 页面渲染之前执行
      created() {
      this.fetchData()
      },
      methods: {
      // 条件分页查询列表,pageNum=1 代表默认值
      async fetchData(pageNum = 1) {
      // 页数赋值
      this.page = pageNum
      // 发起 ajax 调用
      const { data: res } = await api.getPageList(
      this.page,
      this.limit,
      this.searchObj
      )
      console.log(res)
      // 角色列表赋值
      this.list = res.records
      this.total = res.total
      },
      },
      }
      </script>

      <style>
      </style>
  • SysRole 页面实现

    角色管理页面
    在这里插入图片描述
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    <template>
    <div class="app-container">
    <!--查询表单-->
    <div class="search-div">
    <el-form label-width="70px" size="small">
    <el-row>
    <el-col :span="24">
    <el-form-item label="角色名称">
    <el-input
    style="width: 100%"
    v-model="searchObj.roleName"
    placeholder="角色名称"
    ></el-input>
    </el-form-item>
    </el-col>
    </el-row>
    <el-row style="display: flex">
    <el-button
    type="primary"
    icon="el-icon-search"
    size="mini"
    @click="fetchData()"
    >搜索
    >
    <el-button icon="el-icon-refresh" size="mini" @click="resetData"
    >重置
    >
    </el-row>
    </el-form>
    </div>

    <!-- 表格 -->
    <el-table
    v-loading="listLoading"
    :data="list"
    stripe
    border
    style="width: 100%; margin-top: 10px"
    >
    <el-table-column label="序号" width="70" align="center">
    <template slot-scope="scope">
    {{ (page - 1) * limit + scope.$index + 1 }}
    </template>
    </el-table-column>

    <el-table-column prop="roleName" label="角色名称" />
    <el-table-column prop="roleCode" label="角色编码" />
    <el-table-column prop="createTime" label="创建时间" width="160" />
    <el-table-column label="操作" width="200" align="center">
    <template slot-scope="scope">
    <el-button
    type="primary"
    icon="el-icon-edit"
    size="mini"
    @click="edit(scope.row.id)"
    title="修改"
    />
    <el-button
    type="danger"
    icon="el-icon-delete"
    size="mini"
    @click="removeDataById(scope.row.id)"
    title="删除"
    />
    </template>
    </el-table-column>
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
    :current-page="page"
    :total="total"
    :page-size="limit"
    style="padding: 30px 0; text-align: center"
    layout="total, prev, pager, next, jumper"
    @current-change="fetchData"
    />
    </div>
    </template>
    <script>
    // 引入定义接口的 js 文件
    import api from '@/api/role/role'
    export default {
    data() {
    return {
    listLoading: true, // 是否显示加载图标
    list: [], // 角色列表
    total: 0, // 总记录数
    page: 1, // 当前页
    limit: 3, // 每页显示记录数
    searchObj: {}, // 条件查询封装对象
    }
    },
    // 页面渲染之前执行
    created() {
    this.fetchData()
    },
    methods: {
    // 条件分页查询列表,pageNum=1 代表默认值
    async fetchData(pageNum = 1) {
    // 页数赋值
    this.page = pageNum
    // 发起 ajax 调用
    const { data: res } = await api.getPageList(
    this.page,
    this.limit,
    this.searchObj
    )
    this.listLoading = false
    console.log(res)
    // 角色列表赋值
    this.list = res.records
    this.total = res.total
    },
    // 重置
    resetData() {
    // 清空表单
    this.searchObj = {}
    // 显示所有数据
    this.fetchData()
    },
    },
    }
    </script>

    <style>
    </style>
  • 角色删除

    • 前端请求接口定义

      1
      2
      3
      4
      5
      6
      7
      8
      // api
      // 根据 id 删除
      removeById(id) {
      return request({
      url: `${API_PATH}/remove/${id}`,
      method: "delete",
      });
      },
    • 事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 根据 id 删除
      removeDataById(id) {
      // debugger
      this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      })
      .then(() => {
      // promise
      // 点击确定,远程调用ajax
      return api.removeById(id)
      })
      .then((response) => {
      this.fetchData(this.page)
      this.$message.success(response.message || '删除成功')
      })
      },
    • Javascript 处理数值问题

      • 由于Javascript 对数值的精度问题,可能导致错误
      • 解决方案: 修改实体类中数据类型
  • 添加角色实现

    • 添加按钮

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <div class="tools-div">
      <el-button type="success" icon="el-icon-plus" size="mini" @click="add">添 加</el-button>
      </div>

      // 放在全局样式
      <style>
      .search-div {
      padding:10px;border: 1px solid #EBEEF5;border-radius:3px;
      }
      .tools-div {
      margin-top: 10px;padding:10px;border: 1px solid #EBEEF5;border-radius:3px;
      }
      </style>
    • api

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 添加角色
      saveRole(role) {
      return request({
      url: `${API_PATH}/save`,
      method: "post",
      // 后端存在 @RequestBody 注解时,需要前端传递 json 数据
      data: role,
      });
      },
    • element-ui 弹窗

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
      <el-form ref="dataForm" :model="sysRole" label-width="150px" size="small" style="padding-right: 40px;">
      <el-form-item label="角色名称">
      <el-input v-model="sysRole.roleName"/>
      </el-form-item>
      <el-form-item label="角色编码">
      <el-input v-model="sysRole.roleCode"/>
      </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
      <el-button @click="dialogVisible = false" size="small" icon="el-icon-refresh-right">取 消</el-button>
      <el-button type="primary" icon="el-icon-check" @click="saveOrUpdate()" size="small">确 定</el-button>
      </span>
      </el-dialog>
    • 事件

      1
      2
      3
      // 初始值
      sysRole: {}, // 封装添加的表单数据
      dialogVisible: false, // 添加角色弹框
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // 添加添加按钮出现弹窗
      add() {
      this.dialogVisible = true
      // 清空原始数据,便于添加
      this.sysRole = {}
      },
      // 添加或更新事件
      saveOrUpdate() {
      // 判断添加是修改还是更新
      if (!this.sysRole.id) {
      // 添加
      this.saveRole()
      } else {
      this.updateRole()
      }
      },
      // 添加角色事件
      saveRole() {
      api.saveRole(this.sysRole).then((res) => {
      // 1. 出现成功或失败的提示信息
      this.$message({
      type: 'success',
      message: '添加成功',
      })
      // 2. 关闭弹窗
      this.dialogVisible = false
      // 3. 刷新数据
      this.fetchData()
      })
      },
  • 编辑按钮事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 根据 id 删除
    removeDataById(id) {
    // debugger
    this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
    }).then(() => {
    // promise
    // 点击确定,远程调用 ajax
    api.removeById(id).then((res) => {
    this.$message.success(res.message || '删除成功')
    })
    this.fetchData(this.page)
    })
    },
  • 更新按钮事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 更新角色事件
    updateRole() {
    api.update(this.sysRole).then((res) => {
    // 1. 出现成功或失败的提示信息
    this.$message({
    type: 'success',
    message: '更新成功',
    })
    // 2. 关闭弹窗
    this.dialogVisible = false
    // 3. 刷新数据
    this.fetchData()
    })
    },
    // 修改-回显数据
    edit(id) {
    // 显示弹窗
    this.dialogVisible = true
    // 修改数据
    api.getRoleId(id).then((res) => {
    // 将获取到的数据赋值,通过双向绑定回显数据
    this.sysRole = res.data
    })
    },
    添加/修改弹窗 编辑回显数据
  • 批量删除

    • 定义api

      1
      2
      3
      4
      5
      6
      7
      8
      // 根据 id 批量删除
      batchRemove(idList) {
      return request({
      url: `${API_PATH}/batchRemove`,
      method: "delete",
      data: idList,
      });
      },
    • 前后端参数数据模型

      数据模型
    • 复选框实现

      1
      2
      // 在 table 表头内第一行添加
      <el-table-column type="selection" />
      添加位置 实现效果
    • 复选框事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // el-table 添加
      <el-table
      v-loading="listLoading"
      :data="list"
      stripe
      border
      style="width: 100%; margin-top: 10px"
      @selection-change="handleSelectionChange"
      >
      1
      2
      // 初始值
      selectValue: [], // 复选框选择内容封装
      1
      2
      3
      4
      // 复选框发生变化(选中时)时执行
      handleSelectionChange(selection) {
      this.selectValue = selection
      },
    • 批量删除

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      // 批量删除实现
      batchRemove() {
      // 判断是否可以删除
      if (this.selectValue.length == 0) {
      this.$message.warning('请选择要删除的记录')
      return
      }

      this.$confirm('此操作将永久删除该角色, 是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
      }).then(() => {
      // promise
      var idList = []
      this.selectValue.forEach((item) => {
      idList.push(item.id)
      })
      // 点击确定,远程调用 ajax
      api.batchRemove(idList).then((res) => {
      this.$message.success(res.message || '批量删除成功')
      // 刷新页面数据
      this.fetchData(this.page)
      })
      })
      },
用户管理模块
  • 条件分页查询用户

    1
    2
    3
    4
    5
    6
    7
    8
    // 条件分页查询用户
    @GetMapping("/{page}/{limit}")
    @ApiOperation(value = "用户列表")
    public Result list(@PathVariable Long page, @PathVariable Long limit, SysUserQueryVo sysUserQueryVo) {
    Page<SysUser> pageParam = new Page<>(page, limit);
    IPage<SysUser> pageModel = sysUserService.queryUserPage(pageParam, sysUserQueryVo);
    return Result.ok(pageModel);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.system.mapper.SysUserMapper">
    <!-- IPage<SysUser> queryUserPage(Page pageParam, SysUserQueryVo sysUserQueryVo); -->
    <resultMap id="sysUserMap" type="com.example.model.system.SysUser" autoMapping="true"></resultMap>
    <sql id="columns">
    id ,
    username ,
    password ,
    name ,
    phone ,
    head_url ,
    dept_id ,
    post_id ,
    description ,
    status ,
    create_time ,
    update_time ,
    is_deleted
    </sql>
    <select id="queryUserPage" resultMap="sysUserMap">
    select
    <include refid="columns"/>
    from sys_user
    <where>
    <if test="vo.keyword != null and vo.keyword != ''">
    and (username like CONCAT('%',#{vo.keyword},'%') or name like CONCAT('%',#{vo.keyword},'%') or phone
    like CONCAT('%',#{vo.keyword},'%'))
    </if>
    <if test="vo.createTimeBegin != null and vo.createTimeBegin != ''">
    and create_time >= #{vo.createTimeBegin}
    </if>
    <if test="vo.createTimeEnd != null and vo.createTimeEnd != ''">
    and create_time &lt;= #{vo.createTimeEnd}
    </if>
    and is_deleted = 0
    </where>
    order by id desc
    </select>
    </mapper>

  • 用户添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 用户添加
    @PostMapping("/save")
    @ApiOperation(value = "添加用户")
    public Result save(@RequestBody SysUser sysUser) {
    boolean isSuccess = sysUserService.save(sysUser);
    if (isSuccess) {
    return Result.ok();
    } else {
    return Result.fail();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    save(role) {
    return request({
    url: `${api_name}/save`,
    method: "post",
    data: role,
    });
    },

  • 用户修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 用户修改
    // 1. 1 根据 id 查询
    @GetMapping("/get/{id}")
    @ApiOperation(value = "根据id查询用户")
    public Result getUserById(@PathVariable Long id) {
    SysUser sysUser = sysUserService.getById(id);
    return Result.ok(sysUser);
    }

    // 1.2 更新
    @PutMapping("/update")
    @ApiOperation(value = "更新用户")
    public Result update(@RequestBody SysUser sysUser) {
    boolean isSuccess = sysUserService.updateById(sysUser);
    if (isSuccess) {
    return Result.ok();
    } else {
    return Result.fail();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    getById(id) {
    return request({
    url: `${api_name}/get/${id}`,
    method: "get",
    });
    },
    update(role) {
    return request({
    url: `${api_name}/update`,
    method: "put",
    data: role,
    });
    },
  • 用户删除

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 用户删除
    @DeleteMapping("/remove/{id}")
    @ApiOperation(value = "删除用户")
    public Result removeById(@PathVariable Long id) {
    boolean isSuccess = sysUserService.removeById(id);
    if (isSuccess) {
    return Result.ok();
    } else {
    return Result.fail();
    }
    }

    1
    2
    3
    4
    5
    removeById(id) {
    return request({
    url: `${api_name}/remove/${id}`,
    method: "delete",
    });
  • 为用户分配角色

    一个用户可以有多个角色

    一个角色可以有多个用户

    关系描述
    • 接口分析

      1. 进入分配页面: 获取已分配角色与全部角色,进行页面展示
        • 查询用户已经分配的角色和全部角色,页面显示
      2. 保存分配角色: 删除之前分配的角色和保存现在分配的角色
        • 删除之前已经分配的角色
        • 保存新分配的角色
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // TODO: 2022-12-15 16:54 添加角色分配接口
      @ApiOperation("根据用户获取角色数据")
      @GetMapping("/toAssign/{userId}")
      public Result toAssign(@PathVariable String userId) {
      Map<String, Object> roleMap = sysRoleService.getRolesByUserId(userId);
      return Result.ok(roleMap);
      }

      // TODO: 用户重新分配角色
      @ApiOperation("用户分配(重新)角色")
      @PostMapping("/doAssign")
      public Result doAssign(@RequestBody AssginRoleVo assginRoleVo) {
      // 给用户分配角色
      sysRoleService.doAssign(assginRoleVo);
      return Result.ok();
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      package com.example.system.service.impl;



      @Service
      public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> implements SysRoleService {
      @Autowired
      private SysUserRoleMapper sysUserRoleMapper;

      // 条件分页查询
      @Override
      public IPage<SysRole> queryPage(Page<SysRole> pageparam, SysRoleQueryVo sysRoleQueryVo) {
      IPage<SysRole> pageModel = baseMapper.queryPage(pageparam, sysRoleQueryVo);
      return pageModel;
      }

      // TODO: 难点一
      @Override
      public Map<String, Object> getRolesByUserId(String userId) {
      // 获取所有角色
      List<SysRole> roleList = baseMapper.selectList(null);
      // 根据用户 id 查询,也就是查询用户已经分配的角色
      QueryWrapper<SysUserRole> wrapper = new QueryWrapper<>();
      wrapper.eq("user_id", userId);
      List<SysUserRole> userRoles = sysUserRoleMapper.selectList(wrapper);
      // 从 userRoles 集合中获取所有角色 id
      List<String> userRoleIds = new ArrayList<>();
      userRoles.forEach(userRole -> {
      String roleId = userRole.getRoleId();
      userRoleIds.add(roleId);
      });
      // 封装到 map 集合
      Map<String, Object> returnMap = new HashMap<>();
      // 所有角色
      returnMap.put("allRoles", roleList);
      // 用户分配角色 id 集合
      returnMap.put("userRoleIds", userRoleIds);
      return returnMap;
      }

      // 给用户分配角色
      @Override
      public void doAssign(AssginRoleVo assginRoleVo) {
      // 根据用户id删除之前分配角色
      QueryWrapper<SysUserRole> wrapper = new QueryWrapper<>();
      wrapper.eq("user_id", assginRoleVo.getUserId());
      sysUserRoleMapper.delete(wrapper);
      // 获取所有角色 id,添加角色用户关系表
      // 角色 id 列表
      List<String> roleIdList = assginRoleVo.getRoleIdList();
      for (String roleId : roleIdList) {
      SysUserRole sysUserRole = new SysUserRole();
      sysUserRole.setUserId(assginRoleVo.getUserId());
      sysUserRole.setRoleId(roleId);
      sysUserRoleMapper.insert(sysUserRole);
      }
      }
      }
      分配角色
菜单管理模块
  • 接口实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    @ApiOperation("菜单列表(树形显示)")
    @GetMapping("findNodes")
    public Result findNodes() {
    List<SysMenu> list = sysMenuService.findNodes();
    return Result.ok(list);
    }

    @ApiOperation("删除菜单")
    @DeleteMapping("/remove/{id}")
    public Result remove(@PathVariable String id) {
    sysMenuService.removeMenuById(id);
    return Result.ok();
    }

    // 添加菜单
    @ApiOperation("添加菜单")
    @PostMapping("save")
    public Result save(@RequestBody SysMenu sysMenu) {
    boolean isSuccess = sysMenuService.save(sysMenu);
    if (isSuccess) {
    return Result.ok().message("添加成功");
    }
    return Result.fail().message("添加失败");
    }

    // 根据 id 查询
    // 添加菜单
    @ApiOperation("根据 id 查询菜单")
    @GetMapping("findNode/{id}")
    public Result save(@PathVariable String id) {
    SysMenu sysMenu = sysMenuService.getById(id);
    return Result.ok(sysMenu);
    }

    // 修改
    @ApiOperation("修改菜单")
    @PostMapping("/update")
    public Result update(@RequestBody SysMenu sysMenu) {
    boolean isSuccess = sysMenuService.updateById(sysMenu);
    if (isSuccess) {
    return Result.ok().message("更新成功");
    }
    return Result.fail().message("更新失败");
    }

    @ApiOperation("删除菜单")
    @DeleteMapping("/remove/{id}")
    public Result remove(@PathVariable String id) {
    boolean isSuccess = sysMenuService.removeById(id);
    if (isSuccess) {
    return Result.ok().message("删除成功");
    }
    return Result.fail().message("删除失败");
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    package com.example.system.service.impl;


    /**
    * <p>
    * 菜单表 服务实现类
    * </p>
    *
    * @author coder-itl
    * @since 2022-12-17
    */
    @Service
    public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
    /**
    * 菜单列表,树形显示
    *
    * @return
    */
    @Override
    public List<SysMenu> findNodes() {
    // 获取所有菜单
    List<SysMenu> sysMenuList = baseMapper.selectList(null);
    // 所有菜单数据转换为要求的数据格式
    List<SysMenu> resultList = MenuHelper.buildTree(sysMenuList);
    return resultList;
    }

    /**
    * 删除菜单
    *
    * @param id
    */
    @Override
    public void removeMenuById(String id) {
    // 查询当前删除菜单下面是否有子菜单
    // 根据 id==parentId
    QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
    wrapper.eq("parent_id", id);
    Integer count = baseMapper.selectCount(wrapper);
    if (count > 0) {
    // 子菜单存在
    throw new coderItlException(201, "请先删除子菜单");
    }
    // 调用删除
    baseMapper.deleteById(id);
    }
    }

  • 前端路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    name: "sysMenu",
    path: "sysMenu",
    component: () => import("@/views/system/sysMenu/list"),
    meta: {
    title: "菜单管理",
    icon: "el-icon-s-unfold",
    },
    },
    {
    path: "assignAuth",
    component: () => import("@/views/system/sysRole/assignAuth"),
    meta: {
    activeMenu: "/system/sysRole",
    title: "角色授权",
    },
    hidden: true,
    },
  • 工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    package com.example.system.utils;

    import com.example.model.system.SysMenu;

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

    public class MenuHelper {
    // 构建树形结构
    public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
    // 创建集合封装最终数据
    List<SysMenu> trees = new ArrayList<>();
    // 遍历所有菜单集合
    for (SysMenu sysMenu : sysMenuList) {
    // 找到递归入口
    if (sysMenu.getParentId().longValue() == 0) {
    trees.add(findChildren(sysMenu, sysMenuList));
    }
    }
    return trees;
    }

    /**
    * 从根节点进行递归查询,查询是否存在子节点
    * 判断 id == parentId 是否相同,如果相同是子节点,进行数据封装
    *
    * @param sysMenu
    * @param treeNodes
    * @return
    */
    private static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> treeNodes) {
    // 数据初始化
    sysMenu.setChildren(new ArrayList<SysMenu>());
    // 遍历递归查找
    for (SysMenu it : treeNodes) {
    // 获取当前菜单 id
    // String id = sysMenu.getId();
    // 数据类型转换处理
    // long lid = Long.parseLong(id);
    // 获取所有菜单的 parentId
    // Long parentId = it.getParentId();
    // 比对
    if (Long.parseLong(sysMenu.getId()) == it.getParentId()) {
    if (sysMenu.getChildren() == null) {
    sysMenu.setChildren(new ArrayList<SysMenu>());
    }
    // 递归查找
    sysMenu.getChildren().add(findChildren(it, treeNodes));
    }
    }
    return sysMenu;
    }
    }

  • 前端接口定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // sysMenu.js
    import request from '@/utils/request'

    /*
    菜单管理相关的API请求函数
    */
    const api_name = '/admin/system/sysMenu'

    export default {

    /*
    获取权限(菜单/功能)列表
    */
    findNodes() {
    return request({
    url: `${api_name}/findNodes`,
    method: 'get'
    })
    },

    /*
    删除
    */
    removeById(id) {
    return request({
    url: `${api_name}/remove/${id}`,
    method: "delete"
    })
    },

    /*
    保存
    */
    save(sysMenu) {
    return request({
    url: `${api_name}/save`,
    method: "post",
    data: sysMenu
    })
    },

    /*
    更新
    */
    updateById(sysMenu) {
    return request({
    url: `${api_name}/update`,
    method: "put",
    data: sysMenu
    })
    }
    }
  • 渲染

    页面渲染
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    <template>
    <div class="app-container">
    <!-- 工具条 -->
    <div class="tools-div">
    <el-button type="success" icon="el-icon-plus" size="mini" @click="add()"
    >添 加
    >
    </div>
    <el-table
    :data="sysMenuList"
    style="width: 100%; margin-bottom: 20px; margin-top: 10px"
    row-key="id"
    border
    :default-expand-all="false"
    :tree-props="{ children: 'children' }"
    >
    <el-table-column prop="name" label="菜单名称" width="160" />
    <el-table-column label="图标">
    <template slot-scope="scope">
    <i :class="scope.row.icon"></i>
    </template>
    </el-table-column>
    <el-table-column prop="perms" label="权限标识" width="160" />
    <el-table-column prop="path" label="路由地址" width="120" />
    <el-table-column prop="component" label="组件路径" width="160" />
    <el-table-column prop="sortValue" label="排序" width="60" />
    <el-table-column label="状态" width="80">
    <template slot-scope="scope">
    <el-switch v-model="scope.row.status === 1" disabled="true">
    </el-switch>
    </template>
    </el-table-column>
    <el-table-column prop="createTime" label="创建时间" width="160" />
    <el-table-column label="操作" width="180" align="center" fixed="right">
    <template slot-scope="scope">
    <el-button
    type="success"
    v-if="scope.row.type !== 2"
    icon="el-icon-plus"
    size="mini"
    @click="add(scope.row)"
    title="添加下级节点"
    />
    <el-button
    type="primary"
    icon="el-icon-edit"
    size="mini"
    @click="edit(scope.row)"
    title="修改"
    />
    <el-button
    type="danger"
    icon="el-icon-delete"
    size="mini"
    @click="removeDataById(scope.row.id)"
    title="删除"
    :disabled="scope.row.children.length > 0"
    />
    </template>
    </el-table-column>
    </el-table>

    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="40%">
    <el-form
    ref="dataForm"
    :model="sysMenu"
    label-width="150px"
    size="small"
    style="padding-right: 40px"
    >
    <el-form-item label="上级部门" v-if="sysMenu.id === ''">
    <el-input v-model="sysMenu.parentName" disabled="true" />
    </el-form-item>
    <el-form-item label="菜单类型" prop="type">
    <el-radio-group v-model="sysMenu.type" :disabled="typeDisabled">
    <el-radio :label="0" :disabled="type0Disabled">目录</el-radio>
    <el-radio :label="1" :disabled="type1Disabled">菜单</el-radio>
    <el-radio :label="2" :disabled="type2Disabled">按钮</el-radio>
    </el-radio-group>
    </el-form-item>
    <el-form-item label="菜单名称" prop="name">
    <el-input v-model="sysMenu.name" />
    </el-form-item>
    <el-form-item label="图标" prop="icon" v-if="sysMenu.type !== 2">
    <el-select v-model="sysMenu.icon" clearable>
    <el-option
    v-for="item in iconList"
    :key="item.class"
    :label="item.class"
    :value="item.class"
    >
    <span style="float: left">
    <i :class="item.class"></i>
    <!-- 如果动态显示图标,这里添加判断 -->
    </span>
    <span style="padding-left: 6px">{{ item.class }}</span>
    </el-option>
    </el-select>
    </el-form-item>
    <el-form-item label="排序">
    <el-input-number
    v-model="sysMenu.sortValue"
    controls-position="right"
    :min="0"
    />
    </el-form-item>
    <el-form-item prop="path">
    <span slot="label">
    <el-tooltip content="访问的路由地址,如:`sysUser`" placement="top">
    <i class="el-icon-question"></i>
    </el-tooltip>
    路由地址
    </span>
    <el-input v-model="sysMenu.path" placeholder="请输入路由地址" />
    </el-form-item>
    <el-form-item prop="component" v-if="sysMenu.type !== 0">
    <span slot="label">
    <el-tooltip
    content="访问的组件路径,如:`system/user/index`,默认在`views`目录下"
    placement="top"
    >
    <i class="el-icon-question"></i>
    </el-tooltip>
    组件路径
    </span>
    <el-input v-model="sysMenu.component" placeholder="请输入组件路径" />
    </el-form-item>
    <el-form-item v-if="sysMenu.type === 2">
    <el-input
    v-model="sysMenu.perms"
    placeholder="请输入权限标识"
    maxlength="100"
    />
    <span slot="label">
    <el-tooltip
    content="控制器中定义的权限字符,如:@PreAuthorize(hasAuthority('bnt.sysRole.list'))"
    placement="top"
    >
    <i class="el-icon-question"></i>
    </el-tooltip>
    权限字符
    </span>
    </el-form-item>
    <el-form-item label="状态" prop="type">
    <el-radio-group v-model="sysMenu.status">
    <el-radio :label="1">正常</el-radio>
    <el-radio :label="0">停用</el-radio>
    </el-radio-group>
    </el-form-item>
    </el-form>
    <span slot="footer" class="dialog-footer">
    <el-button
    @click="dialogVisible = false"
    size="small"
    icon="el-icon-refresh-right"
    >取 消
    >
    <el-button
    type="primary"
    icon="el-icon-check"
    @click="saveOrUpdate()"
    size="small"
    >确 定
    >
    </span>
    </el-dialog>
    </div>
    </template>


    <script>
    import api from '@/api/system/sysMenu'
    const defaultForm = {
    id: '',
    parentId: '',
    name: '',
    type: 0,
    path: '',
    component: '',
    perms: '',
    icon: '',
    sortValue: 1,
    status: 1,
    }
    export default {
    // 定义数据
    data() {
    return {
    sysMenuList: [],
    expandKeys: [], // 需要自动展开的项

    typeDisabled: false,
    type0Disabled: false,
    type1Disabled: false,
    type2Disabled: false,
    dialogTitle: '',

    dialogVisible: false,
    sysMenu: defaultForm,
    saveBtnDisabled: false,

    iconList: [
    {
    class: 'el-icon-s-tools',
    },
    {
    class: 'el-icon-s-custom',
    },
    {
    class: 'el-icon-setting',
    },
    {
    class: 'el-icon-user-solid',
    },
    {
    class: 'el-icon-s-help',
    },
    {
    class: 'el-icon-phone',
    },
    {
    class: 'el-icon-s-unfold',
    },
    {
    class: 'el-icon-s-operation',
    },
    {
    class: 'el-icon-more-outline',
    },
    {
    class: 'el-icon-s-check',
    },
    {
    class: 'el-icon-tickets',
    },
    {
    class: 'el-icon-s-goods',
    },
    {
    class: 'el-icon-document-remove',
    },
    {
    class: 'el-icon-warning',
    },
    {
    class: 'el-icon-warning-outline',
    },
    {
    class: 'el-icon-question',
    },
    {
    class: 'el-icon-info',
    },
    ],
    }
    },

    //当页面加载时获取数据
    created() {
    this.fetchData()
    },

    methods: {
    //调用api层获取数据库中的数据
    fetchData() {
    console.log('加载列表')
    api.findNodes().then((response) => {
    this.sysMenuList = response.data
    console.log(this.sysMenuList)
    })
    },

    //根据id删除数据
    removeDataById(id) {
    // debugger
    this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning',
    })
    .then(() => {
    // promise
    // 点击确定,远程调用ajax
    return api.removeById(id)
    })
    .then((response) => {
    this.fetchData()
    this.$message({
    type: 'success',
    message: response.message,
    })
    })
    .catch(() => {
    this.$message.info('取消删除')
    })
    },

    //弹出添加或更新的表单
    add(row) {
    debugger
    this.typeDisabled = false
    this.dialogTitle = '添加下级节点'
    this.dialogVisible = true

    this.sysMenu = Object.assign({}, defaultForm)
    this.sysMenu.id = ''
    if (row) {
    this.sysMenu.parentId = row.id
    this.sysMenu.parentName = row.name
    //this.sysMenu.component = 'ParentView'
    if (row.type === 0) {
    this.sysMenu.type = 1
    this.typeDisabled = false
    this.type0Disabled = false
    this.type1Disabled = false
    this.type2Disabled = true
    } else if (row.type === 1) {
    this.sysMenu.type = 2
    this.typeDisabled = true
    }
    } else {
    this.dialogTitle = '添加目录节点'
    this.sysMenu.type = 0
    this.sysMenu.component = 'Layout'
    this.sysMenu.parentId = 0
    this.typeDisabled = true
    }
    },

    //编辑
    edit(row) {
    debugger
    this.dialogTitle = '修改菜单'
    this.dialogVisible = true

    this.sysMenu = Object.assign({}, row)
    this.typeDisabled = true
    },

    //添加或更新
    saveOrUpdate() {
    if (this.sysMenu.type === 0 && this.sysMenu.parentId !== 0) {
    this.sysMenu.component = 'ParentView'
    }
    this.$refs.dataForm.validate((valid) => {
    if (valid) {
    this.saveBtnDisabled = true // 防止表单重复提交
    if (!this.sysMenu.id) {
    this.save()
    } else {
    this.update()
    }
    }
    })
    },

    //添加
    save() {
    api.save(this.sysMenu).then((response) => {
    this.$message.success(response.message || '操作成功')
    this.dialogVisible = false
    this.fetchData(this.page)
    })
    },

    //更新
    update() {
    api.updateById(this.sysMenu).then((response) => {
    this.$message.success(response.message || '操作成功')
    this.dialogVisible = false
    this.fetchData()
    })
    },
    },
    }
    </script>
  • 给角色分配权限

    • 接口分析
      1. 进入分配页面: 获取全部菜单的以及菜单按钮,选中已选复选框,进行页面展示
      2. 保存分配权限: 删除之前分配的权限和保存现在分配的权限
权限管理
  • 菜单权限

    菜单权限就是对页面的控制,就是有这个权限的用户才能访问这个页面,没这个权限的用户就无法访问,它是以整个页面为维度,对权限的控制并没有那么细,所以是一种粗颗粒权限

  • 权限管理设计思路

    在用户管理、角色管理以及菜单管理,我们把菜单权限分配给角色,把角色分配给用户,那么用户就拥有了角色的所有权限(权限包括: 菜单权限和按钮权限)

    用户所拥有的角色 角色所拥有的权限
  • 用户接口修改密码加密

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 用户添加
    @PostMapping("/save")
    @ApiOperation(value = "添加用户")
    public Result save(@RequestBody SysUser sysUser) {
    // TODO: 将用户输入的密码进行 MD5 加密
    String encrypt = MD5.encrypt(sysUser.getPassword());
    sysUser.setPassword(encrypt);
    boolean isSuccess = sysUserService.save(sysUser);
    if (isSuccess) {
    return Result.ok();
    } else {
    return Result.fail();
    }
    }
  • 修改用户登录接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @ApiOperation("用户登录")
    @PostMapping("/login")
    public Result login(@RequestBody LoginVo loginVo) {
    // 根据 username 查询数据
    SysUser sysUser = sysUserService.getUserInfobyUserName(loginVo.getUsername());
    // 如果查询为空
    if (sysUser == null) {
    throw new coderItlException(20001, "用户不存在");
    }
    // 判断密码是否一致
    String password = loginVo.getPassword();
    String md5Password = MD5.encrypt(password);
    if (!sysUser.getPassword().equals(md5Password)) {
    throw new coderItlException(20001, "密码不正确!");
    }
    // 判断用户是否可用
    if (sysUser.getStatus().intValue() == 0) {
    throw new coderItlException(20001, "用户已禁用!");
    }
    // 根据 userid 和 username 生成 token 字符串,返回 map
    String token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
    Map<String, Object> map = new HashMap<>();
    map.put("token", token);
    return Result.ok(map);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    // dao 根据用户名查询
    @Override
    public SysUser getUserInfobyUserName(String username) {
    QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
    wrapper.eq("username", username);
    SysUser sysUser = sysUserMapper.selectOne(wrapper);
    return sysUser;
    }
  • 修改用户信息接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    @ApiOperation("用户信息")
    @GetMapping("/info")
    public Result info(HttpServletRequest request) {
    // 获取请求头中的 token 字符串
    String token = request.getHeader("token");
    // 从 token 中获取(用户名称/id)
    String username = JwtHelper.getUsername(token);
    // 根据用户名称获取用户信息(基本信息和菜单权限和按钮权限数据)
    Map<String, Object> map = sysUserService.getUserInfo(username);
    return Result.ok(map);
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // dao
    @Override
    public Map<String, Object> getUserInfo(String username) {
    // 根据用户名查询用户基本信息
    SysUser sysUser = this.getUserInfobyUserName(username);
    // 根据 userid查询菜单权限值
    List<RouterVo> routerVoList = sysMenuService.getUserMenuList(sysUser.getId());
    // 根据 userid 查询按钮权限值
    List<String> permsList = sysMenuService.getUserButtonList(sysUser.getId());
    Map<String, Object> map = new HashMap<>();
    map.put("roles", "[admin]");
    map.put("name", "admin");
    map.put("avatar", "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
    map.put("buttons", permsList);
    map.put("routers", routerVoList);
    return map;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 根据 userid 查询菜单权限值
    @Override
    public List<RouterVo> getUserMenuList(String userId) {
    // admin 是超级管理员,可以操作所有内容
    List<SysMenu> sysMenuList = null;
    // 判断 userid 的值是 1(代表超级管理员),查询所有权限数据
    if ("1".equals((userId))) {
    QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
    wrapper.eq("status", 1);
    wrapper.orderByAsc("sort_value");
    sysMenuList = baseMapper.selectList(wrapper);
    } else {
    // 如果 userid 不是 1,是其他类型用户,查询这个用户的权限
    sysMenuList = baseMapper.findMenuListUserId(userId);
    }
    // 构建树形结构
    List<SysMenu> sysMenuTreeList = MenuHelper.buildTree(sysMenuList);
    // 转换成前端路由要求的格式数据
    List<RouterVo> routerVoList = RouterHelper.buildRouters(sysMenuTreeList);
    return routerVoList;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 根据 userid 查询按钮权限值
    @Override
    public List<String> getUserButtonList(String userId) {
    List<SysMenu> sysMenuList = null;
    // 判断是否是管理员
    if ("1".equals(userId)) {
    sysMenuList = baseMapper.selectList(new QueryWrapper<SysMenu>().eq("status", 1));
    } else {
    sysMenuList = baseMapper.findMenuListUserId(userId);
    }
    List<String> permissionList = new ArrayList<>();
    for (SysMenu sysMenu : sysMenuList) {
    if (sysMenu.getType().intValue() == 2) {
    String perms = sysMenu.getPerms();
    permissionList.add(perms);
    }
    }
    // 返回按钮的权限
    return permissionList;
    }
  • 工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    // 构建动态路由
    package com.example.system.utils;


    import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
    import com.example.model.system.SysMenu;
    import com.example.model.vo.MetaVo;
    import com.example.model.vo.RouterVo;
    import org.springframework.util.StringUtils;

    import java.util.LinkedList;
    import java.util.List;
    import java.util.stream.Collectors;

    /**
    * 根据菜单数据构建路由的工具类
    */
    public class RouterHelper {

    /**
    * 根据菜单构建路由
    *
    * @param menus
    * @return
    */
    public static List<RouterVo> buildRouters(List<SysMenu> menus) {
    List<RouterVo> routers = new LinkedList<RouterVo>();
    for (SysMenu menu : menus) {
    RouterVo router = new RouterVo();
    router.setHidden(false);
    router.setAlwaysShow(false);
    router.setPath(getRouterPath(menu));
    router.setComponent(menu.getComponent());
    router.setMeta(new MetaVo(menu.getName(), menu.getIcon()));
    List<SysMenu> children = menu.getChildren();
    //如果当前是菜单,需将按钮对应的路由加载出来,如:“角色授权”按钮对应的路由在“系统管理”下面
    if (menu.getType().intValue() == 1) {
    List<SysMenu> hiddenMenuList = children.stream().filter(item -> !StringUtils.isEmpty(item.getComponent())).collect(Collectors.toList());
    for (SysMenu hiddenMenu : hiddenMenuList) {
    RouterVo hiddenRouter = new RouterVo();
    hiddenRouter.setHidden(true);
    hiddenRouter.setAlwaysShow(false);
    hiddenRouter.setPath(getRouterPath(hiddenMenu));
    hiddenRouter.setComponent(hiddenMenu.getComponent());
    hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
    routers.add(hiddenRouter);
    }
    } else {
    if (!CollectionUtils.isEmpty(children)) {
    if (children.size() > 0) {
    router.setAlwaysShow(true);
    }
    router.setChildren(buildRouters(children));
    }
    }
    routers.add(router);
    }
    return routers;
    }

    /**
    * 获取路由地址
    *
    * @param menu 菜单信息
    * @return 路由地址
    */
    public static String getRouterPath(SysMenu menu) {
    String routerPath = "/" + menu.getPath();
    if (menu.getParentId().intValue() != 0) {
    routerPath = menu.getPath();
    }
    return routerPath;
    }
    }
  • 权限测试

    • 创建新用户,分配权限与角色

      添加新用户,分配权限 对角色分配权限
    • 修改前端页面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!-- 工具条 -->
      <div class="tools-div">
      <el-button
      type="success"
      icon="el-icon-plus"
      size="mini"
      @click="add"
      :disabled="$hasBP('bnt.sysUser.add') === false"
      >添 加
      >
      </div>

      <!-- :disabled="$hasBP('bnt.sysUser.add') === false" -->

    • 登录查看

      实现客户端按钮的权限控制
  • 前端权限对接

    • 修改axios 对请求头token 的设置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      service.interceptors.request.use(
      (config) => {
      // do something before request is sent

      if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers["token"] = getToken();
      }
      return config;
      },

SpringSecurity

  • 创建项目

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>
    </dependency>
  • 用户认证流程

    用户认证流程
  • 用户认证

    • 自定义密码组件(采用 MD5)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      package com.example.system.customer;

      import com.example.common.utils.MD5;
      import org.springframework.security.crypto.password.PasswordEncoder;
      import org.springframework.stereotype.Component;

      // 自定义密码组件
      @Component
      public class CustomerMD5Paddword implements PasswordEncoder {
      @Override
      public String encode(CharSequence charSequence) {
      // MD5 是自定义的工具类
      return MD5.encrypt(charSequence.toString());
      }

      @Override
      public boolean matches(CharSequence charSequence, String encodedPassword) {
      return encodedPassword.equals(MD5.encrypt(charSequence.toString()));
      }
      }

    • 自定义用户对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54

      @Data
      @ApiModel(description = "用户")
      @TableName("sys_user")
      public class SysUser extends BaseEntity {

      private static final long serialVersionUID = 1L;

      @ApiModelProperty(value = "用户名")
      @TableField("username")
      private String username;

      @ApiModelProperty(value = "密码")
      @TableField("password")
      private String password;

      @ApiModelProperty(value = "姓名")
      @TableField("name")
      private String name;

      @ApiModelProperty(value = "手机")
      @TableField("phone")
      private String phone;

      @ApiModelProperty(value = "头像地址")
      @TableField("head_url")
      private String headUrl;

      @ApiModelProperty(value = "部门id")
      @TableField("dept_id")
      private Long deptId;

      @ApiModelProperty(value = "岗位id")
      @TableField("post_id")
      private Long postId;

      @ApiModelProperty(value = "描述")
      @TableField("description")
      private String description;

      @ApiModelProperty(value = "状态(1:正常 0:停用)")
      @TableField("status")
      private Integer status;

      @TableField(exist = false)
      private List<SysRole> roleList;
      //岗位
      @TableField(exist = false)
      private String postName;
      //部门
      @TableField(exist = false)
      private String deptName;
      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      package com.example.system.customer;

      import com.example.model.system.SysUser;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.userdetails.User;

      import java.util.Collection;

      public class CustomerUser extends User {

      /**
      * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象
      */
      private SysUser sysUser;

      public CustomerUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
      super(sysUser.getUsername(), sysUser.getPassword(), authorities);
      this.sysUser = sysUser;
      }

      public SysUser getSysUser() {
      return sysUser;
      }

      public void setSysUser(SysUser sysUser) {
      this.sysUser = sysUser;
      }
      }

    • 创建方法根据用户名查询用户信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      package com.example.system.service.impl;

      import com.example.model.system.SysUser;
      import com.example.system.customer.CustomerUser;
      import com.example.system.service.SysUserService;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Service;

      import java.util.Collections;

      @Service
      public class UserDetailServiceImpl implements UserDetailsService {
      @Autowired
      private SysUserService sysUserService;

      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      SysUser sysUser = sysUserService.getUserInfobyUserName(username);
      if (sysUser == null) {
      // UsernameNotFoundException: 是 security 中提供的
      throw new UsernameNotFoundException("用户不存在");
      }
      if (sysUser.getStatus().intValue() == 0) {
      throw new RuntimeException("用户已禁用!");
      }
      return new CustomerUser(sysUser, Collections.emptyList());
      }
      }

    • 自定义认证过滤器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      package com.example.system.filter;

      import com.example.common.result.Result;
      import com.example.common.result.ResultCodeEnum;
      import com.example.common.utils.JwtHelper;
      import com.example.model.vo.LoginVo;
      import com.example.system.customer.CustomerUser;
      import com.example.system.utils.ResponseUtil;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.Authentication;
      import org.springframework.security.core.AuthenticationException;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

      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.HashMap;
      import java.util.Map;

      /**
      * 登录过滤器,继承 UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
      */
      public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

      public TokenLoginFilter(AuthenticationManager authenticationManager) {
      this.setAuthenticationManager(authenticationManager);
      this.setPostOnly(false);
      // 指定登录接口及提交方式,可以指定任意路径
      this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login", "POST"));
      }

      /**
      * 登录认证
      *
      * @param req
      * @param res
      * @return
      * @throws AuthenticationException
      */
      @Override
      public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
      throws AuthenticationException {
      try {
      LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

      Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
      return this.getAuthenticationManager().authenticate(authenticationToken);
      } catch (IOException e) {
      throw new RuntimeException(e);
      }

      }

      /**
      * 登录成功
      *
      * @param request
      * @param response
      * @param chain
      * @param auth
      * @throws IOException
      * @throws ServletException
      */
      @Override
      protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
      Authentication auth) throws IOException, ServletException {
      CustomerUser customUser = (CustomerUser) auth.getPrincipal();
      String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

      Map<String, Object> map = new HashMap<>();
      map.put("token", token);
      ResponseUtil.out(response, Result.ok(map));
      }

      /**
      * 登录失败
      *
      * @param request
      * @param response
      * @param e
      * @throws IOException
      * @throws ServletException
      */
      @Override
      protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException e) throws IOException, ServletException {

      if (e.getCause() instanceof RuntimeException) {
      ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
      } else {
      ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
      }
      }
      }
    • 返回信息工具类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      // 响应工具类
      package com.example.system.utils;


      import com.example.common.result.Result;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import org.springframework.http.HttpStatus;
      import org.springframework.http.MediaType;

      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;

      public class ResponseUtil {

      public static void out(HttpServletResponse response, Result r) {
      ObjectMapper mapper = new ObjectMapper();
      response.setStatus(HttpStatus.OK.value());
      response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
      try {
      mapper.writeValue(response.getWriter(), r);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      }
    • 认证解析过滤器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      package com.example.system.filter;

      import com.example.common.result.Result;
      import com.example.common.result.ResultCodeEnum;
      import com.example.common.utils.JwtHelper;
      import com.example.system.utils.ResponseUtil;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
      import org.springframework.security.core.context.SecurityContextHolder;
      import org.springframework.util.StringUtils;
      import org.springframework.web.filter.OncePerRequestFilter;

      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.Collections;

      /**
      * 认证解析 token 过滤器
      */
      @Slf4j
      public class TokenAuthenticationFilter extends OncePerRequestFilter {

      public TokenAuthenticationFilter() {
      }

      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
      log.info("uri:" + request.getRequestURI());
      // 如果是登录接口,直接放行
      if ("/admin/system/index/login".equals(request.getRequestURI())) {
      chain.doFilter(request, response);
      return;
      }

      UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
      if (null != authentication) {
      SecurityContextHolder.getContext().setAuthentication(authentication);
      chain.doFilter(request, response);
      } else {
      ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
      }
      }

      private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
      // token 置于 header 里
      String token = request.getHeader("token");
      logger.info("token:" + token);
      if (!StringUtils.isEmpty(token)) {
      String useruame = JwtHelper.getUsername(token);
      log.info("useruame:" + useruame);
      if (!StringUtils.isEmpty(useruame)) {
      return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
      }
      }
      return null;
      }
      }
    • 配置用户认证全局信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      package com.example.system.config;

      import com.example.system.customer.CustomMd5PasswordEncoder;
      import com.example.system.filter.TokenAuthenticationFilter;
      import com.example.system.filter.TokenLoginFilter;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.context.annotation.Bean;
      import org.springframework.security.authentication.AuthenticationManager;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.builders.WebSecurity;
      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 org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

      @EnableWebSecurity
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

      @Autowired
      private UserDetailsService userDetailsService;

      @Autowired
      private CustomMd5PasswordEncoder customMd5PasswordEncoder;


      @Bean
      @Override
      protected AuthenticationManager authenticationManager() throws Exception {
      return super.authenticationManager();
      }

      @Override
      protected void configure(HttpSecurity http) throws Exception {
      // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
      http
      //关闭csrf
      .csrf().disable()
      // 开启跨域以便前端调用接口
      .cors().and()
      .authorizeRequests()
      // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
      .antMatchers("/admin/system/index/login").permitAll()
      // 这里意思是其它所有接口需要认证才能访问
      .anyRequest().authenticated()
      .and()
      //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
      .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
      .addFilter(new TokenLoginFilter(authenticationManager()));

      //禁用session
      http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
      }

      @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      // 指定UserDetailService和加密器
      auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
      }

      /**
      * 配置哪些请求不拦截
      * 排除swagger相关请求
      *
      * @param web
      * @throws Exception
      */
      @Override
      public void configure(WebSecurity web) throws Exception {
      web.ignoring().antMatchers("/favicon.ico", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
      }
      }

      前后端分离项目,使用jwt 生成token ,即用户状态保存在客户端中,前后端交互通过api 接口 无session 生成,所以我们不需要配置formLoginsession 禁用

  • 用户授权

    SpringSecurity 中,会使用默认的FilterSecurityInterceptor 来进行权限校验。在FilterSecurityInterceptor 中会从SecurityContextHolder 获取其中的Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。

    • 修改loadUserByUsername,查询用户权限操作数据返回

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      SysUser sysUser = sysUserService.getUserInfobyUserName(username);
      if (sysUser == null) {
      // UsernameNotFoundException: 是 security 中提供的
      throw new UsernameNotFoundException("用户不存在");
      }
      if (sysUser.getStatus().intValue() == 0) {
      throw new RuntimeException("用户已禁用!");
      }
      // 根据 userid 查询操作权限数据
      List<String> userPermsList = sysMenuService.getUserButtonList(sysUser.getId());
      // 转换成 security 要求的格式
      List<SimpleGrantedAuthority> authorities = new ArrayList<>();
      for (String perm : userPermsList) {
      authorities.add(new SimpleGrantedAuthority(perm.trim()));
      }
      return new CustomerUser(sysUser, authorities);
      }
    • 配置Redis,存储权限数据

    • 修改过滤器

      • 认证过滤器
      • 解析过滤器
    • 在项目配置redis,完成Controller 相关代码

日志模块

  • 获取日志工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    package com.example.system.utils;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.server.reactive.ServerHttpRequest;

    import javax.servlet.http.HttpServletRequest;
    import java.net.InetAddress;
    import java.net.UnknownHostException;

    /**
    * 获取ip地址
    */
    @Slf4j
    public class IpUtil {

    public static String getIpAddress(HttpServletRequest request) {
    String ipAddress = null;
    try {
    ipAddress = request.getHeader("x-forwarded-for");
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
    ipAddress = request.getHeader("Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
    ipAddress = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
    ipAddress = request.getRemoteAddr();
    if (ipAddress.equals("127.0.0.1")) {
    // 根据网卡取本机配置的IP
    InetAddress inet = null;
    try {
    inet = InetAddress.getLocalHost();
    log.info("inet: {}", inet.toString());
    } catch (UnknownHostException e) {
    log.error("UnknownHostException: {}", e);
    }
    ipAddress = inet.getHostAddress();
    }
    }
    // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
    if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
    // = 15
    if (ipAddress.indexOf(",") > 0) {
    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
    }
    }
    } catch (Exception e) {
    ipAddress = "";
    }
    return ipAddress;
    }

    public static String getGatwayIpAddress(ServerHttpRequest request) {
    HttpHeaders headers = request.getHeaders();
    String ip = headers.getFirst("x-forwarded-for");
    if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
    // 多次反向代理后会有多个ip值,第一个ip才是真实ip
    if (ip.indexOf(",") != -1) {
    ip = ip.split(",")[0];
    }
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = headers.getFirst("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = headers.getFirst("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = headers.getFirst("HTTP_CLIENT_IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = headers.getFirst("HTTP_X_FORWARDED_FOR");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = headers.getFirst("X-Real-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
    ip = request.getRemoteAddress().getAddress().getHostAddress();
    }
    return ip;
    }
    }
  • 简单使用

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取 IPV4 地址
    try {
    InetAddress inet = InetAddress.getLocalHost();
    String ipAdddr = inet.getHostAddress();
    System.out.println(ipAdddr);
    } catch (UnknownHostException e) {
    throw new RuntimeException(e);
    }
  • 表结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    CREATE TABLE `sys_login_log`
    (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '访问ID',
    `username` varchar(50) DEFAULT '' COMMENT '用户账号',
    `ipaddr` varchar(128) DEFAULT '' COMMENT '登录IP地址',
    `status` tinyint(1) DEFAULT '0' COMMENT '登录状态(0成功 1失败)',
    `msg` varchar(255) DEFAULT '' COMMENT '提示信息',
    `access_time` datetime DEFAULT NULL COMMENT '访问时间',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
    `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:可用 1:已删除)',
    PRIMARY KEY (`id`)
    ) ENGINE = InnoDB
    AUTO_INCREMENT = 11
    DEFAULT CHARSET = utf8mb3 COMMENT ='系统访问记录'

项目部署

  • 后端打包

    • 在主启动类的项目中的pom.xml 添加build

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <build>
      <finalName>${project.artifactId}</finalName>
      <plugins>
      <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      </plugins>
      </build>
    • 删除父工程的build

    • 目录结构预览

      目录结构预览
    • 在主启动的项目中点击maven package

      打包
    • 打包成功

      打包结果