当前位置:   article > 正文

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

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)引入依赖

  1. <!--整合mybatis plus https://baomidou.com/-->
  2. <dependency>
  3. <groupId>com.baomidou</groupId>
  4. <artifactId>mybatis-plus-boot-starter</artifactId>
  5. <version>3.4.1</version>
  6. </dependency>
  7. <!--mp代码生成器-->
  8. <dependency>
  9. <groupId>com.baomidou</groupId>
  10. <artifactId>mybatis-plus-generator</artifactId>
  11. <version>3.4.1</version>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.freemarker</groupId>
  15. <artifactId>freemarker</artifactId>
  16. <version>2.3.30</version>
  17. </dependency>
  18. <dependency>
  19. <groupId>mysql</groupId>
  20. <artifactId>mysql-connector-java</artifactId>
  21. <scope>runtime</scope>
  22. </dependency>复制代码

(2)设置配置文件

  1. server:
  2. port: 8081
  3. # DataSource Config
  4. spring:
  5. datasource:
  6. driver-class-name: com.mysql.cj.jdbc.Driver
  7. url: jdbc:mysql://localhost:3306/zhengadminvue?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  8. username: root
  9. password: root
  10. mybatis-plus:
  11. mapper-locations: classpath*:/mapper/**Mapper.xml复制代码

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

  1. @Configuration
  2. @ManagedBean("cn.itbluebox.springbootadminvue.mapper")
  3. 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语句

  1. DROP TABLE IF EXISTS `sys_menu`;
  2. CREATE TABLE `sys_menu` (
  3. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  4. `parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
  5. `name` varchar(64) NOT NULL,
  6. `path` varchar(255) DEFAULT NULL COMMENT '菜单URL',
  7. `perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  8. `component` varchar(255) DEFAULT NULL,
  9. `type` int(5) NOT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',
  10. `icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',
  11. `orderNum` int(11) DEFAULT NULL COMMENT '排序',
  12. `created` datetime NOT NULL,
  13. `updated` datetime DEFAULT NULL,
  14. `statu` int(5) NOT NULL,
  15. PRIMARY KEY (`id`),
  16. UNIQUE KEY `name` (`name`) USING BTREE
  17. ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
  18. -- ----------------------------
  19. -- Table structure for sys_role
  20. -- ----------------------------
  21. DROP TABLE IF EXISTS `sys_role`;
  22. CREATE TABLE `sys_role` (
  23. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  24. `name` varchar(64) NOT NULL,
  25. `code` varchar(64) NOT NULL,
  26. `remark` varchar(64) DEFAULT NULL COMMENT '备注',
  27. `created` datetime DEFAULT NULL,
  28. `updated` datetime DEFAULT NULL,
  29. `statu` int(5) NOT NULL,
  30. PRIMARY KEY (`id`),
  31. UNIQUE KEY `name` (`name`) USING BTREE,
  32. UNIQUE KEY `code` (`code`) USING BTREE
  33. ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
  34. -- ----------------------------
  35. -- Table structure for sys_role_menu
  36. -- ----------------------------
  37. DROP TABLE IF EXISTS `sys_role_menu`;
  38. CREATE TABLE `sys_role_menu` (
  39. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  40. `role_id` bigint(20) NOT NULL,
  41. `menu_id` bigint(20) NOT NULL,
  42. PRIMARY KEY (`id`)
  43. ) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;
  44. -- ----------------------------
  45. -- Table structure for sys_user
  46. -- ----------------------------
  47. DROP TABLE IF EXISTS `sys_user`;
  48. CREATE TABLE `sys_user` (
  49. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  50. `username` varchar(64) DEFAULT NULL,
  51. `password` varchar(64) DEFAULT NULL,
  52. `avatar` varchar(255) DEFAULT NULL,
  53. `email` varchar(64) DEFAULT NULL,
  54. `city` varchar(64) DEFAULT NULL,
  55. `created` datetime DEFAULT NULL,
  56. `updated` datetime DEFAULT NULL,
  57. `last_login` datetime DEFAULT NULL,
  58. `statu` int(5) NOT NULL,
  59. PRIMARY KEY (`id`),
  60. UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
  61. ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
  62. -- ----------------------------
  63. -- Table structure for sys_user_role
  64. -- ----------------------------
  65. DROP TABLE IF EXISTS `sys_user_role`;
  66. CREATE TABLE `sys_user_role` (
  67. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  68. `user_id` bigint(20) NOT NULL,
  69. `role_id` bigint(20) NOT NULL,
  70. PRIMARY KEY (`id`)
  71. ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;复制代码

(4)代码生成

  1. package cn.itbluebox.springbootadminvue;
  2. import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
  3. import com.baomidou.mybatisplus.core.toolkit.StringPool;
  4. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
  5. import com.baomidou.mybatisplus.generator.AutoGenerator;
  6. import com.baomidou.mybatisplus.generator.InjectionConfig;
  7. import com.baomidou.mybatisplus.generator.config.*;
  8. import com.baomidou.mybatisplus.generator.config.po.TableInfo;
  9. import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
  10. import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

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

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

  1. /**
  2. * &lt;p&gt;
  3. * 读取控制台内容
  4. * &lt;/p&gt;
  5. */
  6. public static String scanner(String tip) {
  7. Scanner scanner = new Scanner(System.in);
  8. StringBuilder help = new StringBuilder();
  9. help.append("请输入" + tip + ":");
  10. System.out.println(help.toString());
  11. if (scanner.hasNext()) {
  12. String ipt = scanner.next();
  13. if (StringUtils.isNotBlank(ipt)) {
  14. return ipt;
  15. }
  16. }
  17. throw new MybatisPlusException("请输入正确的" + tip + "!");
  18. }
  19. public static void main(String[] args) {
  20. // 代码生成器
  21. AutoGenerator mpg = new AutoGenerator();
  22. // 全局配置
  23. GlobalConfig gc = new GlobalConfig();
  24. String projectPath = System.getProperty("user.dir");
  25. gc.setOutputDir(projectPath + "/src/main/java");
  26. gc.setAuthor("itbluebox");
  27. gc.setOpen(false);
  28. // gc.setSwagger2(true); 实体属性 Swagger2 注解
  29. gc.setServiceName("%sService");
  30. mpg.setGlobalConfig(gc);
  31. // 数据源配置
  32. DataSourceConfig dsc = new DataSourceConfig();
  33. dsc.setUrl("jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&amp;useSSL=false&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai");
  34. // dsc.setSchemaName("public");
  35. dsc.setDriverName("com.mysql.cj.jdbc.Driver");
  36. dsc.setUsername("root");
  37. dsc.setPassword("root");
  38. mpg.setDataSource(dsc);
  39. // 包配置
  40. PackageConfig pc = new PackageConfig();

// pc.setModuleName(scanner("模块名"));
pc.setParent("cn.itbluebox.springbootadminvue");
mpg.setPackageInfo(pc);

  1. // 自定义配置
  2. InjectionConfig cfg = new InjectionConfig() {
  3. @Override
  4. public void initMap() {
  5. // to do nothing
  6. }
  7. };
  8. // 如果模板引擎是 freemarker
  9. String templatePath = "/templates/mapper.xml.ftl";
  10. // 如果模板引擎是 velocity

// String templatePath = "/templates/mapper.xml.vm";

  1. // 自定义输出配置
  2. List&lt;FileOutConfig&gt; focList = new ArrayList&lt;&gt;();
  3. // 自定义配置会被优先输出
  4. focList.add(new FileOutConfig(templatePath) {
  5. @Override
  6. public String outputFile(TableInfo tableInfo) {
  7. // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
  8. return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
  9. + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
  10. }
  11. });
  12. /*
  13. cfg.setFileCreate(new IFileCreate() {
  14. @Override
  15. public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
  16. // 判断自定义文件夹是否需要创建
  17. checkDir("调用默认方法创建的目录,自定义目录用");
  18. if (fileType == FileType.MAPPER) {
  19. // 已经生成 mapper 文件判断存在,不想重新生成返回 false
  20. return !new File(filePath).exists();
  21. }
  22. // 允许生成模板文件
  23. return true;
  24. }
  25. });
  26. */
  27. cfg.setFileOutConfigList(focList);
  28. mpg.setCfg(cfg);
  29. // 配置模板
  30. TemplateConfig templateConfig = new TemplateConfig();
  31. // 配置自定义输出模板
  32. //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
  33. // templateConfig.setEntity("templates/entity2.java");
  34. // templateConfig.setService();
  35. // templateConfig.setController();
  36. templateConfig.setXml(null);
  37. mpg.setTemplate(templateConfig);
  38. // 策略配置
  39. StrategyConfig strategy = new StrategyConfig();
  40. strategy.setNaming(NamingStrategy.underline_to_camel);
  41. strategy.setColumnNaming(NamingStrategy.underline_to_camel);
  42. strategy.setSuperEntityClass("BaseEntity");
  43. strategy.setEntityLombokModel(true);
  44. strategy.setRestControllerStyle(true);
  45. // 公共父类
  46. strategy.setSuperControllerClass("BaseController");
  47. // 写于父类中的公共字段
  48. strategy.setSuperEntityColumns("id", "created", "updated", "statu");
  49. strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
  50. strategy.setControllerMappingHyphenStyle(true);

// strategy.setTablePrefix("sys_");//动态调整
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
复制代码

1、获取对应项目所有的表和字段的信息

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

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

  1. # 获取表
  2. SELECT
  3. *
  4. FROM
  5. information_schema. TABLES
  6. WHERE
  7. TABLE_SCHEMA = (SELECT DATABASE());复制代码

  1. # 获取字段
  2. SELECT
  3. *
  4. FROM
  5. information_schema. COLUMNS
  6. WHERE
  7. TABLE_SCHEMA = (SELECT DATABASE())
  8. 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 {

  1. @TableId(value = "id", type = IdType.AUTO)
  2. private Long id;
  3. private LocalDateTime created;
  4. private LocalDateTime updated;
  5. private Integer statu;

}
复制代码

注意每一个Controller的引入

(5)编写测试方法

  1. /**
  2. * <p>
  3. * 前端控制器
  4. * </p>
  5. *
  6. * @author itbluebox
  7. * @since 2022-05-26
  8. */
  9. @RestController
  10. @RequestMapping("/sys-user")
  11. 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扫描

  1. @SpringBootApplication
  2. @MapperScan("cn.itbluebox.springbootadminvue.mapper")
  3. 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 {

  1. private int code;
  2. private String msg;
  3. private Object data;
  4. public static Result success(Object data){
  5. return success(200,"操作成功",data);
  6. }
  7. public static Result success(int code,String msg,Object data){
  8. Result r = new Result();
  9. r.setData(data);
  10. r.setMsg(msg);
  11. r.setCode(code);
  12. return r;
  13. }
  14. public static Result fail(String msg){
  15. return fail(400,msg, null);
  16. }
  17. public static Result fail(int code,String msg,Object data){
  18. Result r = new Result();
  19. r.setData(data);
  20. r.setMsg(msg);
  21. r.setCode(code);
  22. return r;
  23. }

}
复制代码

修改SysUserController

  1. /**
  2. * <p>
  3. * 前端控制器
  4. * </p>
  5. * @author itbluebox
  6. * @since 2022-05-26
  7. */
  8. @RestController
  9. @RequestMapping("/sys-user")
  10. 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复制代码

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

  1. /**
  2. * 全局异常处理
  3. */
  4. @Slf4j
  5. @RestControllerAdvice
  6. 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

  1. <!-- springboot security -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-data-redis</artifactId>
  9. </dependency>
  10. <!-- jwt -->
  11. <dependency>
  12. <groupId>io.jsonwebtoken</groupId>
  13. <artifactId>jjwt</artifactId>
  14. <version>0.9.1</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>com.github.axet</groupId>
  18. <artifactId>kaptcha</artifactId>
  19. <version>0.0.9</version>
  20. </dependency>
  21. <!-- hutool工具类-->
  22. <dependency>
  23. <groupId>cn.hutool</groupId>
  24. <artifactId>hutool-all</artifactId>
  25. <version>5.3.3</version>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.apache.commons</groupId>
  29. <artifactId>commons-lang3</artifactId>
  30. <version>3.11</version>
  31. </dependency>复制代码

重新启动项目

访问: http://localhost:8081

用户名:user

密码:控制台已经输出

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

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

application.yml

  1. spring:
  2. security:
  3. user:
  4. name: user
  5. 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 {

  1. @Autowired
  2. private RedisTemplate redisTemplate;
  3. /**
  4. * 指定缓存失效时间
  5. *
  6. * @param key 键
  7. * @param time 时间(秒)
  8. * @return
  9. */
  10. public boolean expire(String key, long time) {
  11. try {
  12. if (time &gt; 0) {
  13. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  14. }
  15. return true;
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. return false;
  19. }
  20. }
  21. /**
  22. * 根据key 获取过期时间
  23. *
  24. * @param key 键 不能为null
  25. * @return 时间(秒) 返回0代表为永久有效
  26. */
  27. public long getExpire(String key) {
  28. return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  29. }
  30. /**
  31. * 判断key是否存在
  32. *
  33. * @param key 键
  34. * @return true 存在 false不存在
  35. */
  36. public boolean hasKey(String key) {
  37. try {
  38. return redisTemplate.hasKey(key);
  39. } catch (Exception e) {
  40. e.printStackTrace();
  41. return false;
  42. }
  43. }
  44. /**
  45. * 删除缓存
  46. *
  47. * @param key 可以传一个值 或多个
  48. */
  49. @SuppressWarnings("unchecked")
  50. public void del(String... key) {
  51. if (key != null &amp;&amp; key.length &gt; 0) {
  52. if (key.length == 1) {
  53. redisTemplate.delete(key[0]);
  54. } else {
  55. redisTemplate.delete(CollectionUtils.arrayToList(key));
  56. }
  57. }
  58. }
  59. //============================String=============================
  60. /**
  61. * 普通缓存获取
  62. *
  63. * @param key 键
  64. * @return 值
  65. */
  66. public Object get(String key) {
  67. return key == null ? null : redisTemplate.opsForValue().get(key);
  68. }
  69. /**
  70. * 普通缓存放入
  71. *
  72. * @param key 键
  73. * @param value 值
  74. * @return true成功 false失败
  75. */
  76. public boolean set(String key, Object value) {
  77. try {
  78. redisTemplate.opsForValue().set(key, value);
  79. return true;
  80. } catch (Exception e) {
  81. e.printStackTrace();
  82. return false;
  83. }
  84. }
  85. /**
  86. * 普通缓存放入并设置时间
  87. *
  88. * @param key 键
  89. * @param value 值
  90. * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
  91. * @return true成功 false 失败
  92. */
  93. public boolean set(String key, Object value, long time) {
  94. try {
  95. if (time &gt; 0) {
  96. redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
  97. } else {
  98. set(key, value);
  99. }
  100. return true;
  101. } catch (Exception e) {
  102. e.printStackTrace();
  103. return false;
  104. }
  105. }
  106. /**
  107. * 递增
  108. *
  109. * @param key 键
  110. * @param delta 要增加几(大于0)
  111. * @return
  112. */
  113. public long incr(String key, long delta) {
  114. if (delta &lt; 0) {
  115. throw new RuntimeException("递增因子必须大于0");
  116. }
  117. return redisTemplate.opsForValue().increment(key, delta);
  118. }
  119. /**
  120. * 递减
  121. *
  122. * @param key 键
  123. * @param delta 要减少几(小于0)
  124. * @return
  125. */
  126. public long decr(String key, long delta) {
  127. if (delta &lt; 0) {
  128. throw new RuntimeException("递减因子必须大于0");
  129. }
  130. return redisTemplate.opsForValue().increment(key, -delta);
  131. }
  132. //================================Map=================================
  133. /**
  134. * HashGet
  135. *
  136. * @param key 键 不能为null
  137. * @param item 项 不能为null
  138. * @return 值
  139. */
  140. public Object hget(String key, String item) {
  141. return redisTemplate.opsForHash().get(key, item);
  142. }
  143. /**
  144. * 获取hashKey对应的所有键值
  145. *
  146. * @param key 键
  147. * @return 对应的多个键值
  148. */
  149. public Map&lt;Object, Object&gt; hmget(String key) {
  150. return redisTemplate.opsForHash().entries(key);
  151. }
  152. /**
  153. * HashSet
  154. *
  155. * @param key 键
  156. * @param map 对应多个键值
  157. * @return true 成功 false 失败
  158. */
  159. public boolean hmset(String key, Map&lt;String, Object&gt; map) {
  160. try {
  161. redisTemplate.opsForHash().putAll(key, map);
  162. return true;
  163. } catch (Exception e) {
  164. e.printStackTrace();
  165. return false;
  166. }
  167. }
  168. /**
  169. * HashSet 并设置时间
  170. *
  171. * @param key 键
  172. * @param map 对应多个键值
  173. * @param time 时间(秒)
  174. * @return true成功 false失败
  175. */
  176. public boolean hmset(String key, Map&lt;String, Object&gt; map, long time) {
  177. try {
  178. redisTemplate.opsForHash().putAll(key, map);
  179. if (time &gt; 0) {
  180. expire(key, time);
  181. }
  182. return true;
  183. } catch (Exception e) {
  184. e.printStackTrace();
  185. return false;
  186. }
  187. }
  188. /**
  189. * 向一张hash表中放入数据,如果不存在将创建
  190. *
  191. * @param key 键
  192. * @param item 项
  193. * @param value 值
  194. * @return true 成功 false失败
  195. */
  196. public boolean hset(String key, String item, Object value) {
  197. try {
  198. redisTemplate.opsForHash().put(key, item, value);
  199. return true;
  200. } catch (Exception e) {
  201. e.printStackTrace();
  202. return false;
  203. }
  204. }
  205. /**
  206. * 向一张hash表中放入数据,如果不存在将创建
  207. *
  208. * @param key 键
  209. * @param item 项
  210. * @param value 值
  211. * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
  212. * @return true 成功 false失败
  213. */
  214. public boolean hset(String key, String item, Object value, long time) {
  215. try {
  216. redisTemplate.opsForHash().put(key, item, value);
  217. if (time &gt; 0) {
  218. expire(key, time);
  219. }
  220. return true;
  221. } catch (Exception e) {
  222. e.printStackTrace();
  223. return false;
  224. }
  225. }
  226. /**
  227. * 删除hash表中的值
  228. *
  229. * @param key 键 不能为null
  230. * @param item 项 可以使多个 不能为null
  231. */
  232. public void hdel(String key, Object... item) {
  233. redisTemplate.opsForHash().delete(key, item);
  234. }
  235. /**
  236. * 判断hash表中是否有该项的值
  237. *
  238. * @param key 键 不能为null
  239. * @param item 项 不能为null
  240. * @return true 存在 false不存在
  241. */
  242. public boolean hHasKey(String key, String item) {
  243. return redisTemplate.opsForHash().hasKey(key, item);
  244. }
  245. /**
  246. * hash递增 如果不存在,就会创建一个 并把新增后的值返回
  247. *
  248. * @param key 键
  249. * @param item 项
  250. * @param by 要增加几(大于0)
  251. * @return
  252. */
  253. public double hincr(String key, String item, double by) {
  254. return redisTemplate.opsForHash().increment(key, item, by);
  255. }
  256. /**
  257. * hash递减
  258. *
  259. * @param key 键
  260. * @param item 项
  261. * @param by 要减少记(小于0)
  262. * @return
  263. */
  264. public double hdecr(String key, String item, double by) {
  265. return redisTemplate.opsForHash().increment(key, item, -by);
  266. }
  267. //============================set=============================
  268. /**
  269. * 根据key获取Set中的所有值
  270. *
  271. * @param key 键
  272. * @return
  273. */
  274. public Set&lt;Object&gt; sGet(String key) {
  275. try {
  276. return redisTemplate.opsForSet().members(key);
  277. } catch (Exception e) {
  278. e.printStackTrace();
  279. return null;
  280. }
  281. }
  282. /**
  283. * 根据value从一个set中查询,是否存在
  284. *
  285. * @param key 键
  286. * @param value 值
  287. * @return true 存在 false不存在
  288. */
  289. public boolean sHasKey(String key, Object value) {
  290. try {
  291. return redisTemplate.opsForSet().isMember(key, value);
  292. } catch (Exception e) {
  293. e.printStackTrace();
  294. return false;
  295. }
  296. }
  297. /**
  298. * 将数据放入set缓存
  299. *
  300. * @param key 键
  301. * @param values 值 可以是多个
  302. * @return 成功个数
  303. */
  304. public long sSet(String key, Object... values) {
  305. try {
  306. return redisTemplate.opsForSet().add(key, values);
  307. } catch (Exception e) {
  308. e.printStackTrace();
  309. return 0;
  310. }
  311. }
  312. /**
  313. * 将set数据放入缓存
  314. *
  315. * @param key 键
  316. * @param time 时间(秒)
  317. * @param values 值 可以是多个
  318. * @return 成功个数
  319. */
  320. public long sSetAndTime(String key, long time, Object... values) {
  321. try {
  322. Long count = redisTemplate.opsForSet().add(key, values);
  323. if (time &gt; 0) expire(key, time);
  324. return count;
  325. } catch (Exception e) {
  326. e.printStackTrace();
  327. return 0;
  328. }
  329. }
  330. /**
  331. * 获取set缓存的长度
  332. *
  333. * @param key 键
  334. * @return
  335. */
  336. public long sGetSetSize(String key) {
  337. try {
  338. return redisTemplate.opsForSet().size(key);
  339. } catch (Exception e) {
  340. e.printStackTrace();
  341. return 0;
  342. }
  343. }
  344. /**
  345. * 移除值为value的
  346. *
  347. * @param key 键
  348. * @param values 值 可以是多个
  349. * @return 移除的个数
  350. */
  351. public long setRemove(String key, Object... values) {
  352. try {
  353. Long count = redisTemplate.opsForSet().remove(key, values);
  354. return count;
  355. } catch (Exception e) {
  356. e.printStackTrace();
  357. return 0;
  358. }
  359. }
  360. //===============================list=================================
  361. /**
  362. * 获取list缓存的内容
  363. *
  364. * @param key 键
  365. * @param start 开始
  366. * @param end 结束 0 到 -1代表所有值
  367. * @return
  368. */
  369. public List&lt;Object&gt; lGet(String key, long start, long end) {
  370. try {
  371. return redisTemplate.opsForList().range(key, start, end);
  372. } catch (Exception e) {
  373. e.printStackTrace();
  374. return null;
  375. }
  376. }
  377. /**
  378. * 获取list缓存的长度
  379. *
  380. * @param key 键
  381. * @return
  382. */
  383. public long lGetListSize(String key) {
  384. try {
  385. return redisTemplate.opsForList().size(key);
  386. } catch (Exception e) {
  387. e.printStackTrace();
  388. return 0;
  389. }
  390. }
  391. /**
  392. * 通过索引 获取list中的值
  393. *
  394. * @param key 键
  395. * @param index 索引 index&gt;=0时, 0 表头,1 第二个元素,依次类推;index&lt;0时,-1,表尾,-2倒数第二个元素,依次类推
  396. * @return
  397. */
  398. public Object lGetIndex(String key, long index) {
  399. try {
  400. return redisTemplate.opsForList().index(key, index);
  401. } catch (Exception e) {
  402. e.printStackTrace();
  403. return null;
  404. }
  405. }
  406. /**
  407. * 将list放入缓存
  408. *
  409. * @param key 键
  410. * @param value 值
  411. * @return
  412. */
  413. public boolean lSet(String key, Object value) {
  414. try {
  415. redisTemplate.opsForList().rightPush(key, value);
  416. return true;
  417. } catch (Exception e) {
  418. e.printStackTrace();
  419. return false;
  420. }
  421. }
  422. /**
  423. * 将list放入缓存
  424. *
  425. * @param key 键
  426. * @param value 值
  427. * @param time 时间(秒)
  428. * @return
  429. */
  430. public boolean lSet(String key, Object value, long time) {
  431. try {
  432. redisTemplate.opsForList().rightPush(key, value);
  433. if (time &gt; 0) expire(key, time);
  434. return true;
  435. } catch (Exception e) {
  436. e.printStackTrace();
  437. return false;
  438. }
  439. }
  440. /**
  441. * 将list放入缓存
  442. *
  443. * @param key 键
  444. * @param value 值
  445. * @return
  446. */
  447. public boolean lSet(String key, List&lt;Object&gt; value) {
  448. try {
  449. redisTemplate.opsForList().rightPushAll(key, value);
  450. return true;
  451. } catch (Exception e) {
  452. e.printStackTrace();
  453. return false;
  454. }
  455. }
  456. /**
  457. * 将list放入缓存
  458. *
  459. * @param key 键
  460. * @param value 值
  461. * @param time 时间(秒)
  462. * @return
  463. */
  464. public boolean lSet(String key, List&lt;Object&gt; value, long time) {
  465. try {
  466. redisTemplate.opsForList().rightPushAll(key, value);
  467. if (time &gt; 0) expire(key, time);
  468. return true;
  469. } catch (Exception e) {
  470. e.printStackTrace();
  471. return false;
  472. }
  473. }
  474. /**
  475. * 根据索引修改list中的某条数据
  476. *
  477. * @param key 键
  478. * @param index 索引
  479. * @param value 值
  480. * @return
  481. */
  482. public boolean lUpdateIndex(String key, long index, Object value) {
  483. try {
  484. redisTemplate.opsForList().set(key, index, value);
  485. return true;
  486. } catch (Exception e) {
  487. e.printStackTrace();
  488. return false;
  489. }
  490. }
  491. /**
  492. * 移除N个值为value
  493. *
  494. * @param key 键
  495. * @param count 移除多少个
  496. * @param value 值
  497. * @return 移除的个数
  498. */
  499. public long lRemove(String key, long count, Object value) {
  500. try {
  501. Long remove = redisTemplate.opsForList().remove(key, count, value);
  502. return remove;
  503. } catch (Exception e) {
  504. e.printStackTrace();
  505. return 0;
  506. }
  507. }
  508. //================有序集合 sort set===================
  509. /**
  510. * 有序set添加元素
  511. *
  512. * @param key
  513. * @param value
  514. * @param score
  515. * @return
  516. */
  517. public boolean zSet(String key, Object value, double score) {
  518. return redisTemplate.opsForZSet().add(key, value, score);
  519. }
  520. public long batchZSet(String key, Set&lt;ZSetOperations.TypedTuple&gt; typles) {
  521. return redisTemplate.opsForZSet().add(key, typles);
  522. }
  523. public void zIncrementScore(String key, Object value, long delta) {
  524. redisTemplate.opsForZSet().incrementScore(key, value, delta);
  525. }
  526. public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
  527. redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
  528. }
  529. /**
  530. * 获取zset数量
  531. * @param key
  532. * @param value
  533. * @return
  534. */
  535. public long getZsetScore(String key, Object value) {
  536. Double score = redisTemplate.opsForZSet().score(key, value);
  537. if(score==null){
  538. return 0;
  539. }else{
  540. return score.longValue();
  541. }
  542. }
  543. /**
  544. * 获取有序集 key 中成员 member 的排名 。
  545. * 其中有序集成员按 score 值递减 (从大到小) 排序。
  546. * @param key
  547. * @param start
  548. * @param end
  549. * @return
  550. */
  551. public Set&lt;ZSetOperations.TypedTuple&gt; getZSetRank(String key, long start, long end) {
  552. return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
  553. }

}
复制代码

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 {

  1. @Bean
  2. RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
  3. RedisTemplate redisTemplate = new RedisTemplate();
  4. redisTemplate.setConnectionFactory(redisConnectionFactory);
  5. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  6. jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());
  7. redisTemplate.setKeySerializer(new StringRedisSerializer());
  8. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
  9. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
  10. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
  11. return redisTemplate;
  12. }

}
复制代码

六、用户认证

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

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

二次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 {

  1. @Bean
  2. public DefaultKaptcha producer() {
  3. Properties properties = new Properties();
  4. properties.put("kaptcha.border", "no");
  5. properties.put("kaptcha.textproducer.font.color", "black");
  6. properties.put("kaptcha.textproducer.char.space", "4");
  7. properties.put("kaptcha.image.height", "40");
  8. properties.put("kaptcha.image.width", "120");
  9. properties.put("kaptcha.textproducer.font.size", "30");
  10. Config config = new Config(properties);
  11. DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
  12. defaultKaptcha.setConfig(config);
  13. return defaultKaptcha;
  14. }

}
复制代码

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 {

  1. @Autowired
  2. Producer producer;
  3. @GetMapping("/captcha")
  4. public Result captcha() throws IOException {
  5. String key = UUID.randomUUID().toString();
  6. String code = producer.createText();
  7. BufferedImage image = producer.createImage(code);
  8. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  9. ImageIO.write(image,"jpg",outputStream);
  10. BASE64Encoder encoder = new BASE64Encoder();
  11. String str = "data:image/jpeg;base64,";
  12. String base64Img = str + encoder.encode(outputStream.toByteArray());
  13. redisUtil.hset(Const.CAPTCHA_KEY,key,code,120);
  14. return Result.success(
  15. MapUtil.builder()
  16. .put("token",key)
  17. .put("captchaImg",base64Img)
  18. .build()
  19. );
  20. }

}
复制代码

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

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

}
复制代码

启动

先启动Redis

启动项目

2、前端实现验证码显示

启动前端项目

去除moke

3、解决跨域问题

  1. @Configuration
  2. 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、设置过滤器

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. 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()
//异常处理器
//配置自定义的过滤器
;
}
}
复制代码

重新启动项目

刷新页面

  1. @Component
  2. 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(); }

}
复制代码

  1. @Component
  2. 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(); }

}
复制代码

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. 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、设置点击刷新二维码

  1. <el-image style="width: 80px; height: 40px;float: left;padding-left: 25px;"
  2. @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 {

  1. @Autowired
  2. private RedisUtil redisUtil;
  3. @Autowired
  4. private LoginFailureHandler loginFailureHandler;
  5. @Override
  6. protected void doFilterInternal(
  7. HttpServletRequest request,
  8. HttpServletResponse response,
  9. FilterChain filterChain) throws ServletException, IOException {
  10. String url = request.getRequestURI();
  11. if("/login".equals(url) &amp;&amp; request.getMethod().equals("POST") ){
  12. try{
  13. //校验验证码
  14. validate(request);
  15. //如果不正确,就跳转到认证失败处理器
  16. }catch (CaptchaException e){
  17. //交给失败的处理器(认证失败处理器)
  18. loginFailureHandler.onAuthenticationFailure(request,response,e);
  19. }
  20. }
  21. filterChain.doFilter(request,response);
  22. }
  23. //校验逻辑
  24. private void validate(HttpServletRequest request) {
  25. String code = request.getParameter("code");
  26. String key = request.getParameter("token");
  27. if(StringUtils.isBlank(code) || StringUtils.isBlank(key)){
  28. throw new CaptchaException("验证码错误");
  29. }
  30. if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,key))){
  31. throw new CaptchaException("验证码错误");
  32. }
  33. //一次性使用
  34. redisUtil.hdel(Const.CAPTCHA_KEY);
  35. }

}
复制代码

7、配置过滤器

  1. //异常处理器
  2. //配置自定义的过滤器
  3. .and()
  4. .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 {

  1. private long expire;
  2. private String secret;
  3. private String header;
  4. //生成 JWT
  5. public String generateToken(String username){
  6. Date nowDate = new Date();
  7. Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
  8. return Jwts.builder()
  9. .setHeaderParam("typ","JWT")
  10. .setSubject(username)
  11. .setIssuedAt(nowDate)
  12. .setExpiration(expireDate)//7天逾期
  13. .signWith(SignatureAlgorithm.ES512,secret)
  14. .compact();
  15. }
  16. //解析JWT
  17. public Claims getClaimByToken(String jwt){
  18. try{
  19. return Jwts.parser()
  20. .setSigningKey(secret)
  21. .parseClaimsJws(jwt)
  22. .getBody();
  23. }catch (Exception e){
  24. return null;
  25. }
  26. }
  27. //JWT 是否过期的方法
  28. public boolean isTokenExpired(Claims claims){
  29. return claims.getExpiration().before(new Date());
  30. }

}
复制代码

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

  1. itbluebox:
  2. jwt:
  3. header: Authorization
  4. expire: 604800 #7天,秒单位
  5. 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

  1. @Configuration
  2. @EnableWebSecurity
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. 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、认证失败处理器

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

}
复制代码

2、异常处理器

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

}
复制代码

3、SecurityConfig当中

  1. @Autowired
  2. JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired JwtAccessDeniedHandler jwtAccessDeniedHandler;

复制代码

  1. //异常处理器
  2. .and()
  3. .exceptionHandling()
  4. .authenticationEntryPoint(jwtAuthenticationEntryPoint)
  5. .accessDeniedHandler(jwtAccessDeniedHandler)复制代码

4、完善JwtAccessDeniedHandler和JwtAuthenticationEntryPoint

(1)JwtAccessDeniedHandler

  1. @Component
  2. 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

  1. @Component
  2. 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);

}
复制代码

  1. @Service
  2. 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)); }

}
复制代码

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

public class AccountUser implements UserDetails {

  1. private Long userId;
  2. private String password;
  3. private final String username;
  4. private final Collection&lt;? extends GrantedAuthority&gt; authorities;
  5. private final boolean accountNonExpired;
  6. private final boolean accountNonLocked;
  7. private final boolean credentialsNonExpired;
  8. private final boolean enabled;
  9. public AccountUser(Long userId, String username, String password, Collection&lt;? extends GrantedAuthority&gt; authorities) {
  10. this(userId, username, password, true, true, true, true, authorities);
  11. }
  12. public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
  13. boolean credentialsNonExpired, boolean accountNonLocked,
  14. Collection&lt;? extends GrantedAuthority&gt; authorities) {
  15. Assert.isTrue(username != null &amp;&amp; !"".equals(username) &amp;&amp; password != null,
  16. "Cannot pass null or empty values to constructor");
  17. this.userId = userId;
  18. this.username = username;
  19. this.password = password;
  20. this.enabled = enabled;
  21. this.accountNonExpired = accountNonExpired;
  22. this.credentialsNonExpired = credentialsNonExpired;
  23. this.accountNonLocked = accountNonLocked;
  24. this.authorities = authorities;
  25. }
  26. @Override
  27. public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
  28. return this.authorities;
  29. }
  30. @Override
  31. public String getPassword() {
  32. return this.password;
  33. }
  34. @Override
  35. public String getUsername() {
  36. return this.username;
  37. }
  38. @Override
  39. public boolean isAccountNonExpired() {
  40. return this.accountNonExpired;
  41. }
  42. @Override
  43. public boolean isAccountNonLocked() {
  44. return this.accountNonLocked;
  45. }
  46. @Override
  47. public boolean isCredentialsNonExpired() {
  48. return this.credentialsNonExpired;
  49. }
  50. @Override
  51. public boolean isEnabled() {
  52. return this.enabled;
  53. }

}
复制代码

完善SecurityConfig

  1. @Bean
  2. BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder(); }

复制代码

完善UserDetailServiceImpl

  1. @Service
  2. 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

  1. @Autowired
  2. UserDetailServiceImpl userDetailService;复制代码

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

复制代码

  1. @RestController
  2. @RequestMapping("/sys-user")
  3. 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); }

}
复制代码

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

  1. @SpringBootTest
  2. 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的内容注释掉

  1. server:
  2. port: 8081
  3. # DataSource Config
  4. spring:
  5. datasource:
  6. driver-class-name: com.mysql.cj.jdbc.Driver
  7. url: jdbc:mysql://localhost:3306/itzheng-vue-admin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  8. username: root
  9. password: root
  10. # security:
  11. # user:
  12. # name: user
  13. # password: 111111
  14. mybatis-plus:
  15. mapper-locations: classpath*:/mapper/**Mapper.xml
  16. itbluebox:
  17. jwt:
  18. header: Authorization
  19. expire: 604800 #7天,秒单位
  20. secret: 212wdseqw23red232r3rds23r21212hg #填够32位复制代码

发送登录请求

7、用户授权

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

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

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

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

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

Security内置的权限注解:

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

  1. @RestController
  2. @RequestMapping("/sys-user")
  3. 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、完善权限方法

  1. /*
  2. * 获取用户权限信息(角色,菜单权限)
  3. * */
  4. 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当中

  1. @Service
  2. 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; }

}
复制代码

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <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

  1. /**
  2. * <p>
  3. * 服务实现类
  4. * </p>
  5. * @author itbluebox
  6. * @since 2022-05-26
  7. */
  8. @Service
  9. 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/369691
推荐阅读
相关标签
  

闽ICP备14008679号