• Java 之SpringBoot+Vue实现后台管理系统的开发


    从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为我们的框架基础,这是离不开的了。

    然后数据层,我们常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里我推荐使用Mybatis Plus( mp.baomidou.com/ ),为简化开发而生,只需简单配置,即可快速进行CRUD操作,从而节省大量时间。

    SpringSecurity,使用security作为我们的权限控制和会话控制的框架。

    • SpringBoot
    • mybatis plus
    • spring security
    • lombok
    • redis
    • hibernate validatior
    • jwt

    二、新建SpringBoot 项目,注意版本

    1、新建SpringBoot工程

    这里,我们使用IDEA来开发我们项目

    开发工具与环境:

    idea

    mysql

    jdk 8

    maven3.3.9

    新建SpringBoot

    删除部分内容

    2、整合MyBatis plus,生成代码

    (1)引入依赖

    <!--整合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)设置配置文件

    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
    复制代码

    新建一个包:通过@mapperScan注解指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类。

    @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 -&gt; configuration.setUseDeprecatedExecutor(false);
    }
    

    }
    复制代码

    创建对应的mapper文件

    (3)创建数据库和表

    SQL语句

    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)代码生成

    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 {

    /**
     * &lt;p&gt;
     * 读取控制台内容
     * &lt;/p&gt;
     */
    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&amp;useSSL=false&amp;characterEncoding=utf8&amp;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&lt;FileOutConfig&gt; focList = new ArrayList&lt;&gt;();
        // 自定义配置会被优先输出
        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、获取对应项目所有的表和字段的信息

    2、新建一个freemarker的页面模板

    3、提供相关需要进行渲染的动态数据

    # 获取表
    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
    复制代码

    自动生成代码

    我们发现实体类和controller报错缺少对应的Bese

    创建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;
    

    }
    复制代码

    注意每一个Controller的引入

    (5)编写测试方法

    /**
     * <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&lt;SysUser&gt; getUserList(){
    
        List&lt;SysUser&gt; list = sysUserService.list(new QueryWrapper&lt;&gt;(null));
        return  list;
    }
    

    }
    复制代码

    在启动类上设置对应的mapper扫描

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

    }
    复制代码

    启动项目

    访问接口

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

    访问成功

    在数据库当中添加一些数据

    刷新页面

    三、结果封装

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

    这里我们用到了一个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&lt;SysUser&gt; list = sysUserService.list(new QueryWrapper&lt;&gt;(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 &gt; 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 &amp;&amp; key.length &gt; 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 &gt; 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 &lt; 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 &lt; 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&lt;Object, Object&gt; hmget(String key) {
    
        return redisTemplate.opsForHash().entries(key);
    }
    
    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map&lt;String, Object&gt; 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&lt;String, Object&gt; map, long time) {
    
        try {
    
            redisTemplate.opsForHash().putAll(key, map);
            if (time &gt; 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 &gt; 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&lt;Object&gt; 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 &gt; 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&lt;Object&gt; 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&gt;=0时, 0 表头,1 第二个元素,依次类推;index&lt;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 &gt; 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&lt;Object&gt; 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&lt;Object&gt; value, long time) {
    
        try {
    
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time &gt; 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&lt;ZSetOperations.TypedTuple&gt; 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&lt;ZSetOperations.TypedTuple&gt; 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=" 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) &amp;&amp; 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&lt;SysUser&gt;().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&lt;? extends GrantedAuthority&gt; 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&lt;? extends GrantedAuthority&gt; 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&lt;? extends GrantedAuthority&gt; authorities) {
    
        Assert.isTrue(username != null &amp;&amp; !"".equals(username) &amp;&amp; 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&lt;? extends GrantedAuthority&gt; 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&lt;GrantedAuthority&gt; 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&lt;SysUser&gt; list = sysUserService.list(new QueryWrapper&lt;&gt;(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&lt;SysUser&gt; list = sysUserService.list(new QueryWrapper&lt;&gt;(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&lt;SysUser&gt;().eq("username",username));
    }
    
    @Override
    public String getUserAuthorityInfo(Long userId) {
    
        //通过用户id获取对应的角色信息
    
        String authority = null;
    
        //获取角色
        //通过用户id,查出对应的用户角色id,通过角色id查询,对应用户的角色信息
        List&lt;SysRole&gt; roles = sysUserService.list(new QueryWrapper&lt;SysRole&gt;().inSql("id", "select role_id from sys_user_role where user_id = " + userId));
    
        if(roles.size() &gt; 0){
    
            String roleCode = roles.stream().map(r -&gt; "ROLE_"+r.getCode()).collect(Collectors.joining(","));
            authority = roleCode;
        }
        //获取菜单操作权限
        List&lt;Long&gt; 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">
    
    &lt;select id="getNavMenuIds" resultType="java.lang.Long"&gt;
    
        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}
    
    &lt;/select&gt;
    

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

    来源:https://juejin.cn/post/7118206768634675207
  • 相关阅读:
    Thinkphp M方法出错,D方法却可以
    Composer项目安装依赖包
    wamp httpd-vhosts.conf
    博客园报错 Mixed Content: The page at 'https://i.cnblogs.com/EditPosts.aspx?opt=1' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://upload.cnblogs.com/imageuploa
    Thinkphp js、css压缩类minify
    Thinkphp 不足之处
    Thinkphp 调试方法
    Lavavel 程序报错 MassAssignmentException in Model.php line 452: _token
    Laravel 安装mysql、表增加模拟数据、生成控制器
    Laravel 安装登录模块
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16475949.html
Copyright © 2020-2023  润新知