Vue+Springboot项目练手(主要是后端)

如果观看的朋友不太了解Vue的话我建议你可以学习一下Vue框架,如果你没有太多时间的话,可以参考我如下文章,不懂的可以给发信息,应该能解决问题。

Vue学习笔记:我个人不太建议看这篇文章,可以自己去找文章看看,我这篇文章我感觉写得有点乱,我建议跳过前面四部分,直接看后面。
Vue项目笔记:如果你已经学习了Vue框架,我建议你看看这篇文章,真的是保姆级别的教程,当然这篇文章我是建议各位看的。

一、概述与前期打杂工作

1、本项目概述

本项目是从公众号:Java问答社上获取的一个项目,大概组成部分如下,因为上面一篇Vue项目是教你手把手的写一个前端项目,虽然只是其中的几个页面的编写,但是你可以有个大概的了解,不然你很吃力的。
Vue+Springboot项目练手(主要是后端)_第1张图片

  • 首先从git上拉取项目下来:https://github.com/markerhub/vueadmin
    在这里插入图片描述
  • 第二步解决前端
    Vue+Springboot项目练手(主要是后端)_第2张图片
    Vue+Springboot项目练手(主要是后端)_第3张图片
  • 处理数据库
    Vue+Springboot项目练手(主要是后端)_第4张图片
  • 处理后端
    在这里插入图片描述
    Vue+Springboot项目练手(主要是后端)_第5张图片

【我之前也不理解的建议】作为小白,这时候用VScode启动前端了,再启动后端的可能会后端报错,端口号被占用的问题,这时候不要去盲目的去改变后端的yaml文件的端口号,这个端口号和前端的配置文件是绑定的,随便改动是会连接不了的。

2、项目练习定位

本次的操作很简单,根据前端页面样式写后端逻辑和数据库的设计问题。仅仅是想练习一下,复习一下,不是作为企业家的项目。前端就不去写了,有需要理解分析的时候再去分析学习。

二、着手开发

(一)创建项目

1、首先理解目录结构

  • 为了方便查看别人的源码,我这次如下进行,了解目录结构
    Vue+Springboot项目练手(主要是后端)_第6张图片
    Vue+Springboot项目练手(主要是后端)_第7张图片
  • 把我们的vueadmin-java拷贝到刚刚创建在上级目录的VueAndSpringBoot项目里面
    Vue+Springboot项目练手(主要是后端)_第8张图片

好了,接下来可以用idea打开创建的VueAndSpringBoot项目了,然后创建我们的项目。

2、创建我们的项目

(1)创建Maven项目

Vue+Springboot项目练手(主要是后端)_第9张图片
一般在公司开发就会在创建好的项目下面再创建我们的模块,但是这里就下面不在细分了,简单一点
Vue+Springboot项目练手(主要是后端)_第10张图片

(2)导入依赖

Vue+Springboot项目练手(主要是后端)_第11张图片
Vue+Springboot项目练手(主要是后端)_第12张图片
Vue+Springboot项目练手(主要是后端)_第13张图片

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!--<parent>
        <artifactId>VueAndSpringBoot</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>-->
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.mldn.vueandspringboot</groupId>
    <artifactId>vueandspringboot</artifactId>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

</project>

(二)分析登录模块

1、Vue框架分析

Vue+Springboot项目练手(主要是后端)_第14张图片
总体的其实就是这个三角关系页面展示的永远是public下面的index.html文件,App.vue是主组件,它是最重要的,main.js控制App.vue把相应得组件加载进来给App.vue,在index.html里面显示。

2、前端的登录模块

Vue+Springboot项目练手(主要是后端)_第15张图片

(1)先理解Vue的数据双向绑定

在上面的表单中输入数据后,我们的程序会有如下响应
Vue+Springboot项目练手(主要是后端)_第16张图片
你在页面内输入内容后,也会同时绑定过来,相当于我们的loginFrom对象里面的数据就绑定为你输入的数据了,这个model都是都是这样的。

  • 这里的知识点就是 :model语法,它就是实现数据的绑定的

(2)分析验证码的请求过程

Vue+Springboot项目练手(主要是后端)_第17张图片
Vue+Springboot项目练手(主要是后端)_第18张图片
这个captchaImg就是我们的一个验证码的数据,这个数据是从哪里获得呢
Vue+Springboot项目练手(主要是后端)_第19张图片
这个created就是在我们刷新页面了,它就会把这个内容填充起来

下面这个方法就是请求后端的创建验证码的一个请求,后端会创建验证码图片内的数据,返回给前端,前端通过这个captchaImg绑定到我们的验证码图片上。Vue+Springboot项目练手(主要是后端)_第20张图片
其实很好理解:首先每次我们打开我们的登录页面,就这个写在created里面的内容就会被触发,第二这个created里面的方法getCaptcha就会被触发,触发后这个就是一个请求了,第三,后端收到了这个请求了,就会创建一个验证码,返回给前端,第四,前端得到数据后,就会通过captchaImg双向绑定到页面上,我们就能看见了

  • 如果对验证码还是不是很理解,就去看我主页的分类中的代码复用模块,里面的验证码复用
  • 这部分的知识:其实就是事件的学习,也是和js里面的@XXX事件差不多的理解

(3)分析登录方法

  • 分析一下验证过程
    Vue+Springboot项目练手(主要是后端)_第21张图片
    这个随机码,在不是前后端分离的情况下,是采用的是session实现的
  • 后端发过来的随机码,就是token如下绑定过程
    Vue+Springboot项目练手(主要是后端)_第22张图片
    Vue+Springboot项目练手(主要是后端)_第23张图片

总结一下:首先我们前端得到了验证码后,然后你就要输入用户名和密码,验证码内容,然后点提交。第二,点了提交后,我们的submitForm方法就会触发,它触发后就会发起请求,然后返回我们的jwt内容,第三,前端得到了jwt后,我们就要保存起来,以便后续使用。

(4)axios前端的拦截分析

  • 上面分析的情况都是我们不存在错误的情况,所以就必须要处理一下,可以在后端处理,但是axios里面也可以处理拦截。
    Vue+Springboot项目练手(主要是后端)_第24张图片
    这个axios配置:返回结果

    • 如果是正常返回,交给你验证
    • 如果是错误的,就比如你后端空指针异常,数据库什么的错误,还是什么返回过程中的错误就下面判断。
  • 然后引入应用
    Vue+Springboot项目练手(主要是后端)_第25张图片

3、后端处理登录模块

对前端逻辑有个大概了解后,接下来我们编写后端内容

(1)数据库的连接

Vue+Springboot项目练手(主要是后端)_第26张图片
Vue+Springboot项目练手(主要是后端)_第27张图片
Vue+Springboot项目练手(主要是后端)_第28张图片
Vue+Springboot项目练手(主要是后端)_第29张图片
Vue+Springboot项目练手(主要是后端)_第30张图片
Vue+Springboot项目练手(主要是后端)_第31张图片

(2)编写配置文件并导入依赖

<!--整合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>
server.port=8081
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=111

mybatis-plus.mapper-locations=classpath*:/mapper/**Mapper.xml

Vue+Springboot项目练手(主要是后端)_第32张图片

(3)编写mybatisplus配置文件和开启接口扫描

Vue+Springboot项目练手(主要是后端)_第33张图片

(4)编写我们的类生成器

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 {

    /**
     * 

* 读取控制台内容 *

*/
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("确定了就这样"); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 gc.setServiceName("%sService"); mpg.setGlobalConfig(gc); //这一部分其实在这里不用配置了,因为我刚才已经写过了 // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/vueadmin?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"); // dsc.setSchemaName("public"); dsc.setDriverName("com.mysql.cj.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("111"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); // pc.setModuleName(scanner("模块名")); pc.setParent("cn.mldn.vueadnspringboot"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setSuperEntityClass("BaseEntity"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 strategy.setSuperControllerClass("BaseController"); // 写于父类中的公共字段 strategy.setSuperEntityColumns("id", "created", "updated", "statu"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); // strategy.setTablePrefix("sys_");//动态调整 mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }
  • 然后启动本类,输入表明
  • 发现什么都没有
    Vue+Springboot项目练手(主要是后端)_第34张图片
    去本地磁盘看一下(发现是存在的,复制到里面来)
    Vue+Springboot项目练手(主要是后端)_第35张图片
    Vue+Springboot项目练手(主要是后端)_第36张图片

(5)编写测试类

Vue+Springboot项目练手(主要是后端)_第37张图片
Vue+Springboot项目练手(主要是后端)_第38张图片

像这总返回的数据,只有我们自己能看懂,要前端能看懂是比较难的,它能看懂就是你返回的code编码,到底是200还是401什么的,前后端分离的情况下,那就要做到我们的统一的数据返回给前端,不管你需要返回什么数据,都是封装在一个类里面,那就很方便,前端没有后端基础的人不要紧,后端不知道前端的也不要紧,反正把这个反给你了,你自己看着办

(6)统一返回结果编写

首先分析一下,前端只要是正确的请求,那我们就必须给与回应,具体分析可以包括哪些呢?

  • 首先要code吧:到底是成功了,还是失败了,要返回给前端
  • 第二要返回的是结果消息
  • 第三要返回的是结果数据
package cn.mldn.vueandspringboot.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 succ(Object data) {
		return succ(200, "操作成功", data);
	}

	public static Result succ(int code, String msg, Object data) {
		Result r = new Result();
		r.setCode(code);
		r.setMsg(msg);
		r.setData(data);
		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.setCode(code);
		r.setMsg(msg);
		r.setData(data);
		return r;
	}

}

Vue+Springboot项目练手(主要是后端)_第39张图片
Vue+Springboot项目练手(主要是后端)_第40张图片

(7)全局异常处理

  • 有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。
  • 处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。步骤二、定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。
package cn.mldn.vueandspringboot.config.common;

import cn.mldn.vueandspringboot.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;


import java.nio.file.AccessDeniedException;

@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());
    }
}

上面我们捕捉了几个异常:

  • ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
  • IllegalArgumentException:处理Assert的异常
  • MethodArgumentNotValidException:处理实体校验的异常
  • RuntimeException:捕捉其他异常
  • (同样的,如果有其他错误,也可以进行这样编写,在我们的程序里面会自动捕获到这些错误的)

(8)分析整合SpringSecurity框架(总体项目的重点)

  • 首先来理解我们的SpringSecurity框架的验证流程
    Vue+Springboot项目练手(主要是后端)_第41张图片
    当然我此篇文章学习的内容也没有那么的多,所以肯定不会写那么多过滤器。
    是江南一点雨的作者画的(引用与java问答社)

  • 可以参考此篇文章:https://blog.csdn.net/u012702547/article/details/89629415?

  • 来看一些java问答社的理解思想分析
    Vue+Springboot项目练手(主要是后端)_第42张图片
    Vue+Springboot项目练手(主要是后端)_第43张图片

    • UsernamePasswordAuthtionFilter过滤器就是我们重点要考虑的过滤器了

Vue+Springboot项目练手(主要是后端)_第44张图片
Vue+Springboot项目练手(主要是后端)_第45张图片

  • 可以参考一篇文章:https://blog.csdn.net/qq_35067322/article/details/102690579

4、整合SpringSecurity(重点)

(1)整合springsecurity和jwt

由于这一部分太多,所以独立出来

  • 首先导入security与Jwt依赖(不知道的话可以看这篇文章:https://blog.csdn.net/weixin_46635575/article/details/120516502)
<!-- 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>
  • 此时可以先启动我们的项目看看,看看到底是什么样子【样子如下,如果此时在浏览器输入我们的test测试请求,会发现登录从定向到另外一个登录页面,此时就可以用用户名user,复制控制台下面输入的密码登录了】
    Vue+Springboot项目练手(主要是后端)_第46张图片
  • 发现会在控制台有输出一部分其他信息,这个可以不用管,之后会详细的说到,我们只需要在配置文件中配置如下的内容即可。
    Vue+Springboot项目练手(主要是后端)_第47张图片

(2)配置Redis类编写

首先理解为什么要使用Redis,原因在于我们需要使用图片验证码什么的,后续采用的token都是要返回给前端,前端要保存起来,第二次请求,所以要配置我们的Redis。

  • 编写操作Redis的文件
package com.markerhub.utils;

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

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

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

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

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

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

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

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

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

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

    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}
  • 配置config
@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.setHashKeySerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
}

(3)用户认证分析

  • 提前了解点概念
    这个也要先来了解一些概念:单点登录需要提前了解的概念

  • 我们的用户认证问题,分为如下两种

    • 首次登录认证:用户名、密码和验证码完成登录
    • 二次token认证:请求头携带Jwt进行身份认证
  • 分析一下逻辑
    我们SpringSecurity提供的UsernamePasswordAuthenticationFilter过滤器是无法完成图片的认证的,这一点看起来是不灵活的,但是可以另外灵活的使用,可以在完成UsernamePasswordAuthenticationFilter之前添加一个图片CaptchaFilter过滤器,提前验证我们的验证码,然后再完成密码和用户名的验证,登录成功与否交给Handler处理。
    Vue+Springboot项目练手(主要是后端)_第48张图片
    Vue+Springboot项目练手(主要是后端)_第49张图片

(4)生成图片验证码和Key和Code

  • 这里需要需要使用到Google的一个验证码生成器,之前已经导入过
    Vue+Springboot项目练手(主要是后端)_第50张图片
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;
   }
}

大小都是可以自定义的,随便自己怎么设置

  • 然后编写Controller
    Vue+Springboot项目练手(主要是后端)_第51张图片

    • 在我们的BaseController里面可以注入一些公用的注解。
      Vue+Springboot项目练手(主要是后端)_第52张图片
  • 生成Key(一定不要倒错包了)

import cn.hutool.core.map.MapUtil;
import cn.mldn.vueandspringboot.lang.Const;
import cn.mldn.vueandspringboot.utils.RedisUtil;
import com.google.code.kaptcha.Producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.mldn.vueandspringboot.lang.Result;
import sun.misc.BASE64Encoder;

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

@Slf4j
@RestController
public class AuthController extends BaseController{
    @Autowired
    Producer producer;

    @Autowired
    RedisUtil redisUtil;

    @GetMapping("/captcha")
    public Result captcha() throws IOException {
        String key = UUID.randomUUID().toString();//生成需要使用的Key,
        String code = producer.createText();//也就是生成的图片验证码
        BufferedImage image = producer.createImage(code);//把验证码处理成image形式
        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());

        // 存储到redis中
        redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
        log.info("验证码 -- {} - {}", key, code);
        return Result.succ(
                MapUtil.builder()
                        .put("token", key)
                        .put("base64Img", base64Img)
                        .build()
        );
    }
}

  • 还有编写一个常量类
public class Const {
    public final static String CAPTCHA_KEY = "captcha";
}

  • 启动一下,我们试一试启动
    Vue+Springboot项目练手(主要是后端)_第53张图片
    发现还是加载不出来,去对比前端和后端的请求
    Vue+Springboot项目练手(主要是后端)_第54张图片
    再次启动还是不行,查看报错如下。原来是跨域的问题
    Vue+Springboot项目练手(主要是后端)_第55张图片
    Vue+Springboot项目练手(主要是后端)_第56张图片
    而我们后端是配置的8081,所以问题就出现在了这里。

(5)配置跨域

我们的Springboot项目解决跨域问题,需要两步

  • 首选要编写跨域配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@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);
    }

}

  • 再次对核心进行配置
    Vue+Springboot项目练手(主要是后端)_第57张图片
    Vue+Springboot项目练手(主要是后端)_第58张图片

(6)登录失败和成功处理器

  • 首先来分析一个问题
    Vue+Springboot项目练手(主要是后端)_第59张图片
    这里我们难道不应该是直接采用方法(‘/login’,this.loginForm)即可嘛,为什么要去调用了qs.stringify(‘/login’,this.loginForm)呢?
    原因是我们之前在后端里面配置的原因是form表单的形式,而前端像前面种配置的话就提交的是JSON格式,所以就会有一个差别,所以就要调用qs.的原因
    Vue+Springboot项目练手(主要是后端)_第60张图片

  • 现在点提交的话是会有问题的
    Vue+Springboot项目练手(主要是后端)_第61张图片
    返回了一个页面给我们,现在也没有对错误页面处理,它没有权限访问错误页面,所以它会自己返回一个token给你。所以这里去处理错误类。

  • 登录失败类
    Vue+Springboot项目练手(主要是后端)_第62张图片
    Vue+Springboot项目练手(主要是后端)_第63张图片
    Vue+Springboot项目练手(主要是后端)_第64张图片

  • 同样的登录成功类
    Vue+Springboot项目练手(主要是后端)_第65张图片

(7)图片验证码处理器

package cn.mldn.vueandspringboot.security;

import cn.mldn.vueandspringboot.config.common.exception.CaptchaException;
import cn.mldn.vueandspringboot.lang.Const;
import cn.mldn.vueandspringboot.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
    RedisUtil redisUtil;

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //并不是所有的URL都需要拦截,
        String url = httpServletRequest.getRequestURI();

        if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST") ) {
            //效验验证码
            try {
                validate(httpServletRequest);
            } catch (CaptchaException e) {
                //如果不正确就跳转到错误处理器里面去
                loginFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
            }

        }

        //否则就继续走下去
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

    private void validate(HttpServletRequest httpServletRequest) throws CaptchaException {

        String code = httpServletRequest.getParameter("code");//之前在登录里面处理code,
        String key = httpServletRequest.getParameter("token");//之前存的token

        if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
            //如果你验证码为空,或者key为空,就直接有问题,然后直接抛出自己定义的异常
            throw new CaptchaException("验证码错误");
        }

        //否则要从Redis里面获取code
        if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,key))) {
            throw new CaptchaException("验证码错误");
        }

        redisUtil.hdel(Const.CAPTCHA_KEY,key);
    }
}

import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;



public class CaptchaException extends AuthenticationException {

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


Vue+Springboot项目练手(主要是后端)_第66张图片
再次重启,然后尝试登录就会发现有了提示验证码错误

如果各位跟我一样是直接使用开发好的前端,这里会报错的,不管怎么填都是会报验证码错误,而且是报如下的错误2
Vue+Springboot项目练手(主要是后端)_第67张图片
Vue+Springboot项目练手(主要是后端)_第68张图片

(8)整合JWT

由于之前已经导入过JWT的依赖,这里就不管了,直接写类就OK了

  • 这部分建议自己写,而且理解,不知道的话,可以去找教程,我记得我也写过JWT的文章可以参考,要弄懂了。

@Data
@Component
@ConfigurationProperties(prefix = "mydemo.jwt")
public class JWTUtils {

    /**
     * 生成JWT的方法,由于生成的是一个字符串所以返回值是string
     * @param username
     * @return
     */

    private long expire;
    private String secret;//秘钥
     private String header;//给jwt起的别名

    public String generateToken(String username) {

        Date newDate = new Date();//生成时间
        Date expireDate = new Date(newDate.getTime() + 1000 *expire);

        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setSubject(username)//生成jwt给谁
                .setIssuedAt(newDate)//设置创建时间
                .setExpiration(expireDate)//设置过期时间
                .signWith(SignatureAlgorithm.ES512,secret)//设置秘钥
                .compact();
    }


    /**
     * 解析JWT的方法,由于是返回的是Body部分,所以肯定返回值就是Claims了
     * @param jwt
     * @return
     */
    public Claims getClaimByBody(String jwt) {
        //如果你的JWT被人故意修改的话,会有问题,这里会报错,所以这里要处理异常
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 判断jwt是否过期
     * @param claims
     * @return
     */
    public boolean isTokenExpired(Claims claims) {
        return claims.getExpiration().before(new Date());//过期时间是否在此之前
    }
}

  • 填写配置文件
    Vue+Springboot项目练手(主要是后端)_第69张图片
    看清楚它必须是32字节哦。

  • 获取JWT返回给钱端
    Vue+Springboot项目练手(主要是后端)_第70张图片
    通过这个请求头setHeader返回给前端,前端也是通过authorization保存期起来。
    Vue+Springboot项目练手(主要是后端)_第71张图片

  • 之前也发现自己在调试方面有些不足,今天也学习到了一些记录一下,采用postman测试

    • 这里可以创建一个测试组
      Vue+Springboot项目练手(主要是后端)_第72张图片
    • 在这个测试组里面创建测试的请求
      Vue+Springboot项目练手(主要是后端)_第73张图片
    • 我们除了主要的请求页面外,还可以在这个请求之前发送一个请求
      Vue+Springboot项目练手(主要是后端)_第74张图片
  • 本次整合也就完成了

Vue+Springboot项目练手(主要是后端)_第75张图片
Vue+Springboot项目练手(主要是后端)_第76张图片

(9)自定义jwt过滤器

  • 每一次的请求都会携带jwt,通过这个axios.js里面配置
    Vue+Springboot项目练手(主要是后端)_第77张图片
  • 我们现在要编写完成自动登录的过程
    Vue+Springboot项目练手(主要是后端)_第78张图片
import cn.hutool.core.util.StrUtil;
import cn.mldn.vueandspringboot.utils.JWTUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {


    @Autowired
    JWTUtils jwtUtils;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    //因为我们要进行过滤的流程,所以重写如下方法

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取JWT的信息
        String jwt = request.getHeader(jwtUtils.getHeader());
        //没有jwt的情况
        if (StrUtil.isBlankOrUndefined(jwt)) {
            chain.doFilter(request,response);
            return;//如果条件满足,就直接让他跳过,到达登录失败页面
        }
        //有jwt的情况
        Claims claims = jwtUtils.getClaimByBody(jwt);
        if (claims == null) {
            throw new JwtException("token异常");
        }
        if (jwtUtils.isTokenExpired(claims)) {
            throw new JwtException("token已过期");
        }

        String username =claims.getSubject();
        //这样就可以获取用户的一些信息,比如权限信息,这里就要放到如下里面
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,null,null);

        //设置上下文的主题
        SecurityContextHolder.getContext().setAuthentication(token);

        //让过滤连继续往下面走
        chain.doFilter(request,response);
    }
}

Vue+Springboot项目练手(主要是后端)_第79张图片
为什么这里又采用bean的方式配置了呢?原因在于我们自己重写了构造方法,不能根据继承得到的解析。
Vue+Springboot项目练手(主要是后端)_第80张图片

  • 到这里来梳理一下逻辑
    • 首先前端在发起登录请求的时候,加载页面后,就会刷新我们的登录页面,此时就会触发去请求后端验证码的的Controller,这里就会生成一个验证码,和一个对应的key存起来,存成一对Map的格式,然后放在Redis里面
    • 当用户发起了登录请求之后,此时后端首先经过CaptchaFilter这个过滤器,这个过滤器首先验证验证码是否正确,如果这里没什么问题,接着往下面走 。
      Vue+Springboot项目练手(主要是后端)_第81张图片
    • 验证码的关卡过了后,接下来就要进行JwtAuthenticationFilter 过滤器验证了,如果说这一关过了后,接下里再学到的时候总结。

(10)编写登录认证失败或权限不足的处理器

Vue+Springboot项目练手(主要是后端)_第82张图片
Vue+Springboot项目练手(主要是后端)_第83张图片
Vue+Springboot项目练手(主要是后端)_第84张图片
Vue+Springboot项目练手(主要是后端)_第85张图片
Vue+Springboot项目练手(主要是后端)_第86张图片
Vue+Springboot项目练手(主要是后端)_第87张图片
其实这个和登录失败差不多的,所以直接拷贝,修改状态码即可。

(11)实现数据库的认证

Vue+Springboot项目练手(主要是后端)_第88张图片

  • 看图咱得先去编写UserDetailsService的实现类
    Vue+Springboot项目练手(主要是后端)_第89张图片
    这个AccountUser是我们编写一个如下的类,让他去实现我们UserDetailsService接口,这样我们就实现数据库的认证了,不用再到yaml文件里面配置了
    Vue+Springboot项目练手(主要是后端)_第90张图片
  • 所以删除如下
    在这里插入图片描述
  • 还有配置如下
    Vue+Springboot项目练手(主要是后端)_第91张图片
    在这里插入图片描述

你可能感兴趣的:(#,前后端分离项目,前后端分离项目,vue框架,spring,boot)