文章目录
前言
上一章我们把项目运行起来了,首先进入的就是登录页面,来,就先看看这个验证码是怎么实现的。
点击验证码,网络请求会有两个请求
code请求
data:image/gif
显然,第一个请求是向后端发送一个验证码获取的请求;
第二个请求传来的是一个gif。
此处科普一下
- GIF的全称是Graphics Interchange Format,可译为图形交换格式,用于以超文本标志语言(Hypertext Markup Language)方式显示索引彩色图像,在因特网和其他在线服务系统上得到广泛应用。GIF是一种公用的图像文件格式标准,版权归Compu Serve公司所有*
看一下人家怎么写的按钮
<el-form-item prop="code" v-if="captchaOnOff">
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
我怎么就写不出来呢???
代码解读一下
<el-form-item prop="code" v-if="captchaOnOff">
<!-- el开头,是element UI 官网: https://element.eleme.cn/#/zh-CN/component/installation -->
<!-- code 是自己输入的验证码,此处先讲获取验证码
在 Vue 中,父子组件的关系可以总结为 props down, events up。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。
显然这个code是父组件通过prop传递数据给子组件的
-->
<!-- captchaOnOff 初始值为true,是验证码开关 -->
<el-input
v-model="loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<!-- 此处绑定了loginForm.code,这个code是表单对象里的code,不是上文说的code
auto-complete="off" 关闭代码自动补全,什么意思呢,把鼠标光标放到输入框,不会自动出现下拉框,如果改为on,则会出现近期输入的数据。
autocomplete 属性规定输入字段是否应该启用自动完成功能。默认情况下是启动的,也就是当你点击了input
获取焦点之后浏览器会自动将以前的输入记录作为填入选项显示出来。这个是HTML5中的新属性,在不支持HTML5的浏览器下是没有用的。
注释:autocomplete 属性适用于 <form>,以及下面的 <input> 类型:text, search, url, telephone, email, password, datepickers, range 以及 color。
在一部分浏览器中,autocomplete 属性是默认关闭的,如果需要打开就打开 一下。因为这是input自带的数学,会在自己的div上加了一层div的ul列表。
placeholder 属性提供可描述输入字段预期值的提示信息(hint)。
该提示会在输入字段为空时显示,并会在字段获得焦点时消失。
@keyup.enter 是 vue 监听键盘回车事件
如果是封装了组件,要用
@keyup.enter.native
最后就是,按下回车键,调用handleLogin方法
-->
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
<!-- Vue中slot的使用 https://www.cnblogs.com/qq735675958/p/8377761.html
是对组件的扩展,通过slot插槽向组件内部指定位置传递内容,通过slot可以父子传参
即 slot的出现是为了父组件可以堂而皇之地在子组件中加入内容。
<span slot=”header”>hello world</span>
<slot name=”header”>这是拥有命名的slot的默认内容</slot>
显然这个叫 prefix 的slot 就是上文提到过的gif
-->
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
<!-- 根据codeUrl显示验证码图片,点击这个img会调用 getCode方法 -->
</div>
</el-form-item>
一. 看一下前端逻辑
1. 登录页被创建时,会调用getCode()方法
2.看一下 getCode()
getCode() {
getCodeImg().then(res => {
this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
if (this.captchaOnOff) {
this.codeUrl = "data:image/gif;base64," + res.img;
this.loginForm.uuid = res.uuid;
}
});
}
解读
getCode() {
getCodeImg().then(res => {
// 会调用 getCodeImg()接口,把返回的res进行处理
this.captchaOnOff = res.captchaOnOff === undefined ? true : res.captchaOnOff;
//如果返回的captchaOnOff是undefined,估计是没访问出来啥东西,就返回true,否则返回false
// 什么鬼?仔细看看data里的captchaOnOff,嗷嗷,是验证码开关,初始值为true,所以这行的代码就是能不能继续 请求验证码 呗
//如果是true,说明没返回出来东西,肯定就能继续请求,如果是false,那就不能继续请求验证码
//再看一下这个在哪用captchaOnOff,哦,原来是 <el-form-item prop="code" v-if="captchaOnOff">
//显然,如果captchaOnOff为false就不会显示这个组件了
if (this.captchaOnOff) { //如果有验证码
this.codeUrl = "data:image/gif;base64," + res.img; //codeUrl的值即改为 data:image/gif;base64,res.img
//如果你不懂 base64 看看这个 https://blog.csdn.net/weixin_38465623/article/details/80199999
//当然,也可以看我总结的
/**
* base64 data:image/gif;base64," + res.img
* data表示取得数据的协定名称,image/gif 是数据类型名称,base64 是数据的编码方法,逗号后面就是这个image/gif文件base64编码后的数据。
* 目前,Data URI scheme支持的类型有:
data:,文本数据
data:text/plain,文本数据
data:text/html,HTML代码
data:text/html;base64,base64编码的HTML代码
data:text/css,CSS代码
data:text/css;base64,base64编码的CSS代码
data:text/javascript,Javascript代码
data:text/javascript;base64,base64编码的Javascript代码
data:image/gif;base64,base64编码的gif图片数据
data:image/png;base64,base64编码的png图片数据
data:image/jpeg;base64,base64编码的jpeg图片数据
base64简单地说,它把一些 8-bit 数据翻译成标准 ASCII 字符,网上有很多免费的base64 编码和解码的工具,在PHP中可以用函数base64_encode() 进行编码
举个图片的例子:
网页中一张图片可以这样显示:
<img src=“http://www.aimks.com/images/wg.png”/>
也可以这样显示
<img src=“data:image/png;base64,iVBORw0KGgoAAAANSUhEU
gAAAAEAAAAkCAYAAABIdFAMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZS
BJbWFnZVJlYWR5ccllPAAAAHhJREFUeNo8zjsOxCAMBFB/KEAUFFR
0Cbng3nQPw68ArZdAlOZppPFIBhH5EAB8b+Tlt9MYQ6i1BuqFaq1C
KSVcxZ2Acs6406KUgpt5/LCKuVgz5BDCSb13ZO99ZOdcZGvt4mJjz
MVKqcha68iIePB86GAiOv8CDADlIUQBs7MD3wAAAABJRU5ErkJggg%3D%3D”/>
*/
//显然 res.img就是传来的图片,在后端生成图片,base64图片转成字符 传输回前端,再在前端 字符转成图片 显示给用户 妙啊秒啊
this.loginForm.uuid = res.uuid;
//并且表单的uuid设置为res.uuid
}
});
}
getCode()方法第一行就调用了另一个方法getCodeImg()
来看看这又是何方神圣
3.看一下 getCodeImg()
好家伙,让我好找,这个方法不在methods里
在这里
还好我聪明,ctrl点 方法名 直接找到
// 获取验证码
export function getCodeImg() {
return request({
url: '/code',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}
没啥好解读的,导出一个方法 getCodeImg(),
这个方法调用了一个get请求 /code,请求头isToken值为false,不需要token,tineout为20000,这个参数怎么用后面再看。
二. 看一下后端逻辑
现在开始看后端了
上文前端发出了一个 /code的请求,我们在代码里和网络请求里都看到了,再看一下这个网络请求
后端如何接收这个 /code请求?
1.先进入RouterFunctionConfiguration类
前端所有的接口要访问后端,首先要走的就是网关
这里这个请求被 config 包下的 RouterFunctionConfiguration接收
解读一下
package com.ruoyi.gateway.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import com.ruoyi.gateway.handler.ValidateCodeHandler;
/**
* 路由配置信息
*
* @author ruoyi
*/
@Configuration
public class RouterFunctionConfiguration {
@Autowired // 注入一个 ValidateCodeHandler的对象,相当于Collectors中注入的service,是用来封装逻辑的
private ValidateCodeHandler validateCodeHandler;
@SuppressWarnings("rawtypes")
@Bean
public RouterFunction routerFunction() {
/** 参考 https://www.cnblogs.com/somefuture/p/15433565.html
* Spring框架给我们提供了两种http端点暴露方式来隐藏servlet原理:
* 一种是基于注解的形式@Controller或@RestController以及其他的注解如@RequestMapping、@GetMapping等等。
* 另外一种是基于路由配置RouterFunction和HandlerFunction的,称为“函数式WEB”。
*/
return RouterFunctions.route(
// 接收 /code 请求
/** Spring WebFlux 是一个异步非阻塞式的 Web 框架,它能够充分利用多核 CPU 的硬件资源去处理大量的并发请求。
* Spring Cloud Gateway将路由作为Spring WebFlux HandlerMapping基础架构的一部分进行匹配。
* Spring Cloud Gateway包括许多内置的路由断言工厂。所有这些断言都与HTTP请求的不同属性匹配。可以将多个路由断言工厂与逻辑and语句结合使用。
* 参考 https://www.cnblogs.com/h--d/p/12741901.html
*/
RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), //TEXT_PLAIN = new MediaType("text", "plain");
// 显然这行代码 会把code转发到 validateCodeHandler 中 去处理
// text/html的意思是将文件的content-type设置为text/html的形式,浏览器在获取到这种文件时会自动调用html的解析器对文件进行相应的处理。
//text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。
validateCodeHandler);
}
}
2.再跳到ValidateCodeFilter
显然,这是一个验证码过滤器,这里
非登录/注册请求或验证码关闭,不处理
所以放行
3.再跳到ValidateCodeHandler
直接上解读
package com.ruoyi.gateway.handler;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.ruoyi.common.core.exception.CaptchaException;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.gateway.service.ValidateCodeService;
import reactor.core.publisher.Mono;
/**
* 验证码获取
*
* @author ruoyi
*/
@Component
public class ValidateCodeHandler implements HandlerFunction<ServerResponse> {
@Autowired
private ValidateCodeService validateCodeService; //验证码服务对象
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest) {
// 响应式编程之Reactor的关于Flux和Mono概念
/**
* Flux类似RxJava的Observable,它可以触发零到多个事件,并根据实际情况结束处理或触发错误。
* Mono最多只触发一个事件
*/
AjaxResult ajax; //操作消息提醒,就是一个 统一标准响应体
try {
ajax = validateCodeService.createCapcha(); //创建一个验证码
} catch (CaptchaException | IOException e) {
return Mono.error(e);
}
//
return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(ajax));
}
}
最后生成的验证码,一块组成响应体传到前端了。
4.看一下ValidateCodeService.createCapcha()干的好事
原来是生成一个验证码,直接解读了
/**
* 生成验证码
*/
@Override
public AjaxResult createCapcha() throws IOException, CaptchaException {
AjaxResult ajax = AjaxResult.success(); //统一响应体
System.out.println("captchaProperties初始值----" + "captchaProperties.getType()---" + captchaProperties.getType() + "---captchaProperties.getEnabled()---" + captchaProperties.getEnabled());
//打印结果:captchaProperties初始值----captchaProperties.getType()---math---captchaProperties.getEnabled()---true
boolean captchaOnOff = captchaProperties.getEnabled(); //boolean的默认值为false
ajax.put("captchaOnOff", captchaOnOff); //这就是前端需要的验证码开关,返回默认值 false,那么前端就关闭验证码请求
if (!captchaOnOff) { //我至今不知道这段代码什么意思,我读的是 永远执行 ,因为没初始化,默认值为false
return ajax; //如果有童鞋找到了,请评论区指点迷津
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID(); //生成唯一UUID
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; //作为redis存储的键
String capStr = null, code = null; //验证码字符串,验证码
BufferedImage image = null;
String captchaType = captchaProperties.getType(); //验证码类型(math 数组计算 char 字符)一样,没找到在哪赋值的
// 生成验证码
if ("math".equals(captchaType)) { //math类型
String capText = captchaProducerMath.createText();
System.out.println("captchaProducerMath.createText()初始值---"+capText);
//打印结果:captchaProducerMath.createText()初始值---8-5=?@3
capStr = capText.substring(0, capText.lastIndexOf("@")); // lastIndexOf 返回指定字符在此字符串中最后一次出现处的索引
// captchaProducerMath.createText()是创建一个数学字符串 比如 8-5=?@3
// 字符串根据 @ 进行分割
// 要显示的图片为 @ 前面的字符串生成的图片
// 验证码应该为 @ 后面的字符串生成的字符
// 妙啊秒啊
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
//通过这个 数学验证码生产者 ,根据 capStr ,就是 8-5=? 给BufferedImage对象赋值
} else if ("char".equals(captchaType)) { //char类型
capStr = code = captchaProducer.createText();
/**
*captchaProducer.createImage(capStr)
* 生成图像,图像的样子是 capStr 的内容
* 是字符 转 图像
*/
image = captchaProducer.createImage(capStr);
}
/**
* 最终capStr就是根据验证码生成的编码
*/
redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES); //redis存储验证码,键为 captcha_codes+UUID 值为 验证码的值 ,Constants.CAPTCHA_EXPIRATION 是验证码存储的世时间,单位是分钟
// 转换流信息写出
/**
* 字节数组输出流 在内存中创建一个字节数组缓冲区,所有发送到输出流的数据保存在该字节数组缓冲区中。
* FastByteArrayOutputStream内部实现由一个LinkedList<byte[]>组成,每一次扩容中分配一个数组的空间,
* 并当该数据放入到List中。需要分配的数组长度为调用FastByteArrayOutputStream的write方法决定。
* 而ByteArrayOutputStream内部实现为一个数组每一次扩容需要重新分配空间并将数据复制到新数组中,
* 这就是FastByteArrayOutputStream比ByteArrayOutputStream主要区别。
*/
// 快速字节数组输出流
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try {
//
/**
* 使用支持给定格式的任意 ImageWriter 将一个图像写入 File。如果已经有一个 File 存在,则丢弃其内容。
* im - 要写入的 RenderedImage。
* formatName - 包含格式非正式名称的 String。
* output - 将在其中写入数据的 File。
* 如果没有找到合适的 writer,则返回 false。
*/
//显然是把captchaProducerMath创建的图像image 以jpg格式 写入 输出流
//这样做的目的是 为了接下来把图像转成字符
//是 图像 转 图像字符传
ImageIO.write(image, "jpg", os);
} catch (IOException e) {
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
// 图像字符串 通过Base64编码传输
// Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。采用Base64编码具有不可读性,需要解码后才能阅读。
// 为了加密
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
请求验证码大概就是这个流程
5.看看其他相关类
CaptchaConfig类
package com.ruoyi.gateway.config;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import static com.google.code.kaptcha.Constants.*;
/**
* 验证码配置
*
* @author ruoyi
*/
@Configuration
public class CaptchaConfig
{
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.gateway.config.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
KaptchaTextCreator类
package com.ruoyi.gateway.config;
import java.util.Random;
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
/**
* 验证码文本生成器
*
* @author ruoyi
*/
public class KaptchaTextCreator extends DefaultTextCreator
{
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText()
{
Integer result = 0;
Random random = new Random();
int x = random.nextInt(10);
int y = random.nextInt(10);
StringBuilder suChinese = new StringBuilder();
int randomoperands = (int) Math.round(Math.random() * 2);
if (randomoperands == 0)
{
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
}
else if (randomoperands == 1)
{
if (!(x == 0) && y % x == 0)
{
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
}
else
{
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
}
else if (randomoperands == 2)
{
if (x >= y)
{
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
}
else
{
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
}
else
{
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
suChinese.append("=?@" + result);
return suChinese.toString();
}
}
CaptchaProperties类
package com.ruoyi.gateway.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* 验证码配置
*
* @author ruoyi
*/
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.captcha")
public class CaptchaProperties {
/**
* 验证码开关
*/
private Boolean enabled;
/**
* 验证码类型(math 数组计算 char 字符)
*/
private String type;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}