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

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

一、概述与前期打杂工作

1、本项目概述

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

  • 首先从git上拉取项目下来:https://github.com/markerhub/vueadmin
  • 第二步解决前端

  • 处理数据库
  • 处理后端

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

2、项目练习定位

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

二、着手开发

(一)创建项目

1、首先理解目录结构

  • 为了方便查看别人的源码,我这次如下进行,了解目录结构

  • 把我们的vueadmin-java拷贝到刚刚创建在上级目录的VueAndSpringBoot项目里面

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

2、创建我们的项目

(1)创建Maven项目


一般在公司开发就会在创建好的项目下面再创建我们的模块,但是这里就下面不在细分了,简单一点

(2)导入依赖



<?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框架分析


总体的其实就是这个三角关系页面展示的永远是public下面的index.html文件,App.vue是主组件,它是最重要的,main.js控制App.vue把相应得组件加载进来给App.vue,在index.html里面显示。

2、前端的登录模块

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

在上面的表单中输入数据后,我们的程序会有如下响应

你在页面内输入内容后,也会同时绑定过来,相当于我们的loginFrom对象里面的数据就绑定为你输入的数据了,这个model都是都是这样的。

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

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



这个captchaImg就是我们的一个验证码的数据,这个数据是从哪里获得呢

这个created就是在我们刷新页面了,它就会把这个内容填充起来

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

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

(3)分析登录方法

  • 分析一下验证过程

    这个随机码,在不是前后端分离的情况下,是采用的是session实现的
  • 后端发过来的随机码,就是token如下绑定过程

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

(4)axios前端的拦截分析

  • 上面分析的情况都是我们不存在错误的情况,所以就必须要处理一下,可以在后端处理,但是axios里面也可以处理拦截。

    这个axios配置:返回结果

    • 如果是正常返回,交给你验证
    • 如果是错误的,就比如你后端空指针异常,数据库什么的错误,还是什么返回过程中的错误就下面判断。
  • 然后引入应用

3、后端处理登录模块

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

(1)数据库的连接






(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=111mybatis-plus.mapper-locations=classpath*:/mapper/**Mapper.xml

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

(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 {/*** <p>* 读取控制台内容* </p>*/public static String scanner(String tip) {Scanner scanner = new Scanner(System.in);StringBuilder help = new StringBuilder();help.append("请输入" + tip + ":");System.out.println(help.toString());if (scanner.hasNext()) {String ipt = scanner.next();if (StringUtils.isNotBlank(ipt)) {return ipt;}}throw new MybatisPlusException("请输入正确的" + tip + "!");}public static void main(String[] args) {// 代码生成器AutoGenerator mpg = new AutoGenerator();// 全局配置GlobalConfig gc = new GlobalConfig();String projectPath = System.getProperty("user.dir");gc.setOutputDir(projectPath + "/src/main/java");gc.setAuthor("确定了就这样");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() {@Overridepublic void initMap() {// to do nothing}};// 如果模板引擎是 freemarkerString templatePath = "/templates/mapper.xml.ftl";// 如果模板引擎是 velocity
//         String templatePath = "/templates/mapper.xml.vm";// 自定义输出配置List<FileOutConfig> focList = new ArrayList<>();// 自定义配置会被优先输出focList.add(new FileOutConfig(templatePath) {@Overridepublic String outputFile(TableInfo tableInfo) {// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;}});/*cfg.setFileCreate(new IFileCreate() {@Overridepublic boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {// 判断自定义文件夹是否需要创建checkDir("调用默认方法创建的目录,自定义目录用");if (fileType == FileType.MAPPER) {// 已经生成 mapper 文件判断存在,不想重新生成返回 falsereturn !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();}}
  • 然后启动本类,输入表明
  • 发现什么都没有

    去本地磁盘看一下(发现是存在的,复制到里面来)

(5)编写测试类


像这总返回的数据,只有我们自己能看懂,要前端能看懂是比较难的,它能看懂就是你返回的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;}}


(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框架的验证流程

    当然我此篇文章学习的内容也没有那么的多,所以肯定不会写那么多过滤器。
    是江南一点雨的作者画的(引用与java问答社)

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

  • 来看一些java问答社的理解思想分析

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


  • 可以参考一篇文章: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,复制控制台下面输入的密码登录了】
  • 发现会在控制台有输出一部分其他信息,这个可以不用管,之后会详细的说到,我们只需要在配置文件中配置如下的内容即可。

(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 {@Autowiredprivate 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 {@BeanRedisTemplate 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处理。

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

  • 这里需要需要使用到Google的一个验证码生成器,之前已经导入过
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 {@Beanpublic 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

    • 在我们的BaseController里面可以注入一些公用的注解。
  • 生成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{@AutowiredProducer producer;@AutowiredRedisUtil 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";
}
  • 启动一下,我们试一试启动

    发现还是加载不出来,去对比前端和后端的请求

    再次启动还是不行,查看报错如下。原来是跨域的问题


    而我们后端是配置的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;}@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", buildConfig());return new CorsFilter(source);}@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*")
//          .allowCredentials(true).allowedMethods("GET", "POST", "DELETE", "PUT").maxAge(3600);}}
  • 再次对核心进行配置

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

  • 首先来分析一个问题

    这里我们难道不应该是直接采用方法(‘/login’,this.loginForm)即可嘛,为什么要去调用了qs.stringify(‘/login’,this.loginForm)呢?
    原因是我们之前在后端里面配置的原因是form表单的形式,而前端像前面种配置的话就提交的是JSON格式,所以就会有一个差别,所以就要调用qs.的原因

  • 现在点提交的话是会有问题的

    返回了一个页面给我们,现在也没有对错误页面处理,它没有权限访问错误页面,所以它会自己返回一个token给你。所以这里去处理错误类。

  • 登录失败类


  • 同样的登录成功类

(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 {@AutowiredRedisUtil redisUtil;@AutowiredLoginFailureHandler loginFailureHandler;@Overrideprotected 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");//之前存的tokenif (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {//如果你验证码为空,或者key为空,就直接有问题,然后直接抛出自己定义的异常throw new CaptchaException("验证码错误");}//否则要从Redis里面获取codeif (!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);}
}


再次重启,然后尝试登录就会发现有了提示验证码错误

如果各位跟我一样是直接使用开发好的前端,这里会报错的,不管怎么填都是会报验证码错误,而且是报如下的错误2

(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());//过期时间是否在此之前}
}
  • 填写配置文件

    看清楚它必须是32字节哦。

  • 获取JWT返回给钱端

    通过这个请求头setHeader返回给前端,前端也是通过authorization保存期起来。

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

    • 这里可以创建一个测试组
    • 在这个测试组里面创建测试的请求
    • 我们除了主要的请求页面外,还可以在这个请求之前发送一个请求
  • 本次整合也就完成了


(9)自定义jwt过滤器

  • 每一次的请求都会携带jwt,通过这个axios.js里面配置
  • 我们现在要编写完成自动登录的过程
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 {@AutowiredJWTUtils jwtUtils;public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}//因为我们要进行过滤的流程,所以重写如下方法@Overrideprotected 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);}
}


为什么这里又采用bean的方式配置了呢?原因在于我们自己重写了构造方法,不能根据继承得到的解析。

  • 到这里来梳理一下逻辑

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

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







其实这个和登录失败差不多的,所以直接拷贝,修改状态码即可。

(11)实现数据库的认证

  • 看图咱得先去编写UserDetailsService的实现类

    这个AccountUser是我们编写一个如下的类,让他去实现我们UserDetailsService接口,这样我们就实现数据库的认证了,不用再到yaml文件里面配置了
  • 所以删除如下
  • 还有配置如下

Vue+Springboot项目练手(主要是后端)相关推荐

  1. 教你如何制作vue+springboot项目

    前言 最近刚刚做了一个基于vue+springboot的学生成绩管理系统,于是基于这点,对遇到的一些问题进行一些配置的汇总.以及vue+springboot项目是怎么实现的,以下将贴出vue和spri ...

  2. Springboot简单练手的记账本

    Springboot简单练手的记账本 昨天看雷哥的教程写了个简单的记账本练练手,没有把笔记整理下来放在博客上,今天补上.言归正传,进入正题. 老规矩,我们还是先看看项目的目录结构,以及登陆界面 每个包 ...

  3. 初学 C 语言没有项目练手?这 20 个小项目拿走不谢~

    C 语言是大多数人的编程入门语言,但很多初学者在学习的过程中难免会出现一些迷茫,比如:不知道 C 语言可以开发哪些项目,可以应用在哪些实际的开发中-- 今天我们收集了 20 个 C 语言练手项目,提供 ...

  4. c语言api文档_初学 C 语言没有项目练手?这 20 个小项目拿走不谢

    C 语言是大多数人的编程入门语言,但很多初学者在学习的过程中难免会出现一些迷茫,比如:不知道 C 语言可以开发哪些项目,可以应用在哪些实际的开发中--今天我们收集了 20 个 C 语言练手项目,提供了 ...

  5. c语言倒计时不影响进程_初学C语言没有项目练手怎么行,这17个小项目收下不谢...

    image C语言是我们大多数人的编程入门语言,对其也再熟悉不过了,不过很多初学者在学习的过程中难免会出现迷茫,比如:不知道C语言可以开发哪些项目,可以应用在哪些实际的开发中--,这些迷茫也导致了我们 ...

  6. 在腾讯云服务器跑Vue + SpringBoot项目

    背景:闲来无事,跟着做了个Vue+SpringBoot项目,做了一些之后项目内容没什么思路搞什么了,然后就想着再搭个服务器. 一:项目及云服务器还有域名准备 项目:这个-自己准备吧,只要自己做好的能在 ...

  7. 机器学习没有项目练手?黄博邀您参加天池视觉AI比赛了!还有比赛奖金等你来拿...

    机器学习需要实战,没有项目练手?黄博邀您参加天池视觉AI比赛了!还有奖金哦!百万奖金等你来拿! 2019中国国际智能产业博览会上,重庆市大数据应用发展管理局.重庆市江津区人民政府联合阿里云共同启动首届 ...

  8. 学好Java去哪里找项目练手?

    学好Java去哪里找项目练手? 去那些培训机构的官网找项目视频,自己照着做一遍,不过一般能发出来的都不是最新的,但是肯定是有帮助的,当然这些项目做了你的项目经验也是虚假的,要想要真实的项目经验,那就得 ...

  9. 一个springboot的练手小项目

    文章目录 前言 一.项目需求分析 二.具体实现 三. 技术支持 4.程序功能图 前言   这几天完成了一个基于springboot.mybatis.thymeleaf的ems小项目,主要实现后端的登陆 ...

最新文章

  1. 【分享】博士生提高科研幸福感的途径
  2. anaconda mac安装
  3. webpack4.x加vue模板文件简单还原vue-cli
  4. windows.h与winsock2.h的包含顺序
  5. Android之提示Failed to load WebView provider: No WebView installed
  6. LeetCode 1186. 删除一次得到子数组最大和(DP)
  7. 透明大页相关内核参数_透明大内存页Hugepage支持
  8. Android 打开蓝牙流程
  9. python网络编程01/网络协议
  10. APICS与AX的Master Planning(一)--Phantom bill of Material 虚项
  11. 国密算法使用-SM3
  12. 分享淘宝利器飞天侠4.1至尊商业版 去除域名限制 绕过淘宝API直接采集
  13. [软件工程] 总体设计(概要设计或初步设计)
  14. 2022-02-23 安卓开发七年面试题总结
  15. 关于lombok和mapstruct整合报无参构造函数错误
  16. Navicat for mysql 在WIN10下导入SQL不成功解决办法
  17. 如何修改视频尺寸而不让画面变形?
  18. 微信小程序05---聊天室的搭建
  19. html5 php整站源码下载,HTML5响应式简洁企业织梦模板整站源码 v5.7
  20. 高斯过程的matlab程序实现及其参数优化

热门文章

  1. 数据基础---《利用Python进行数据分析·第2版》第7章 数据清洗和准备
  2. Linux内核中获取纳秒时间戳的方法
  3. Shiro框架学习笔记、整合Springboot、redis缓存
  4. GMK4045-ASEMI光伏逆变器二极管GMK4045
  5. 李奇霖:通道业务山穷水尽 券商资管何去何从?
  6. 企业应如何运用ERP系统的BOM表?
  7. 独木舟上的旅行-OJ
  8. 4月20日第壹简报,星期四,农历三月初一,谷雨
  9. 【无标题】Ds1302驱动代码编写并在Lcd1602液晶显示
  10. win10计算机亮度无法调节,win10电脑调不了亮度怎么办?教你win10电脑调不了亮度处理方法...