Java 之SpringBoot+Vue实现后台管理系统的开发_springboot+vue后台管理系统-程序员宅基地

技术标签: spring boot  java  项目  前端  vue.js  javascript  

从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以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="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) &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
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/china_coding/article/details/125776016

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan