Java's SpringBoot+Vue realizes the development of background management system

To build a project skeleton from scratch, it is best to choose a suitable and familiar technology, and it is easy to expand in the future, suitable for micro-service systems, etc. Therefore, Springboot is generally used as the basis of our framework, which is inseparable.

Then for the data layer, we commonly use Mybatis, which is easy to use and easy to maintain. However, it is difficult to operate a single table, especially when adding fields or reducing fields, which is cumbersome, so here I recommend using Mybatis Plus (  mp.baomidou.com/  ), which is born to simplify development, and can be quickly and easily configured with simple configuration. Do CRUD operations, which saves a lot of time.

Spring Security, using security as our framework for permission control and session control.

  • SpringBoot
  • mybatis plus
  • spring security
  • lombok
  • redis
  • hibernate is stronger
  • jwt

2. Create a new SpringBoot project, pay attention to the version

1. Create a new SpringBoot project

Here, we use IDEA to develop our project

Development tools and environment:

idea

mysql

jdk 8

maven3.3.9

New SpringBoot

delete some content

2. Integrate MyBatis plus to generate code

(1) Introduce dependencies

<!--整合mybatis plus https://baomidou.com/-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!--mp代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

(2) Set the configuration file

server:
  port: 8081
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/zhengadminvue?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml

Create a new package: specify the package where the interface to become the implementation class is located through the @mapperScan annotation, and then all the interfaces under the package will generate the corresponding implementation class after compilation.

@Configuration
@ManagedBean("cn.itbluebox.springbootadminvue.mapper")
public class MybatisPlusConfig {
 
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
 
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        //防止全表更新插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
 
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }

}

Create the corresponding mapper file

(3) Create database and table

SQL statement

DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(64) NOT NULL,
  `path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
  `perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `component` varchar(255) DEFAULT NULL,
  `type` int(5) NOT NULL COMMENT '类型     0:目录   1:菜单   2:按钮',
  `icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
  `orderNum` int(11) DEFAULT NULL COMMENT '排序',
  `created` datetime NOT NULL,
  `updated` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(64) NOT NULL,
  `code` varchar(64) NOT NULL,
  `remark` varchar(64) DEFAULT NULL COMMENT '备注',
  `created` datetime DEFAULT NULL,
  `updated` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`) USING BTREE,
  UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL,
  `menu_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `email` varchar(64) DEFAULT NULL,
  `city` varchar(64) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `updated` datetime DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  `statu` int(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;

(4) Code generation

package cn.itbluebox.springbootadminvue;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

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

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
 
    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
 
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
 
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
 
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
 
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("itbluebox");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
//        pc.setModuleName(scanner("模块名"));
        pc.setParent("cn.itbluebox.springbootadminvue");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
 
            @Override
            public void initMap() {
 
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
//         String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
 
            @Override
            public String outputFile(TableInfo tableInfo) {
 
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setSuperEntityClass("BaseEntity");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 公共父类
        strategy.setSuperControllerClass("BaseController");
        // 写于父类中的公共字段
        strategy.setSuperEntityColumns("id", "created", "updated", "statu");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
//        strategy.setTablePrefix("sys_");//动态调整
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

1. Get information about all the tables and fields of the corresponding project

2. Create a new freemarker page template

3. Provide relevant dynamic data that needs to be rendered

# 获取表
SELECT
    *
FROM
    information_schema. TABLES
WHERE
    TABLE_SCHEMA = (SELECT DATABASE());

# 获取字段
SELECT
    *
FROM
    information_schema. COLUMNS
WHERE
    TABLE_SCHEMA = (SELECT DATABASE())
AND TABLE_NAME = "sys_user";

sys_user_role,sys_user,sys_role_menu,sys_role,sys_menu

Automatically generate code

We found that the entity class and controller error report lacks the corresponding Bese

Create BaseEntity

package cn.itbluebox.springbootadminvue.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class BaseEntity implements Serializable {
 
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private LocalDateTime created;
    private LocalDateTime updated;
    private Integer statu;
}

Pay attention to the introduction of each Controller

(5) Write test methods

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author itbluebox
 * @since 2022-05-26
 */
@RestController
@RequestMapping("/sys-user")
public class SysUserController extends BaseController {
 

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("list")
    public List<SysUser> getUserList(){
 
        List<SysUser> list = sysUserService.list(new QueryWrapper<>(null));
        return  list;
    }

}

Set the corresponding mapper scan on the startup class

@SpringBootApplication
@MapperScan("cn.itbluebox.springbootadminvue.mapper")
public class SpringbootAdminvueApplication {
 
    public static void main(String[] args) {
 
        SpringApplication.run(SpringbootAdminvueApplication.class, args);
    }
}

Startup project

access interface

http://localhost:8081/sys-user/list

Access is successful

Add some data to the database

refresh page

3. Result encapsulation

因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。

这里我们用到了一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

  • 是否成功,可用code表示(如200表示成功,400表示异常)
  • 结果消息
  • 结果数据

package cn.itbluebox.springbootadminvue.common.lang;

import lombok.Data;

import java.io.Serializable;

@Data
public class Result implements Serializable {
 

    private int code;
    private String msg;
    private Object data;

    public static Result success(Object data){
 
        return success(200,"操作成功",data);
    }

    public static Result success(int code,String msg,Object data){
 
        Result r = new Result();
        r.setData(data);
        r.setMsg(msg);
        r.setCode(code);
        return r;
    }
    public static Result fail(String msg){
 
        return fail(400,msg, null);
    }

    public static Result fail(int code,String msg,Object data){
 
        Result r = new Result();
        r.setData(data);
        r.setMsg(msg);
        r.setCode(code);
        return r;
    }
}

修改SysUserController

/**
 * <p>
 *  前端控制器
 * </p>
 * @author itbluebox
 * @since 2022-05-26
 */
@RestController
@RequestMapping("/sys-user")
public class SysUserController extends BaseController {
 

    @Autowired
    private SysUserService sysUserService;

    @GetMapping("list")
    public Result getUserList(){
 
        List<SysUser> list = sysUserService.list(new QueryWrapper<>(null));
        return  Result.success(list);
    }
}

http://localhost:8081/sys-user/list

四、全局异常处理

有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。

处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,

@ExceptionHandler(value = RuntimeException.class)

来指定捕获的Exception各个类型异常,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。

步骤二、定义全局异常处理,

@ControllerAdvice

表示定义全局控制器异常处理,

@ExceptionHandler

表示针对性异常处理,可对每种异常针对性处理。

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = AccessDeniedException.class)
    public Result handler(AccessDeniedException e) {
 
        log.info("security权限不足:----------------{}", e.getMessage());
        return Result.fail("权限不足");
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) {
 
        log.info("实体校验异常:----------------{}", e.getMessage());
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(objectError.getDefaultMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) {
 
        log.error("Assert异常:----------------{}", e.getMessage());
        return Result.fail(e.getMessage());
    }

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

五、整合Spring Security

1、Spring Security介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

它提供了一组可以在Spring应用上下文中配置的Bean,

充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,

为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

流程说明:

客户端发起一个请求,进入 Security 过滤器链。

当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。如果不是登出路径则直接进入下一个过滤器。

当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。

如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。

当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。

2、引入Security与jwt

首先我们导入security包,因为我们前后端交互用户凭证用的是JWT,所以我们也导入jwt的相关包,然后因为验证码的存储需要用到redis,所以引入redis。最后为了一些工具类,我们引入hutool。

  • pom.xml

<!-- springboot security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.github.axet</groupId>
    <artifactId>kaptcha</artifactId>
    <version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.3.3</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.11</version>
</dependency>

重新启动项目

访问: http://localhost:8081

用户名:user

密码:控制台已经输出

http://localhost:8081/sys-user/list

因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:

application.yml

spring:
  security:
    user:
      name: user
      password: 111111

3、设置Redis的工具类

package cn.itbluebox.springbootadminvue.utils;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {
 

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
 
        try {
 
            if (time > 0) {
 
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
 
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
 
        try {
 
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
 
        if (key != null && key.length > 0) {
 
            if (key.length == 1) {
 
                redisTemplate.delete(key[0]);
            } else {
 
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    //============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
 
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
 
        try {
 
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
 
        try {
 
            if (time > 0) {
 
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
 
                set(key, value);
            }
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key 键
     * @param delta  要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
 
        if (delta < 0) {
 
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key 键
     * @param delta  要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
 
        if (delta < 0) {
 
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    //================================Map=================================

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    public Object hget(String key, String item) {
 
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     *
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
 
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
 
        try {
 
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
 
        try {
 
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
 
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
 
        try {
 
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
 
        try {
 
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
 
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
 
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
 
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String item, double by) {
 
        return redisTemplate.opsForHash().increment(key, item, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
 
        return redisTemplate.opsForHash().increment(key, item, -by);
    }

    //============================set=============================

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
 
        try {
 
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
 
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
 
        try {
 
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
 
        try {
 
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
 
        try {
 
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) expire(key, time);
            return count;
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
 
        try {
 
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
 
        try {
 
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }
    //===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束  0 到 -1代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
 
        try {
 
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
 
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     *
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
 
        try {
 
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
 
        try {
 
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
 
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
 
        try {
 
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
 
        try {
 
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
 
        try {
 
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
 
        try {
 
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
 
        try {
 
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
 
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
 
        try {
 
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
 
            e.printStackTrace();
            return 0;
        }
    }

    //================有序集合 sort set===================
    /**
     * 有序set添加元素
     *
     * @param key
     * @param value
     * @param score
     * @return
     */
    public boolean zSet(String key, Object value, double score) {
 
        return redisTemplate.opsForZSet().add(key, value, score);
    }

    public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
 
        return redisTemplate.opsForZSet().add(key, typles);
    }

    public void zIncrementScore(String key, Object value, long delta) {
 
        redisTemplate.opsForZSet().incrementScore(key, value, delta);
    }

    public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
 
        redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
    }

    /**
     * 获取zset数量
     * @param key
     * @param value
     * @return
     */
    public long getZsetScore(String key, Object value) {
 
        Double score = redisTemplate.opsForZSet().score(key, value);
        if(score==null){
 
            return 0;
        }else{
 
            return score.longValue();
        }
    }

    /**
     * 获取有序集 key 中成员 member 的排名 。
     * 其中有序集成员按 score 值递减 (从大到小) 排序。
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
 
        return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
    }

}

4、设置RedisConfig

package cn.itbluebox.springbootadminvue.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
 

    @Bean
    RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
 

        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
}

六、用户认证

首先我们来解决用户认证问题,分为首次登陆,和二次认证。

首次登录认证:用户名、密码和验证码完成登录

二次token认证:请求头携带Jwt进行身份认证

使用用户名密码来登录的,然后我们还想添加图片验证码,那么security给我们提供的UsernamePasswordAuthenticationFilter能使用吗?

首先security的所有过滤器都是没有图片验证码这回事的,看起来不适用了。其实这里我们可以灵活点,如果你依然想沿用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。

我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。

1、生成验证码

首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:

KaptchaConfig

package cn.itbluebox.springbootadminvue.config;


import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class KaptchaConfig {
 
    @Bean
    public DefaultKaptcha producer() {
 
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "4");
        properties.put("kaptcha.image.height", "40");
        properties.put("kaptcha.image.width", "120");
        properties.put("kaptcha.textproducer.font.size", "30");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

package cn.itbluebox.springbootadminvue.common.lang;

public class Const {
 

    public final static String CAPTCHA_KEY = "captcha";

}

package cn.itbluebox.springbootadminvue.controller;

import cn.hutool.core.map.MapUtil;
import cn.itbluebox.springbootadminvue.common.lang.Const;
import cn.itbluebox.springbootadminvue.common.lang.Result;
import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;

@RestController
public class AuthController extends BaseController {
 

    @Autowired
    Producer producer;

    @GetMapping("/captcha")
    public Result captcha() throws IOException {
 
        String key = UUID.randomUUID().toString();
        String code = producer.createText();
        BufferedImage image = producer.createImage(code);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(image,"jpg",outputStream);
        BASE64Encoder encoder = new BASE64Encoder();
        String str = "data:image/jpeg;base64,";
        String base64Img = str + encoder.encode(outputStream.toByteArray());
        redisUtil.hset(Const.CAPTCHA_KEY,key,code,120);
        return Result.success(
                MapUtil.builder()
                        .put("token",key)
                        .put("captchaImg",base64Img)
                        .build()
        );
    }
}

注意在上面的BaseController 当中添加一些新内容

public class BaseController {
 
    @Autowired
    HttpServletRequest req;
    @Autowired
    RedisUtil redisUtil;
}

启动

先启动Redis

启动项目

2、前端实现验证码显示

启动前端项目

去除moke

3、解决跨域问题

@Configuration
public class CorsConfig implements WebMvcConfigurer {
 
    private CorsConfiguration buildConfig() {
 
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.addExposedHeader("Authorization");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
 
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
 
        registry.addMapping("/**")
                .allowedOrigins("*")
//          .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                .maxAge(3600);
    }
}

4、设置过滤器

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 

    private static final String[] URL_WHITELIST = {
 
        "/login",
        "/logout",
        "/captcha",
        "/favicon.ico",
    };
    protected void configure(HttpSecurity http) throws Exception {
 
        http.cors().and().csrf().disable()
        //登录配置
        .formLogin()
/*      .successHandler()
        .failureHandler()
*/
        //禁用session
        .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        //配置拦截规则
        .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()
                .anyRequest().authenticated()
        //异常处理器
        //配置自定义的过滤器
    ;
    }
}

重新启动项目

刷新页面

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
 

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
 

        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();

        Result result = Result.fail("用户名或密码错误");

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

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

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
 
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = response.getOutputStream();
        //生成jwt 。 并放置到请求头中
        Result result = Result.success("成功");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    private static final String[] URL_WHITELIST = {
 
        "/login",
        "/logout",
        "/captcha",
        "/favicon.ico",
    };
    protected void configure(HttpSecurity http) throws Exception {
 
        http.cors().and().csrf().disable()
        //登录配置
        .formLogin()
        .successHandler(loginSuccessHandler)
        .failureHandler(loginFailureHandler)
        //禁用session
        .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        //配置拦截规则
        .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()
                .anyRequest().authenticated()
        //异常处理器
        //配置自定义的过滤器
    ;
    }
}

刷新页面

5、设置点击刷新二维码

<el-image style="width: 80px; height: 40px;float: left;padding-left: 25px;" 
@click="getCaptcha" :src="captchaImg" ></el-image>

设置点击后清空对应的内容

6、设置验证码过滤器

(1)设置验证码错误异常

public class CaptchaException extends AuthenticationException {
 

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

(2)验证码过滤器

package cn.itbluebox.springbootadminvue.security;

import cn.itbluebox.springbootadminvue.common.exception.CaptchaException;
import cn.itbluebox.springbootadminvue.common.lang.Const;
import cn.itbluebox.springbootadminvue.utils.RedisUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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;

@Component
public class CaptchaFilter extends OncePerRequestFilter {
 


    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
 

        String url = request.getRequestURI();

        if("/login".equals(url) && request.getMethod().equals("POST") ){
 
            try{
 
                //校验验证码
                validate(request);
                //如果不正确,就跳转到认证失败处理器
            }catch (CaptchaException e){
 
                //交给失败的处理器(认证失败处理器)
                loginFailureHandler.onAuthenticationFailure(request,response,e);
            }
        }
        filterChain.doFilter(request,response);
    }
    //校验逻辑
    private void validate(HttpServletRequest request) {
 
        String code = request.getParameter("code");
        String key = request.getParameter("token");
        if(StringUtils.isBlank(code) || StringUtils.isBlank(key)){
 
            throw new CaptchaException("验证码错误");
        }
        if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,key))){
 
            throw new CaptchaException("验证码错误");
        }
        //一次性使用
        redisUtil.hdel(Const.CAPTCHA_KEY);
    }
}

7、配置过滤器

//异常处理器
        //配置自定义的过滤器
        .and()
        .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);

七、完成登录并生成JWT

登录成功之后前端就可以获取到了jwt的信息,

前端中我们是保存在了store中,

同时也保存在了localStorage中,

然后每次axios请求之前,

我们都会添加上我们的请求头信息,可以回顾一下。

1、编写JwtUtils

package cn.itbluebox.springbootadminvue.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "itbluebox.jwt")
public class JwtUtils {
 
    private long expire;
    private String secret;
    private String header;
    //生成  JWT
    public String generateToken(String username){
 

        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)//7天逾期
                .signWith(SignatureAlgorithm.ES512,secret)
                .compact();
    }
    //解析JWT
    public Claims getClaimByToken(String jwt){
 
        try{
 
            return   Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        }catch (Exception e){
 
            return null;
        }
    }
    //JWT 是否过期的方法
    public boolean isTokenExpired(Claims claims){
 
        return claims.getExpiration().before(new Date());
    }
}

2、编写Jwt对应的配置文件

itbluebox:
  jwt:
    header: Authorization
    expire: 604800 #7天,秒单位
    secret: 212wdseqw23red232r3rds23r21212hg  #填够32位

八、身份认证 - 1

登录成功之后前端就可以获取到了jwt的信息,前端中我们是保存在了store中,同时也保存在了localStorage中,然后每次axios请求之前,我们都会添加上我们的请求头信息

所以后端进行用户身份识别的时候,我们需要通过请求头中获取jwt,然后解析出我们的用户名,这样我们就可以知道是谁在访问我们的接口啦,然后判断用户是否有权限等操作。

那么我们自定义一个过滤器用来进行识别jwt。

1、JwtAuthenticationFilter

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
 

    @Autowired
    private JwtUtils jwtUtils;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
 
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 
        String jwt = request.getHeader(jwtUtils.getHeader());
        if(StrUtil.isBlankOrUndefined(jwt)){
 
            chain.doFilter(request,response);
            return;
        }
        Claims claim = jwtUtils.getClaimByToken(jwt);
        if(ObjectUtils.isEmpty(claim)){
 
            throw new JwtException("token 异常");
        }
        if(jwtUtils.isTokenExpired(claim)){
 
            throw new JwtException("token已经过期");
        }
        String username = claim.getSubject();
        //获取用户的权限信息
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(username,null,null);
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request,response);
    }
}

2、完善SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Autowired
    private LoginFailureHandler loginFailureHandler;
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    CaptchaFilter captchaFilter;

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
 
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }

    private static final String[] URL_WHITELIST = {
 
        "/login",
        "/logout",
        "/captcha",
        "/favicon.ico",
    };
    protected void configure(HttpSecurity http) throws Exception {
 
        http.cors().and().csrf().disable()
        //登录配置
        .formLogin()
        .successHandler(loginSuccessHandler)
        .failureHandler(loginFailureHandler)
        //禁用session
        .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        //配置拦截规则
        .and()
                .authorizeRequests()
                .antMatchers(URL_WHITELIST).permitAll()
                .anyRequest().authenticated()
        //异常处理器
        //配置自定义的过滤器
                .and()
                .addFilter(jwtAuthenticationFilter())
                .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
    ;
    }
}

3、发起请求测试

http://localhost:8081/sys-user/list

九、用户认证失败或权限不足异常处理

1、认证失败处理器

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
 

    }
}

2、异常处理器

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
 
    }
}

3、SecurityConfig当中

@Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;

//异常处理器
        .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

4、完善JwtAccessDeniedHandler和JwtAuthenticationEntryPoint

(1)JwtAccessDeniedHandler

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
 
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

        ServletOutputStream outputStream = response.getOutputStream();

        Result result = Result.fail(accessDeniedException.getMessage());

        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));

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

(2)JwtAuthenticationEntryPoint

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
 
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        ServletOutputStream outputStream = response.getOutputStream();
        Result result = Result.fail("请先登录");
        outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
        outputStream.flush();
        outputStream.close();
    }
}

5、内容测试

向接口发送请求: http://localhost:8081/sys-user/list

6、用户登录查库

UserDetailServiceImpl

SysUser sysUser =  sysUserService.getByUserName(username);

public interface SysUserService extends IService<SysUser> {
 
    SysUser getByUserName(String username);
}

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
 
    @Override
    public SysUser getByUserName(String username) {
 
        return getOne(new QueryWrapper<SysUser>().eq("username",username));
    }
}

package cn.itbluebox.springbootadminvue.security;
import cn.hutool.core.lang.Assert;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;

public class AccountUser implements UserDetails {
 
    private Long userId;
    private String password;
    private final String username;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;
    public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
 
        this(userId, username, password, true, true, true, true, authorities);
    }

    public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
                       boolean credentialsNonExpired, boolean accountNonLocked,
                       Collection<? extends GrantedAuthority> authorities) {
 
        Assert.isTrue(username != null && !"".equals(username) && password != null,
                "Cannot pass null or empty values to constructor");
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = authorities;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
 
        return this.authorities;
    }
    @Override
    public String getPassword() {
 
        return this.password;
    }
    @Override
    public String getUsername() {
 
        return this.username;
    }
    @Override
    public boolean isAccountNonExpired() {
 
        return this.accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
 
        return this.accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
 
        return this.credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
 
        return this.enabled;
    }
}

完善SecurityConfig

@Bean
    BCryptPasswordEncoder bCryptPasswordEncoder(){
 
        return new BCryptPasswordEncoder();
    }

完善UserDetailServiceImpl

@Service
public class UserDetailServiceImpl implements UserDetailsService {
 

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 
        SysUser sysUser =  sysUserService.getByUserName(username);
        if(ObjectUtils.isEmpty(sysUser)){
 
            throw new UsernameNotFoundException("用户名或密码不正确");
        }
        return new AccountUser(sysUser.getId(),sysUser.getUsername(),sysUser.getPassword(),getUserAuthority(sysUser.getId()));
    }
    /*
    * 获取用户权限信息(角色,菜单权限)
    * */
    public List<GrantedAuthority> getUserAuthority(Long userId){
 

        return null;

    }
}

完善SecurityConfig

@Autowired
    UserDetailServiceImpl userDetailService;

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

@RestController
@RequestMapping("/sys-user")
public class SysUserController extends BaseController {
 

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @GetMapping("list")
    public Result getUserList(){
 
        List<SysUser> list = sysUserService.list(new QueryWrapper<>(null));
        return  Result.success(list);
    }

    @GetMapping("list/pass")
    public Result pass(){
 


        //加密后的密码
        String password = bCryptPasswordEncoder.encode("111111");

        boolean matches = bCryptPasswordEncoder.matches("111111", password);

        System.out.println("匹配结果:"+matches);

        return  Result.success(password);
    }

}

编写一个测试方法生成一下密码

@SpringBootTest
class SpringbootAdminvueApplicationTests {
 
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Test
    void contextLoads() {
 
        String password = bCryptPasswordEncoder.encode("111111");

        boolean matches = bCryptPasswordEncoder.matches("111111", password);

        System.out.println("匹配结果:"+matches);

        System.out.println(password);
    }
}

在数据库当中添加对应的账号和密码

将配置文件当中SpringSecurity的内容注释掉

server:
  port: 8081
# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
#  security:
#    user:
#      name: user
#      password: 111111
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
itbluebox:
  jwt:
    header: Authorization
    expire: 604800 #7天,秒单位
    secret: 212wdseqw23red232r3rds23r21212hg  #填够32位

发送登录请求

7、用户授权

然后关于权限部分,也是security的重要功能,当用户认证成功之后,我们就知道谁在访问系统接口,这是又有一个问题,就是这个用户有没有权限来访问我们这个接口呢,要解决这个问题,我们需要知道用户有哪些权限,哪些角色,这样security才能我们做权限判断。

之前我们已经定义及几张表,用户、角色、菜单、以及一些关联表,一般当权限粒度比较细的时候,我们都通过判断用户有没有此菜单或操作的权限,而不是通过角色判断,而用户和菜单是不直接做关联的,是通过用户拥有哪些角色,然后角色拥有哪些菜单权限这样来获得的。

问题1:我们是在哪里赋予用户权限的?有两个地方:

  • 1、用户登录,调用调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。
  • 2、接口调用进行身份认证过滤器时候JWTAuthenticationFilter,需要返回用户权限信息

问题2:在哪里决定什么接口需要什么权限?

Security内置的权限注解:

  • @PreAuthorize:方法执行前进行权限检查
  • @PostAuthorize:方法执行后进行权限检查
  • @Secured:类似于@PreAuthorize
    可以在Controller的方法前添加这些注解表示接口需要什么权限。

@RestController
@RequestMapping("/sys-user")
public class SysUserController extends BaseController {
 

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    //合作权限拥有admin的才能访问
    @PreAuthorize("hasRole('admin')")
    @GetMapping("list")
    public Result getUserList(){
 
        List<SysUser> list = sysUserService.list(new QueryWrapper<>(null));
        return  Result.success(list);
    }

    //普通用户、超级管理员
    //当前方法只有拥有sys:user:list的权限的管理员才能访问方法
    @PreAuthorize("hasAnyAuthority('sys:user:list')")
    @GetMapping("list/pass")
    public Result pass(){
 

        //加密后的密码
        String password = bCryptPasswordEncoder.encode("111111");

        boolean matches = bCryptPasswordEncoder.matches("111111", password);

        System.out.println("匹配结果:"+matches);

        return  Result.success(password);
    }

}

8、完善权限方法

/*
    * 获取用户权限信息(角色,菜单权限)
    * */
    public List<GrantedAuthority> getUserAuthority(Long userId){
 
        //角色(ROLE_admin)、菜单操作权限、sys:user:list
        String authority = sysUserService.getUserAuthorityInfo(userId); //ROLE_admin,ROLE_normal,sys:user:list,....
        return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
    }

String getUserAuthorityInfo(Long userId);

在SysUserServiceImpl当中

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
 

    @Autowired
    private SysRoleService sysUserService;

    @Autowired
    private SysUserMapper sysUserMapper;

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

    @Override
    public String getUserAuthorityInfo(Long userId) {
 
        //通过用户id获取对应的角色信息

        String authority = null;

        //获取角色
        //通过用户id,查出对应的用户角色id,通过角色id查询,对应用户的角色信息
        List<SysRole> roles = sysUserService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + userId));

        if(roles.size() > 0){
 
            String roleCode = roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(","));
            authority = roleCode;
        }
        //获取菜单操作权限
        List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);



        return null;
    }
}

<?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="cn.itbluebox.springbootadminvue.mapper.SysUserMapper">

    <select id="getNavMenuIds" resultType="java.lang.Long">

        select
            DISTINCT rm.menu_id
        from
            sys_user_role ur
        left join sys_role_menu rm on ur.role_id = rm.role_id

        where ur.user_id = #{userId}

    </select>
</mapper>

完善SysUserServiceImpl

/**
 * <p>
 *  服务实现类
 * </p>
 * @author itbluebox
 * @since 2022-05-26
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
 
    @Autowired
    private SysRoleService sysUserService;

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysMenuService sysMenuService;

    @Override
    public SysUser getByUserName(String username) {
 
        return getOne(new QueryWrapper<SysUser>().eq("username",username));
    }
    @Override
    public String getUserAuthorityInfo(Long userId) {
 
        //通过用户id获取对应的角色信息
        String authority = null;
        //获取角色编码
        //通过用户id,查出对应的用户角色id,通过角色id查询,对应用户的角色信息
        List<SysRole> roles = sysUserService.list(new QueryWrapper<SysRole>().inSql("id", "select role_id from sys_user_role where user_id = " + userId));
        if(roles.size() > 0){
 
            String roleCode = roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(","));
            authority = roleCode.concat(",");
        }
        //获取菜单操作权限
        List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
        if(menuIds.size() > 0){
 
            List<SysMenu> sysMenus = sysMenuService.listByIds(menuIds);
            String menuPerms = sysMenus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
            authority = authority.concat(menuPerms);
        }
        return authority;
    }
}

完善JwtAuthenticationFilter

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
 

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailServiceImpl userDetailService;

    @Autowired
    private SysUserService sysUserService;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
 
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 
        String jwt = request.getHeader(jwtUtils.getHeader());
        if(StrUtil.isBlankOrUndefined(jwt)){
 
            chain.doFilter(request,response);
            return;
        }
        Claims claim = jwtUtils.getClaimByToken(jwt);
        if(ObjectUtils.isEmpty(claim)){
 
            throw new JwtException("token 异常");
        }
        if(jwtUtils.isTokenExpired(claim)){
 
            throw new JwtException("token已经过期");
        }
        String username = claim.getSubject();

        SysUser sysUser = sysUserService.getByUserName(username);

        //获取用户的权限信息
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(username,null,userDetailService.getUserAuthority(sysUser.getId()));
        SecurityContextHolder.getContext().setAuthentication(token);
        chain.doFilter(request,response);
    }
}

9、测试运行

http://localhost:8081/captcha

发起获取验证码请求

发起登录请求

http://localhost:8081/login

复制token

粘贴到回去信息的header当中

发起获取信息请求: http://localhost:8081/sys-user/list

Guess you like

Origin juejin.im/post/7118206768634675207