如果观看的朋友不太了解Vue的话我建议你可以学习一下Vue框架,如果你没有太多时间的话,可以参考我如下文章,不懂的可以给发信息,应该能解决问题。
Vue学习笔记:我个人不太建议看这篇文章,可以自己去找文章看看,我这篇文章我感觉写得有点乱,我建议跳过前面四部分,直接看后面。
Vue项目笔记:如果你已经学习了Vue框架,我建议你看看这篇文章,真的是保姆级别的教程,当然这篇文章我是建议各位看的。
本项目是从公众号:Java问答社上获取的一个项目,大概组成部分如下,因为上面一篇Vue项目是教你手把手的写一个前端项目,虽然只是其中的几个页面的编写,但是你可以有个大概的了解,不然你很吃力的。
【我之前也不理解的建议】作为小白,这时候用VScode启动前端了,再启动后端的可能会后端报错,端口号被占用的问题,这时候不要去盲目的去改变后端的yaml文件的端口号,这个端口号和前端的配置文件是绑定的,随便改动是会连接不了的。
本次的操作很简单,根据前端页面样式写后端逻辑和数据库的设计问题。仅仅是想练习一下,复习一下,不是作为企业家的项目。前端就不去写了,有需要理解分析的时候再去分析学习。
好了,接下来可以用idea打开创建的VueAndSpringBoot项目了,然后创建我们的项目。
一般在公司开发就会在创建好的项目下面再创建我们的模块,但是这里就下面不在细分了,简单一点
<?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>
总体的其实就是这个三角关系页面展示的永远是public下面的index.html文件,App.vue是主组件,它是最重要的,main.js控制App.vue把相应得组件加载进来给App.vue,在index.html里面显示。
在上面的表单中输入数据后,我们的程序会有如下响应
你在页面内输入内容后,也会同时绑定过来,相当于我们的loginFrom对象里面的数据就绑定为你输入的数据了,这个model都是都是这样的。
这个captchaImg就是我们的一个验证码的数据,这个数据是从哪里获得呢
这个created就是在我们刷新页面了,它就会把这个内容填充起来
下面这个方法就是请求后端的创建验证码的一个请求,后端会创建验证码图片内的数据,返回给前端,前端通过这个captchaImg绑定到我们的验证码图片上。
其实很好理解:首先每次我们打开我们的登录页面,就这个写在created里面的内容就会被触发,第二这个created里面的方法getCaptcha就会被触发,触发后这个就是一个请求了,第三,后端收到了这个请求了,就会创建一个验证码,返回给前端,第四,前端得到数据后,就会通过captchaImg双向绑定到页面上,我们就能看见了
总结一下:首先我们前端得到了验证码后,然后你就要输入用户名和密码,验证码内容,然后点提交。第二,点了提交后,我们的submitForm方法就会触发,它触发后就会发起请求,然后返回我们的jwt内容,第三,前端得到了jwt后,我们就要保存起来,以便后续使用。
上面分析的情况都是我们不存在错误的情况,所以就必须要处理一下,可以在后端处理,但是axios里面也可以处理拦截。
这个axios配置:返回结果
对前端逻辑有个大概了解后,接下来我们编写后端内容
<!--整合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
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();
}
}
像这总返回的数据,只有我们自己能看懂,要前端能看懂是比较难的,它能看懂就是你返回的code编码,到底是200还是401什么的,前后端分离的情况下,那就要做到我们的统一的数据返回给前端,不管你需要返回什么数据,都是封装在一个类里面,那就很方便,前端没有后端基础的人不要紧,后端不知道前端的也不要紧,反正把这个反给你了,你自己看着办
首先分析一下,前端只要是正确的请求,那我们就必须给与回应,具体分析可以包括哪些呢?
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;
}
}
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());
}
}
上面我们捕捉了几个异常:
首先来理解我们的SpringSecurity框架的验证流程
当然我此篇文章学习的内容也没有那么的多,所以肯定不会写那么多过滤器。
是江南一点雨的作者画的(引用与java问答社)
可以参考此篇文章:https://blog.csdn.net/u012702547/article/details/89629415?
由于这一部分太多,所以独立出来
<!-- 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>
首先理解为什么要使用Redis,原因在于我们需要使用图片验证码什么的,后续采用的token都是要返回给前端,前端要保存起来,第二次请求,所以要配置我们的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);
}
}
@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;
}
}
提前了解点概念
这个也要先来了解一些概念:单点登录需要提前了解的概念
我们的用户认证问题,分为如下两种
分析一下逻辑
我们SpringSecurity提供的UsernamePasswordAuthenticationFilter过滤器是无法完成图片的认证的,这一点看起来是不灵活的,但是可以另外灵活的使用,可以在完成UsernamePasswordAuthenticationFilter之前添加一个图片CaptchaFilter过滤器,提前验证我们的验证码,然后再完成密码和用户名的验证,登录成功与否交给Handler处理。
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;
}
}
大小都是可以自定义的,随便自己怎么设置
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";
}
我们的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);
}
}
首先来分析一个问题
这里我们难道不应该是直接采用方法(‘/login’,this.loginForm)即可嘛,为什么要去调用了qs.stringify(‘/login’,this.loginForm)呢?
原因是我们之前在后端里面配置的原因是form表单的形式,而前端像前面种配置的话就提交的是JSON格式,所以就会有一个差别,所以就要调用qs.的原因
现在点提交的话是会有问题的
返回了一个页面给我们,现在也没有对错误页面处理,它没有权限访问错误页面,所以它会自己返回一个token给你。所以这里去处理错误类。
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);
}
}
如果各位跟我一样是直接使用开发好的前端,这里会报错的,不管怎么填都是会报验证码错误,而且是报如下的错误2
由于之前已经导入过JWT的依赖,这里就不管了,直接写类就OK了
@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());//过期时间是否在此之前
}
}
之前也发现自己在调试方面有些不足,今天也学习到了一些记录一下,采用postman测试
本次整合也就完成了
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);
}
}
为什么这里又采用bean的方式配置了呢?原因在于我们自己重写了构造方法,不能根据继承得到的解析。