1. 验证码的作用
验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。
2. 验证码的业务逻辑
业务逻辑大致如上。
- 前端调用API接口,向后端发起一个[获取验证码]请求。
- 后端得到请求,创建验证码ID(通常利用UUID直接创建),然后以各种方式生成一串随机内容(常见的包括英文、数字、中文、数学公式)
- 根据随机内容分配出Key(返回给前端显示的内容)和value(验证码的唯一标准答案)
- 将验证码ID和value存放到Redis中,同时可以利用Redis的缓存过期时间功能同步设置本次验证码的实际有效时间。
- 将String格式的验证码内容Key通过一系列方式转换成图片pic
- 将UUID与pic返回给前端进行展示即可。
- 前端获取验证码后填写业务表单(username, password, captchaID, captchaCode),调用提交相关API
- 后端根据验证码的ID再次向Redis发起查询,进行验证码匹配。
- 若Redis返回空值,则表明该ID对应的验证码已经过期失效。若Redis不返回空值,则获取到该值,并与前端传来的code进行比对,若一致则比对成功,不一致则验证码错误。
3. 验证码绘制实现
1. 手动绘制
可以利用 Graphics
、Graphics2D
等类进行图片操作,利用draw
系列函数手动绘制自定义模式的验证码效果。
2. 自动绘制 - kaptcha
仓库地址:https://github.com/trydofor/kaptcha
该模块将绘制验证码需要的各种类进行了封装整合调用,对内设计了配置类Config(com.google.code.kaptcha.Constants
),我们在使用该模块时只需要编写Config文件并进行set操作后即可生成验证码。
模块引用
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.3</version>
</dependency>
1. 利用spring配置文件配置Kaptcha
<!-- 配置Kaptcha -->
<bean id="kaptchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha">
<property name="config">
<bean class="com.google.code.kaptcha.util.Config">
<constructor-arg>
<props>
<!--验证码图片不生成边框-->
<prop key="kaptcha.border">no</prop>
<!-- 验证码图片宽度为120像素 -->
<prop key="kaptcha.image.width">120</prop>
<!-- 验证码图片字体颜色为蓝色 -->
<prop key="kaptcha.textproducer.font.color">blue</prop>
<!-- 每个字符最大占用40像素 -->
<prop key="kaptcha.textproducer.font.size">40</prop>
<!-- 验证码包含4个字符 -->
<prop key="kaptcha.textproducer.char.length">4</prop>
</props>
</constructor-arg>
</bean>
</property>
</bean>
引用自https://blog.csdn.net/m0_47119893/article/details/122397803
2. 利用注解配置Kaptcha
首先编写一个config.java
@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;
}
}
Kaptcha自带了Producer
类,调用其createText()
和createImage(arg)
即可生成图片存放到BufferedImage
中。
完成config配置后,编写controller,将图片刷入outputStream中,将os编码后返回前端即可。
@RestController
public class CaptchaController {
@Resource
private Producer captchaProducer;
@GetMapping("/captchaImage")
public AjaxResult getCode() throws IOException {
// AjaxResult 本质是一个 HashMap, 调用put方法向内放置内容
AjaxResult ajax = AjaxResult.success();
String uuid = UUID.randomUUID().toString();
String captcha = captchaProducer.createText();
BufferedImage image = captchaProducer.createImage(captcha);
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", os);
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}
3. 效果展示
4. 验证码业务实现
修改controller内容,添加Redis部分:
- 提交Redis - {redis_key, captcha_code},其中[redis_key]值为"captcha_codes:" + uuid; (方便查看redis存放内容信息)
- 返回前端 - {uuid, img},其中[img]值为base64转码后的图片流
@RestController
public class CaptchaController {
@Resource(name = "captchaProducer")
private Producer captchaProducer;
@Autowired
public RedisTemplate redisTemplate;
@GetMapping("/captchaImage")
public AjaxResult getCode() throws IOException {
AjaxResult ajax = AjaxResult.success();
String uuid = UUID.randomUUID().toString();
String redis_key = "captcha_codes:" + uuid;
String captcha_text = captchaProducer.createText();
String captcha_code = captcha_text;
BufferedImage image = captchaProducer.createImage(captcha_text);
redisTemplate.opsForValue().set(redis_key, captcha_code, 3, TimeUnit.MINUTES);
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
ImageIO.write(image, "jpg", os);
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
}
该步骤效果展示
在其他业务逻辑之前添加验证码校验功能
public void validateCaptcha(String code, String uuid) {
String verifyKey = "captcha_codes:" + uuid;
String captcha = redisTemplate.opsForValue().get(verifyKey).toString();
redisTemplate.delete(verifyKey);
if (captcha == null) {
// 验证码为空逻辑方法
throw xxxxx;
}
if (!code.equalsIgnoreCase(captcha)) {
// 验证码错误逻辑方法
thorw xxxxx;
}
}
验证码功能至此基本全部实现。
5. 总结与延申
验证码的本质是{id, key, value}的形式,对于不同的目标提供不同的子集内容,借助Redis的过期时间可以很好的管理验证码的有效时间。除此之外,也可以在Controller中自行设计Map容器用于代替Redis。思路如下:
Map的Key仍为uuid的某种形式,Value则封装POJO。
public class CaptchaModel {
Date createTime;
Date endTime;
String value;
public CaptchaModel(String value, Date time) {
this.value = value;
this.createTime = time;
endTime = new Date();
// 有效期3分钟
endTime.setTime(createTime.getTime() + 1000 * 60 * 3);
}
}
在执行业务逻辑时,注意要根据uuid查询endTime确保是否过期即可。缺陷就是需要编写Util专门维护这个Map,即便如此也无法确保Map容量不会爆掉。
关于Kaptcha
我们可以很容易的看到Kaptcha的源码内容,在Config中我们可以看到作者为我们提供了大量的自定义途径,每次调用配置方法时,都会执行类似如下代码
public Color getXXXXXX() {
String paramName = "kaptcha.XXXXXX";
String paramValue = this.properties.getProperty(paramName);
return this.helper.getXXXXXX(paramName, paramValue, YYYYYYY);
}
再看Constants文件,即可完全了解能够配置的内容(部分内容不进行解释)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.google.code.kaptcha;
public class Constants {
public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
public static final String KAPTCHA_SESSION_DATE = "KAPTCHA_SESSION_DATE";
public static final String KAPTCHA_SESSION_CONFIG_KEY = "kaptcha.session.key";
public static final String KAPTCHA_SESSION_CONFIG_DATE = "kaptcha.session.date";
public static final String KAPTCHA_BORDER = "kaptcha.border";
public static final String KAPTCHA_BORDER_COLOR = "kaptcha.border.color";
public static final String KAPTCHA_BORDER_THICKNESS = "kaptcha.border.thickness";
public static final String KAPTCHA_NOISE_COLOR = "kaptcha.noise.color";
public static final String KAPTCHA_NOISE_IMPL = "kaptcha.noise.impl";
public static final String KAPTCHA_OBSCURIFICATOR_IMPL = "kaptcha.obscurificator.impl";
public static final String KAPTCHA_PRODUCER_IMPL = "kaptcha.producer.impl";
public static final String KAPTCHA_TEXTPRODUCER_IMPL = "kaptcha.textproducer.impl";
public static final String KAPTCHA_TEXTPRODUCER_CHAR_STRING = "kaptcha.textproducer.char.string";
public static final String KAPTCHA_TEXTPRODUCER_CHAR_LENGTH = "kaptcha.textproducer.char.length";
public static final String KAPTCHA_TEXTPRODUCER_FONT_NAMES = "kaptcha.textproducer.font.names";
public static final String KAPTCHA_TEXTPRODUCER_FONT_COLOR = "kaptcha.textproducer.font.color";
public static final String KAPTCHA_TEXTPRODUCER_FONT_SIZE = "kaptcha.textproducer.font.size";
public static final String KAPTCHA_TEXTPRODUCER_CHAR_SPACE = "kaptcha.textproducer.char.space";
public static final String KAPTCHA_WORDRENDERER_IMPL = "kaptcha.word.impl";
public static final String KAPTCHA_BACKGROUND_IMPL = "kaptcha.background.impl";
public static final String KAPTCHA_BACKGROUND_CLR_FROM = "kaptcha.background.clear.from";
public static final String KAPTCHA_BACKGROUND_CLR_TO = "kaptcha.background.clear.to";
public static final String KAPTCHA_IMAGE_WIDTH = "kaptcha.image.width";
public static final String KAPTCHA_IMAGE_HEIGHT = "kaptcha.image.height";
public Constants() {
}
}
字段名 | 含义 | 取值 | 默认取 |
---|---|---|---|
KAPTCHA_BORDER | 是否有边框 | yes/no | yes |
KAPTCHA_BORDER_COLOR | 边框颜色 | java.awt.Color | Color.BLACK |
KAPTCHA_BORDER_THICKNESS | 边框厚度 | int | 1 |
KAPTCHA_NOISE_COLOR | 噪点颜色 | java.awt.Color | Color.BLACK |
KAPTCHA_NOISE_IMPL | 噪点生成实现类 | NoNoise | DefaultNoise |
KAPTCHA_OBSCURIFICATOR_IMPL | 图片样式 | WaterRipple/FishEyeGimpy/ShadowGimpy | WaterRipple |
KAPTCHA_PRODUCER_IMPL | 绘制实现类 | (需自己编写) | DefaultKaptcha |
KAPTCHA_TEXTPRODUCER_IMPL | 文本内容生成实现类 | ChineseTextProducer/FiveLetterFirstNameTextCreator | DefaultTextCreator |
KAPTCHA_TEXTPRODUCER_CHAR_STRING | 文本内容限定字符 | 字符串(生成的验证码只包含这些字符,主要用去剔除难以区分的字符如1和I) | “abcde2345678gfynmnpwx” |
KAPTCHA_TEXTPRODUCER_CHAR_LENGTH | 验证码文本字符长度 | int | 5 |
KAPTCHA_TEXTPRODUCER_FONT_NAMES | 验证码文本字体样式 | 字体 | Arial,Courier |
KAPTCHA_TEXTPRODUCER_FONT_COLOR | 文本字符颜色 | java.awt.Color | Color.BLACK |
KAPTCHA_TEXTPRODUCER_FONT_SIZE | 验证码文本字符大小 | int | 40 |
KAPTCHA_TEXTPRODUCER_CHAR_SPACE | 验证码文本字符间隔 | int | 2 |
KAPTCHA_WORDRENDERER_IMPL | 文字呈现器 | DefaultWordRenderer | DefaultWordRenderer |
KAPTCHA_BACKGROUND_IMPL | 背景 | DefaultBackground | DefaultBackground |
KAPTCHA_BACKGROUND_CLR_FROM | 渐变色-左 | java.awt.Color | Color.LIGHT_GRAY |
KAPTCHA_BACKGROUND_CLR_TO | 渐变色-右 | java.awt.Color | Color.WHITE |
KAPTCHA_IMAGE_WIDTH | 验证码图片宽度 | int | 200 |
KAPTCHA_IMAGE_HEIGHT | 验证码图片高度 | int | 50 |
Q:为什么取值为yes/no?
A:Kaptcha作者编写了com.google.code.kaptcha.util.ConfigHelper
,对部分字段进行了人性化转义。内容大致如下:
public boolean getBoolean(String paramName, String paramValue, boolean defaultValue) {
boolean booleanValue;
if (!"yes".equals(paramValue) && !"".equals(paramValue) && paramValue != null) {
if (!"no".equals(paramValue)) {
throw new ConfigException(paramName, paramValue, "Value must be either yes or no.");
}
booleanValue = false;
} else {
booleanValue = defaultValue;
}
return booleanValue;
}
Q:为什么直接填写ChineseTextProducer
报错?
A:需要填写全限定名如com.google.code.kaptcha.text.impl.FiveLetterFirstNameTextCreator
、com.google.code.kaptcha.impl.NoNoise