(二十三)captcha验证码整合
基础项目地址:
https://gitee.com/springzb/admin-boot
一、引入依赖
xml
<!-- 验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
二、编码
新增配置类CaptchaProperties
java
package cn.mesmile.admin.common.captcha;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author zb
* @Description
*/
@Component
@Data
@ConfigurationProperties(prefix = "captcha")
public class CaptchaProperties {
/**
* 是否开启验证码
*/
private Boolean enabled = Boolean.FALSE;
/**
* 验证码模式
*/
private VerifyTypeEnum verifyType = VerifyTypeEnum.CALCULATE;
/**
* 过期时间,单位 秒
*/
private Integer expire = 120;
}
验证码分类
java
package cn.mesmile.admin.common.captcha;
/**
* @author zb
* @Description
*/
public enum VerifyTypeEnum {
/**
* 验证码方式, 计算
*/
CALCULATE,
/**
* 随机字母和数字
*/
RANDOM_LETTER_NUMBER
}
自定义生成验证方式
java
package cn.mesmile.admin.common.captcha;
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
import java.util.Random;
/**
* @author zb
* @Description 自定义计算结果
*/
public class CaptchaTextCreator extends DefaultTextCreator {
private static final String[] CNUMBERS = new String[]{"0","1","2","3","4","5","6","7","8","9","10"};
@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();
}
}
配置相关方式CaptchaConfiguration
java
package cn.mesmile.admin.common.captcha;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.impl.NoNoise;
import com.google.code.kaptcha.impl.ShadowGimpy;
import com.google.code.kaptcha.util.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import static com.google.code.kaptcha.Constants.*;
/**
* @author zb
* @Description
*/
@EnableConfigurationProperties({CaptchaProperties.class})
@Configuration
@ConditionalOnProperty(
value = {"captcha.enabled"},
havingValue = "true"
)
public class CaptchaConfiguration {
@ConditionalOnProperty(
value = {"captcha.verify-type"},
havingValue = "random_letter_number"
)
@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, ShadowGimpy.class.getTypeName());
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
/**
* 默认使用 加减乘除
* @return
*/
@ConditionalOnProperty(
value = {"captcha.verify-type"},
havingValue = "calculate"
)
@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, CaptchaTextCreator.class.getTypeName());
// 验证码文本字符间距 默认为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, NoNoise.class.getTypeName());
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, ShadowGimpy.class.getTypeName());
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
生成以及校验工具类CaptchaUtil
java
package cn.mesmile.admin.common.captcha;
import cn.hutool.core.util.StrUtil;
import cn.mesmile.admin.common.constant.AdminConstant;
import cn.mesmile.admin.common.exceptions.ServiceException;
import cn.mesmile.admin.common.utils.AdminRedisTemplate;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.google.code.kaptcha.Producer;
import org.springframework.util.FastByteArrayOutputStream;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.util.Base64;
/**
* @author zb
* @Description
*/
public class CaptchaUtil {
private CaptchaUtil(){}
/**
* 检查验证码是否有效
* @param uuid 唯一键值
* @param codeAnswer 验证码答案
* @param adminRedisTemplate redis工具类
* @return 验证成功为 TRUE 验证失败为 FALSE
*/
public static boolean checkVerificationCode(String uuid, String codeAnswer,AdminRedisTemplate adminRedisTemplate){
String verifyKey = AdminConstant.CAPTCHA_CODE_KEY + uuid;
String answer = adminRedisTemplate.get(verifyKey);
if (StrUtil.isNotEmpty(codeAnswer) && answer != null && answer.equals(codeAnswer)){
return true;
}
return false;
}
/**
* 获取Base64图片
* @param captchaProducerMath 图片生成器
* @param adminRedisTemplate redis工具类
* @param captchaProperties 配置
* @param uuid 唯一值
* @return base64字符串图片
*/
public static String getImageBase64Str(Producer captchaProducerMath, AdminRedisTemplate adminRedisTemplate,
CaptchaProperties captchaProperties,String uuid){
BufferedImage imageStream = getImageStream(captchaProducerMath, adminRedisTemplate, captchaProperties, uuid);
try (
FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
){
ImageIO.write(imageStream, "jpg", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}catch (Exception e) {
throw new ServiceException("生成验证码异常", e);
}
}
public static BufferedImage getImageStream(Producer captchaProducerMath, AdminRedisTemplate adminRedisTemplate,
CaptchaProperties captchaProperties,String uuid){
String verifyKey = AdminConstant.CAPTCHA_CODE_KEY + uuid;
// 创造图形
String capText = captchaProducerMath.createText();
String capStr = null;
String answer = null;
if (captchaProperties.getVerifyType().equals(VerifyTypeEnum.CALCULATE)){
capStr = capText.substring(0, capText.lastIndexOf(StringPool.AT));
answer = capText.substring(capText.lastIndexOf(StringPool.AT) + 1);
}else if (captchaProperties.getVerifyType().equals(VerifyTypeEnum.RANDOM_LETTER_NUMBER)){
capStr = capText;
answer = capText;
}
BufferedImage image = captchaProducerMath.createImage(capStr);
// 验证码默认有效期为两分钟
adminRedisTemplate.setEx(verifyKey, answer, Duration.ofSeconds(captchaProperties.getExpire()));
return image;
}
}
application.yml中添加相关配置
yaml
# 验证码相关
captcha:
enabled: true
verify-type: calculate
expire: 120
三、测试
java
package cn.mesmile.admin.modules.auth.controller;
import cn.hutool.core.util.IdUtil;
import cn.mesmile.admin.common.captcha.CaptchaProperties;
import cn.mesmile.admin.common.captcha.CaptchaUtil;
import cn.mesmile.admin.common.captcha.CaptchaVO;
import cn.mesmile.admin.common.limit.LimiterModeEnum;
import cn.mesmile.admin.common.limit.RateLimiter;
import cn.mesmile.admin.common.result.R;
import cn.mesmile.admin.common.utils.AdminRedisTemplate;
import cn.mesmile.admin.modules.auth.domain.request.LoginRequest;
import cn.mesmile.admin.modules.auth.domain.vo.LoginVO;
import cn.mesmile.admin.modules.auth.service.ILoginService;
import com.google.code.kaptcha.Producer;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @author zb
* @Description
*/
@Api(value = "登陆退出相关api", tags = {"登陆退出相关api"})
@Validated
@RestController
@RequiredArgsConstructor
public class LoginController {
private final Producer producer;
private final AdminRedisTemplate adminRedisTemplate;
private final CaptchaProperties captchaProperties;
/** 限制频率在 5 秒钟 3次
*
* 若整合了 spring security需要放开 /captcha 接口
*/
@RateLimiter(limiterMode = LimiterModeEnum.LIMITER_IP,max = 3, ttl = 6,timeUnit = TimeUnit.SECONDS)
@ApiOperation("获取验证码")
@GetMapping("/captcha")
public R<CaptchaVO> getCaptcha(){
String uuid = IdUtil.fastSimpleUUID();
String imageBase64Str = CaptchaUtil.getImageBase64Str(producer, adminRedisTemplate, captchaProperties, uuid);
CaptchaVO captchaVO = new CaptchaVO(uuid, imageBase64Str);
return R.data(captchaVO);
}
}
请求 localhost:8080/captcha 地址,获取到结果
json
{
"code": 200,
"success": true,
"data": {
"key": "57e851e88ffa4a0cac5d14c9d5669ae5",
"img": "/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAA8AKADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDtrW1ga1hZoIySikkoOeKsCztv+feL/vgU2z/484P+ua/yqyKiMY8q0IjGPKtCIWdr/wA+0P8A3wKeLK1/59of+/YqUU4U+WPYfLHsRCytP+fWH/v2KcLG0/59YP8Av2Kh1DVLLSbR7q+uY4IF6u5/Qep9hVDQ/GGieIXePT71XlTkxOCj49QD1H0rWOFnKDqRg3Fbu2i+YrRvY2BYWf8Az6wf9+xThYWf/PpB/wB+xVXVtastDsTe38jR24YKzqhbbk4yQOcVfgmjuIUlidXjdQyspyCDyDUOlaKny6PrYfLHsNGn2X/Ppb/9+x/hThp1l/z52/8A36X/AAp5niRwjSKGPQE4Jo+126zrAZ4xMwyELDcfwpci7Byx7ANOsf8Anzt/+/S/4U4abY/8+Vv/AN+l/wAKnXmnilyx7Byx7FcaZYf8+Vt/36X/AAp40yw/58bb/v0v+FWBTtwFHLHsHLHsVxpen/8APjbf9+V/wp40rT/+fC1/78r/AIVkL428OtrQ0ldVt2vCdu0Nxu/u7umfbNdEpBrSdCVO3PG19VdAoxexWGlad/z4Wv8A35X/AApw0nTv+gfa/wDflf8ACrQp4rPlj2Dlj2Ko0nTf+gfaf9+V/wAKranpenx6Reuljaq6wOVYQqCDtPI4rWFVdW/5At//ANe8n/oJpSjHlegpRjyvQ5Kz/wCPOD/rmv8AKrIqvZ/8ecH/AFzX+VWRTj8KHH4UOFDHapNKKGXK4qijx/4rWxuJbe+XJMQMbj2J4P51zeh2q/bbO/0S6EV/AwYwztw3qAR2IyMe/WvT/FekfaoXBXKsCCK8mu/Ds9vN/o7scHkdx719XleZwnhVhKtTkavZtXi091JGE4Pm5krnpHilNS1eAeXqCwW7Q7ZbR13o7Zzyf89KxtA+Il9pdnaaK1vA00Mgj824l2II/r7dK52ZdW0CNLqO+e6tzgSRvnA/Ak/nWhounWmqXw1GABo5AVlgdQwz6fyNOEaFDC/v7VKX2XFWtJdJbO7899NQ1ctNGa3xPlmvls9Ss5yY41w3lvkYPcEVyWmadHNawaouti1u1lyXkONhHTB656V6FdeH0j00wwQLHFydijjnrXl+saVJplwVwxtmbOPQ+lbZLmCrU1gYz5Gno7J80dbxd76/8MKpCz5j6I8M6vcXmnIL4xtOqj99EcxzD+8p/pWIPi74fTW5NOnjvIERzGbiWLaoYHByv3h+I+oFea6Lf69olkJ9CuVu7F/mNtKMlT3x0/T8qtjXdO8V2k/2zS4jeAbpQijzG4xuVupxxx/OvPll1GnKdWouenteLs4u/WL2/Irnbsloz1TW/iP4c0S1WVr6O7d/uRWjLIx9+DgD6muF+Imt6hqenWuoaFezSafcrhlhJz+I6j0PuMVxENz4d0aTz7aF9TnP3EnHyJ9Rjk/h+VbujeJ9EleT7Sv9lu3MkSqTE5/vAAcN+Az78Y6Y4B4TlxGGoynyv7SWv/bvxK3fuLm5tGxNM8J6dc6DHHNFLDqDjebjcdyN1Ax0x+tdZ4V+It/pOqweHPFCFpGZY4b0H74PClvUHpn8+c10Flo1vNpyXMDrJFIgdHXkMCMgivKPGLtH4xsYro7IYmUhj027uT+lZYGvUzOtPD4v3k1JrvF/3fytsOSUEnE+l42DjIqUVlaPcGe2Vic5Fawr5k2HCqurf8gS/wD+vaT/ANBNWxVXV/8AkCX/AP17Sf8AoJqZfCyZfCzkrP8A48oP+ua/yqyKr2X/AB5Qf9c1/lVkUR+FBH4UOFOApBTxVFFa5tVnjIYV5z4z0FxaPcWbGO5iyy7f4h3FeoYyKxtY043MZAFa0arpVFUWtu4mrqx8+tqOq3Fi1q0TSJ0Zihz+ddX8MLS4XU7hpUZYWUDBHVs10k3hOSSQ/Lxn0rodA8PmyYEivZxOeKrh54enRjCMnfQzjSs02zpDYRy2+3aOlcJ4l8MLLvOzKnqMV6XEmEAqve2azxEEV4SbTujU+dI7mbwpfzWsyO9u/wA8eOtL4esLy81oakI/KjZy+PUGvTdZ8LLczjfArhTkbhnFaGjeGSjhnWvdqZ3elLkppVJq0pd15La76syVPXfRHJw+FYYruS6gtgJZCWLdcZ9PSp5vCVpqUqG/s97D+NSVYj0JHWvVoNKiRQNoqb+zIc52ivIWKrqaqKb5lpe7v95pyq1rGZo9lDZ6RFaW0CwwRrtRFHAFecfEO1eyVL+C2jlvEkCQlk3FS3oO54GK9ljt1jTAFYmraSLlwdoJByOOlFCu6dZVZK9nd+fdP16g1dWMr4aX2pXPhyIatHIl4jsrGRNpYZyDj6HH4V3y9KwNHsGtgAa31HFRWqKpUlNJK7vZbLyXkCVlYeKq6v8A8gS//wCvaT/0E1bFVdX/AOQJf/8AXtJ/6Caxl8LFL4WclZf8eVv/ANc1/lVkVzMWtXMUSRqkRCKFGQe341J/b91/zzh/75P+NZRrRsjONWNkdKKcK5n/AISG7/55wf8AfJ/xpf8AhIrv/nnB/wB8n/Gq9tEftonUCl2Buorl/wDhJLz/AJ5Qf98n/Gl/4SW8/wCeUH/fJ/xo9tEPbROnECegqVI1XoK5T/hJ73/nlb/98t/jS/8ACUXv/PK3/wC+W/xo9tEPbROvAp2M1x//AAlV9/zyt/8Avlv8aX/hK77/AJ5W3/fLf40e2iHtonWNbI5yVFTRwqnQVx3/AAlt/wD88bb/AL5b/Gl/4S/UP+eNt/3y3/xVHtoh7aJ2wFPFcP8A8JhqH/PG1/75b/4ql/4TLUf+eNr/AN8t/wDFUe2iHtondCjyw3UVw3/CZ6j/AM8bX/vhv/iqX/hNdS/54Wn/AHw3/wAVR7aIe2id4iBegqQVwH/Cbal/zwtP++G/+Kpf+E41P/nhaf8AfDf/ABVHtoh7aJ6CKq6v/wAgPUP+vaT/ANBNcV/wnOp/88LT/vhv/iqjufGeo3VrNbvDahJUZGKq2QCMcfNUyrRsxSqxsz//2Q=="
},
"msg": "操作成功"
}
校验结果成功: