m = new HashMap();
+ Arrays.stream(jsonStr
+ .replaceFirst(",\\{", "\\{")
+ .replaceFirst("\\{", "")
+ .replaceFirst("\\}", "")
+ .replaceAll("\"", "")
+ .split(",")).forEach(item -> {
+ m.put(item.split(":")[0], item.split(":")[1]);
+ });
+ //PointVO d = new PointVO();
+ setX(Double.valueOf("" + m.get("x")).intValue());
+ setY(Double.valueOf("" + m.get("y")).intValue());
+ setSecretKey(m.getOrDefault("secretKey", "") + "");
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ PointVO pointVO = (PointVO) o;
+ return x == pointVO.x && y == pointVO.y && Objects.equals(secretKey, pointVO.secretKey);
+ }
+
+ @Override
+ public int hashCode() {
+
+ return Objects.hash(secretKey, x, y);
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java
new file mode 100644
index 000000000..c6311af74
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java
@@ -0,0 +1,141 @@
+package com.anji.captcha.properties;
+
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.awt.*;
+
+import static com.anji.captcha.properties.AjCaptchaProperties.PREFIX;
+import static com.anji.captcha.properties.AjCaptchaProperties.StorageType.local;
+
+@Data
+@ConfigurationProperties(PREFIX)
+public class AjCaptchaProperties {
+ public static final String PREFIX = "aj.captcha";
+
+ /**
+ * 验证码类型.
+ */
+ private CaptchaTypeEnum type = CaptchaTypeEnum.DEFAULT;
+
+ /**
+ * 滑动拼图底图路径.
+ */
+ private String jigsaw = "";
+
+ /**
+ * 点选文字底图路径.
+ */
+ private String picClick = "";
+
+
+ /**
+ * 右下角水印文字(我的水印).
+ */
+ private String waterMark = "我的水印";
+
+ /**
+ * 右下角水印字体(文泉驿正黑).
+ */
+ private String waterFont = "WenQuanZhengHei.ttf";
+
+ /**
+ * 点选文字验证码的文字字体(文泉驿正黑).
+ */
+ private String fontType = "WenQuanZhengHei.ttf";
+
+ /**
+ * 校验滑动拼图允许误差偏移量(默认5像素).
+ */
+ private String slipOffset = "5";
+
+ /**
+ * aes加密坐标开启或者禁用(true|false).
+ */
+ private Boolean aesStatus = true;
+
+ /**
+ * 滑块干扰项(0/1/2)
+ */
+ private String interferenceOptions = "0";
+
+ /**
+ * local缓存的阈值
+ */
+ private String cacheNumber = "1000";
+
+ /**
+ * 定时清理过期local缓存(单位秒)
+ */
+ private String timingClear = "180";
+
+ /**
+ * 缓存类型redis/local/....
+ */
+ private StorageType cacheType = local;
+ /**
+ * 历史数据清除开关
+ */
+ private boolean historyDataClearEnable = false;
+
+ /**
+ * 一分钟内接口请求次数限制 开关
+ */
+ private boolean reqFrequencyLimitEnable = false;
+
+ /***
+ * 一分钟内check接口失败次数
+ */
+ private int reqGetLockLimit = 5;
+ /**
+ *
+ */
+ private int reqGetLockSeconds = 300;
+
+ /***
+ * get接口一分钟内限制访问数
+ */
+ private int reqGetMinuteLimit = 100;
+ private int reqCheckMinuteLimit = 100;
+ private int reqVerifyMinuteLimit = 100;
+
+ /**
+ * 点选字体样式
+ */
+ private int fontStyle = Font.BOLD;
+
+ /**
+ * 点选字体大小
+ */
+ private int fontSize = 25;
+
+ /**
+ * 点选文字个数,存在问题,暂不要使用
+ */
+ private int clickWordCount = 4;
+
+ public boolean getReqFrequencyLimitEnable() {
+ return reqFrequencyLimitEnable;
+ }
+
+ public enum StorageType {
+ /**
+ * 内存.
+ */
+ local,
+ /**
+ * redis.
+ */
+ redis,
+ /**
+ * 其他.
+ */
+ other,
+ }
+
+ public static String getPrefix() {
+ return PREFIX;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java
new file mode 100644
index 000000000..55c662a8f
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java
@@ -0,0 +1,43 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service;
+
+/**
+ * 验证码缓存接口
+ *
+ * @author lide1202@hotmail.com
+ * @date 2018-08-21
+ */
+public interface CaptchaCacheService {
+
+ void set(String key, String value, long expiresInSeconds);
+
+ boolean exists(String key);
+
+ void delete(String key);
+
+ String get(String key);
+
+ /**
+ * 缓存类型-local/redis/memcache/..
+ * 通过java SPI机制,接入方可自定义实现类
+ *
+ * @return
+ */
+ String type();
+
+ /***
+ *
+ * @param key
+ * @param val
+ * @return
+ */
+ default Long increment(String key, long val) {
+ return 0L;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java
new file mode 100644
index 000000000..b9db2fbe2
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java
@@ -0,0 +1,63 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service;
+
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+
+import java.util.Properties;
+
+/**
+ * 验证码服务接口
+ *
+ * @author lide1202@hotmail.com
+ * @date 2020-05-12
+ */
+public interface CaptchaService {
+ /**
+ * 配置初始化
+ */
+ void init(Properties config);
+
+ /**
+ * 获取验证码
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel get(CaptchaVO captchaVO);
+
+ /**
+ * 核对验证码(前端)
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel check(CaptchaVO captchaVO);
+
+ /**
+ * 二次校验验证码(后端)
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel verification(CaptchaVO captchaVO);
+
+ /***
+ * 验证码类型
+ * 通过java SPI机制,接入方可自定义实现类,实现新的验证类型
+ * @return
+ */
+ String captchaType();
+
+ /**
+ * 历史资源清除(过期的图片文件,生成的临时图片...)
+ *
+ * @param config 配置项 控制资源清理的粒度
+ */
+ void destroy(Properties config);
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java
new file mode 100644
index 000000000..037070f59
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java
@@ -0,0 +1,269 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.CaptchaService;
+import com.anji.captcha.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.awt.*;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public abstract class AbstractCaptchaService implements CaptchaService {
+
+ protected static final String IMAGE_TYPE_PNG = "png";
+
+ protected static int HAN_ZI_SIZE = 25;
+
+ protected static int HAN_ZI_SIZE_HALF = HAN_ZI_SIZE / 2;
+ //check校验坐标
+ protected static String REDIS_CAPTCHA_KEY = "RUNNING:CAPTCHA:%s";
+
+ //后台二次校验坐标
+ protected static String REDIS_SECOND_CAPTCHA_KEY = "RUNNING:CAPTCHA:second-%s";
+
+ protected static Long EXPIRESIN_SECONDS = 2 * 60L;
+
+ protected static Long EXPIRESIN_THREE = 3 * 60L;
+
+ protected static String waterMark = "我的水印";
+
+ protected static String waterMarkFontStr = "WenQuanZhengHei.ttf";
+
+ protected Font waterMarkFont;//水印字体
+
+ protected static String slipOffset = "5";
+
+ protected static Boolean captchaAesStatus = true;
+
+ protected static String clickWordFontStr = "WenQuanZhengHei.ttf";
+
+ protected Font clickWordFont;//点选文字字体
+
+ protected static String cacheType = "local";
+
+ protected static int captchaInterferenceOptions = 0;
+
+ //判断应用是否实现了自定义缓存,没有就使用内存
+ @Override
+ public void init(final Properties config) {
+ //初始化底图
+ boolean aBoolean = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_INIT_ORIGINAL));
+ if (!aBoolean) {
+ ImageUtils.cacheImage(config.getProperty(Const.ORIGINAL_PATH_JIGSAW),
+ config.getProperty(Const.ORIGINAL_PATH_PIC_CLICK));
+ }
+ log.info("--->>>初始化验证码底图<<<---" + captchaType());
+ waterMark = config.getProperty(Const.CAPTCHA_WATER_MARK, "我的水印");
+ slipOffset = config.getProperty(Const.CAPTCHA_SLIP_OFFSET, "5");
+ waterMarkFontStr = config.getProperty(Const.CAPTCHA_WATER_FONT, "WenQuanZhengHei.ttf");
+ captchaAesStatus = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_AES_STATUS, "true"));
+ clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "WenQuanZhengHei.ttf");
+ //clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "SourceHanSansCN-Normal.otf");
+ cacheType = config.getProperty(Const.CAPTCHA_CACHETYPE, "local");
+ captchaInterferenceOptions = Integer.parseInt(
+ config.getProperty(Const.CAPTCHA_INTERFERENCE_OPTIONS, "0"));
+
+ // 部署在linux中,如果没有安装中文字段,水印和点选文字,中文无法显示,
+ // 通过加载resources下的font字体解决,无需在linux中安装字体
+ loadWaterMarkFont();
+
+ if ("local".equals(cacheType)) {
+ log.info("初始化local缓存...");
+ CacheUtil.init(Integer.parseInt(config.getProperty(Const.CAPTCHA_CACAHE_MAX_NUMBER, "1000")),
+ Long.parseLong(config.getProperty(Const.CAPTCHA_TIMING_CLEAR_SECOND, "180")));
+ }
+ if ("1".equals(config.getProperty(Const.HISTORY_DATA_CLEAR_ENABLE, "0"))) {
+ log.info("历史资源清除开关...开启..." + captchaType());
+ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+ @Override
+ public void run() {
+ destroy(config);
+ }
+ }));
+ }
+ if ("1".equals(config.getProperty(Const.REQ_FREQUENCY_LIMIT_ENABLE, "0"))) {
+ if (limitHandler == null) {
+ log.info("接口分钟内限流开关...开启...");
+ limitHandler = new FrequencyLimitHandler.DefaultLimitHandler(config, getCacheService(cacheType));
+ }
+ }
+ }
+
+ protected CaptchaCacheService getCacheService(String cacheType) {
+ return CaptchaServiceFactory.getCache(cacheType);
+ }
+
+ @Override
+ public void destroy(Properties config) {
+
+ }
+
+ private static FrequencyLimitHandler limitHandler;
+
+ @Override
+ public ResponseModel get(CaptchaVO captchaVO) {
+ if (limitHandler != null) {
+ captchaVO.setClientUid(getValidateClientId(captchaVO));
+ return limitHandler.validateGet(captchaVO);
+ }
+ return null;
+ }
+
+ @Override
+ public ResponseModel check(CaptchaVO captchaVO) {
+ if (limitHandler != null) {
+ // 验证客户端
+ /* ResponseModel ret = limitHandler.validateCheck(captchaVO);
+ if(!validatedReq(ret)){
+ return ret;
+ }
+ // 服务端参数验证*/
+ captchaVO.setClientUid(getValidateClientId(captchaVO));
+ return limitHandler.validateCheck(captchaVO);
+ }
+ return null;
+ }
+
+ @Override
+ public ResponseModel verification(CaptchaVO captchaVO) {
+ if (captchaVO == null) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+ }
+ if (StrUtil.isEmpty(captchaVO.getCaptchaVerification())) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVerification");
+ }
+ if (limitHandler != null) {
+ return limitHandler.validateVerify(captchaVO);
+ }
+ return null;
+ }
+
+ protected boolean validatedReq(ResponseModel resp) {
+ return resp == null || resp.isSuccess();
+ }
+
+ protected String getValidateClientId(CaptchaVO req) {
+ // 以服务端获取的客户端标识 做识别标志
+ if (StrUtil.isNotEmpty(req.getBrowserInfo())) {
+ return MD5Util.md5(req.getBrowserInfo());
+ }
+ // 以客户端Ui组件id做识别标志
+ if (StrUtil.isNotEmpty(req.getClientUid())) {
+ return req.getClientUid();
+ }
+ return null;
+ }
+
+ protected void afterValidateFail(CaptchaVO data) {
+ if (limitHandler != null) {
+ // 验证失败 分钟内计数
+ String fails = String.format(FrequencyLimitHandler.LIMIT_KEY, "FAIL", data.getClientUid());
+ CaptchaCacheService cs = getCacheService(cacheType);
+ if (!cs.exists(fails)) {
+ cs.set(fails, "1", 60);
+ }
+ cs.increment(fails, 1);
+ }
+ }
+
+ /**
+ * 加载resources下的font字体,add by lide1202@hotmail.com
+ * 部署在linux中,如果没有安装中文字段,水印和点选文字,中文无法显示,
+ * 通过加载resources下的font字体解决,无需在linux中安装字体
+ */
+ private void loadWaterMarkFont() {
+ try {
+ if (waterMarkFontStr.toLowerCase().endsWith(".ttf") || waterMarkFontStr.toLowerCase().endsWith(".ttc")
+ || waterMarkFontStr.toLowerCase().endsWith(".otf")) {
+ this.waterMarkFont = Font.createFont(Font.TRUETYPE_FONT,
+ Objects.requireNonNull(getClass().getResourceAsStream("/fonts/" + waterMarkFontStr)))
+ .deriveFont(Font.BOLD, HAN_ZI_SIZE / 2);
+ } else {
+ this.waterMarkFont = new Font(waterMarkFontStr, Font.BOLD, HAN_ZI_SIZE / 2);
+ }
+
+ } catch (Exception e) {
+ log.error("load font error:{}", e);
+ }
+ }
+
+ public static boolean base64StrToImage(String imgStr, String path) {
+ if (imgStr == null) {
+ return false;
+ }
+
+ Base64.Decoder decoder = Base64.getDecoder();
+ try {
+ // 解密
+ byte[] b = decoder.decode(imgStr);
+ // 处理数据
+ for (int i = 0; i < b.length; ++i) {
+ if (b[i] < 0) {
+ b[i] += 256;
+ }
+ }
+ //文件夹不存在则自动创建
+ File tempFile = new File(path);
+ if (!tempFile.getParentFile().exists()) {
+ tempFile.getParentFile().mkdirs();
+ }
+ OutputStream out = Files.newOutputStream(tempFile.toPath());
+ out.write(b);
+ out.flush();
+ out.close();
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * 解密前端坐标aes加密
+ *
+ * @param point
+ * @return
+ * @throws Exception
+ */
+ public static String decrypt(String point, String key) throws Exception {
+ return AESUtil.aesDecrypt(point, key);
+ }
+
+ protected static int getEnOrChLength(String s) {
+ int enCount = 0;
+ int chCount = 0;
+ for (int i = 0; i < s.length(); i++) {
+ int length = String.valueOf(s.charAt(i)).getBytes(StandardCharsets.UTF_8).length;
+ if (length > 1) {
+ chCount++;
+ } else {
+ enCount++;
+ }
+ }
+ int chOffset = (HAN_ZI_SIZE / 2) * chCount + 5;
+ int enOffset = enCount * 8;
+ return chOffset + enOffset;
+ }
+
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java
new file mode 100644
index 000000000..6f49a7714
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java
@@ -0,0 +1,425 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.model.vo.PointVO;
+import com.anji.captcha.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Random;
+
+/**
+ * 滑动验证码
+ *
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public class BlockPuzzleCaptchaServiceImpl extends AbstractCaptchaService {
+
+ @Override
+ public void init(Properties config) {
+ super.init(config);
+ }
+
+ @Override
+ public void destroy(Properties config) {
+ log.info("start-clear-history-data-", captchaType());
+ }
+
+ @Override
+ public String captchaType() {
+ return CaptchaTypeEnum.BLOCKPUZZLE.getCodeValue();
+ }
+
+ @Override
+ public ResponseModel get(CaptchaVO captchaVO) {
+ ResponseModel r = super.get(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ //原生图片
+ BufferedImage originalImage = ImageUtils.getOriginal();
+ if (null == originalImage) {
+ log.error("滑动底图未初始化成功,请检查路径");
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+ }
+ //设置水印
+ Graphics backgroundGraphics = originalImage.getGraphics();
+ int width = originalImage.getWidth();
+ int height = originalImage.getHeight();
+ backgroundGraphics.setFont(waterMarkFont);
+ backgroundGraphics.setColor(Color.white);
+ backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7);
+
+ //抠图图片
+ String jigsawImageBase64 = ImageUtils.getslidingBlock();
+ BufferedImage jigsawImage = ImageUtils.getBase64StrToImage(jigsawImageBase64);
+ if (null == jigsawImage) {
+ log.error("滑动底图未初始化成功,请检查路径");
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+ }
+ CaptchaVO captcha = pictureTemplatesCut(originalImage, jigsawImage, jigsawImageBase64);
+ if (captcha == null
+ || StrUtil.isBlank(captcha.getJigsawImageBase64())
+ || StrUtil.isBlank(captcha.getOriginalImageBase64())) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR);
+ }
+ return ResponseModel.successData(captcha);
+ }
+
+ @Override
+ public ResponseModel check(CaptchaVO captchaVO) {
+ ResponseModel r = super.check(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ //取坐标信息
+ String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
+ if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+ }
+ String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
+ //验证码只用一次,即刻失效
+ CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+ PointVO point = null;
+ PointVO point1 = null;
+ String pointJson = null;
+ try {
+ point = JsonUtil.parseObject(s, PointVO.class);
+ //aes解密
+ pointJson = decrypt(captchaVO.getPointJson(), point.getSecretKey());
+ point1 = JsonUtil.parseObject(pointJson, PointVO.class);
+ } catch (Exception e) {
+ log.error("验证码坐标解析失败", e);
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ if (point.x - Integer.parseInt(slipOffset) > point1.x
+ || point1.x > point.x + Integer.parseInt(slipOffset)
+ || point.y != point1.y) {
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
+ }
+ //校验成功,将信息存入缓存
+ String secretKey = point.getSecretKey();
+ String value = null;
+ try {
+ value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey);
+ } catch (Exception e) {
+ log.error("AES加密失败", e);
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
+ CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
+ captchaVO.setResult(true);
+ captchaVO.resetClientFlag();
+ return ResponseModel.successData(captchaVO);
+ }
+
+ @Override
+ public ResponseModel verification(CaptchaVO captchaVO) {
+ ResponseModel r = super.verification(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ try {
+ String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+ if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+ }
+ //二次校验取值后,即刻失效
+ CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+ } catch (Exception e) {
+ log.error("验证码坐标解析失败", e);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ return ResponseModel.success();
+ }
+
+ /**
+ * 根据模板切图
+ *
+ * @throws Exception
+ */
+ public CaptchaVO pictureTemplatesCut(BufferedImage originalImage, BufferedImage jigsawImage, String jigsawImageBase64) {
+ try {
+ CaptchaVO dataVO = new CaptchaVO();
+
+ int originalWidth = originalImage.getWidth();
+ int originalHeight = originalImage.getHeight();
+ int jigsawWidth = jigsawImage.getWidth();
+ int jigsawHeight = jigsawImage.getHeight();
+
+ //随机生成拼图坐标
+ PointVO point = generateJigsawPoint(originalWidth, originalHeight, jigsawWidth, jigsawHeight);
+ int x = point.getX();
+ int y = point.getY();
+
+ //生成新的拼图图像
+ BufferedImage newJigsawImage = new BufferedImage(jigsawWidth, jigsawHeight, jigsawImage.getType());
+ Graphics2D graphics = newJigsawImage.createGraphics();
+
+ int bold = 5;
+ //如果需要生成RGB格式,需要做如下配置,Transparency 设置透明
+ newJigsawImage = graphics.getDeviceConfiguration().createCompatibleImage(jigsawWidth, jigsawHeight, Transparency.TRANSLUCENT);
+ // 新建的图像根据模板颜色赋值,源图生成遮罩
+ cutByTemplate(originalImage, jigsawImage, newJigsawImage, x, 0);
+ if (captchaInterferenceOptions > 0) {
+ int position = 0;
+ if (originalWidth - x - 5 > jigsawWidth * 2) {
+ //在原扣图右边插入干扰图
+ position = RandomUtils.getRandomInt(x + jigsawWidth + 5, originalWidth - jigsawWidth);
+ } else {
+ //在原扣图左边插入干扰图
+ position = RandomUtils.getRandomInt(100, x - jigsawWidth - 5);
+ }
+ while (true) {
+ String s = ImageUtils.getslidingBlock();
+ if (!jigsawImageBase64.equals(s)) {
+ interferenceByTemplate(originalImage, Objects.requireNonNull(ImageUtils.getBase64StrToImage(s)), position, 0);
+ break;
+ }
+ }
+ }
+ if (captchaInterferenceOptions > 1) {
+ while (true) {
+ String s = ImageUtils.getslidingBlock();
+ if (!jigsawImageBase64.equals(s)) {
+ Integer randomInt = RandomUtils.getRandomInt(jigsawWidth, 100 - jigsawWidth);
+ interferenceByTemplate(originalImage, Objects.requireNonNull(ImageUtils.getBase64StrToImage(s)),
+ randomInt, 0);
+ break;
+ }
+ }
+ }
+
+
+ // 设置“抗锯齿”的属性
+ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ graphics.setStroke(new BasicStroke(bold, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+ graphics.drawImage(newJigsawImage, 0, 0, null);
+ graphics.dispose();
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();//新建流。
+ ImageIO.write(newJigsawImage, IMAGE_TYPE_PNG, os);//利用ImageIO类提供的write方法,将bi以png图片的数据模式写入流。
+ byte[] jigsawImages = os.toByteArray();
+
+ ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();//新建流。
+ ImageIO.write(originalImage, IMAGE_TYPE_PNG, oriImagesOs);//利用ImageIO类提供的write方法,将bi以jpg图片的数据模式写入流。
+ byte[] oriCopyImages = oriImagesOs.toByteArray();
+ Base64.Encoder encoder = Base64.getEncoder();
+ dataVO.setOriginalImageBase64(encoder.encodeToString(oriCopyImages).replaceAll("\r|\n", ""));
+ //point信息不传到前端,只做后端check校验
+// dataVO.setPoint(point);
+ dataVO.setJigsawImageBase64(encoder.encodeToString(jigsawImages).replaceAll("\r|\n", ""));
+ dataVO.setToken(RandomUtils.getUUID());
+ dataVO.setSecretKey(point.getSecretKey());
+// base64StrToImage(encoder.encodeToString(oriCopyImages), "D:\\原图.png");
+// base64StrToImage(encoder.encodeToString(jigsawImages), "D:\\滑动.png");
+
+ //将坐标信息存入redis中
+ String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
+ CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(point), EXPIRESIN_SECONDS);
+ log.debug("token:{},point:{}", dataVO.getToken(), JsonUtil.toJSONString(point));
+ return dataVO;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+
+ /**
+ * 随机生成拼图坐标
+ *
+ * @param originalWidth
+ * @param originalHeight
+ * @param jigsawWidth
+ * @param jigsawHeight
+ * @return
+ */
+ private static PointVO generateJigsawPoint(int originalWidth, int originalHeight, int jigsawWidth, int jigsawHeight) {
+ Random random = new Random();
+ int widthDifference = originalWidth - jigsawWidth;
+ int heightDifference = originalHeight - jigsawHeight;
+ int x, y = 0;
+ if (widthDifference <= 0) {
+ x = 5;
+ } else {
+ x = random.nextInt(originalWidth - jigsawWidth - 100) + 100;
+ }
+ if (heightDifference <= 0) {
+ y = 5;
+ } else {
+ y = random.nextInt(originalHeight - jigsawHeight) + 5;
+ }
+ String key = null;
+ if (captchaAesStatus) {
+ key = AESUtil.getKey();
+ }
+ return new PointVO(x, y, key);
+ }
+
+ /**
+ * @param oriImage 原图
+ * @param templateImage 模板图
+ * @param newImage 新抠出的小图
+ * @param x 随机扣取坐标X
+ * @param y 随机扣取坐标y
+ * @throws Exception
+ */
+ private static void cutByTemplate(BufferedImage oriImage, BufferedImage templateImage, BufferedImage newImage, int x, int y) {
+ //临时数组遍历用于高斯模糊存周边像素值
+ int[][] martrix = new int[3][3];
+ int[] values = new int[9];
+
+ int xLength = templateImage.getWidth();
+ int yLength = templateImage.getHeight();
+ // 模板图像宽度
+ for (int i = 0; i < xLength; i++) {
+ // 模板图片高度
+ for (int j = 0; j < yLength; j++) {
+ // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
+ int rgb = templateImage.getRGB(i, j);
+ if (rgb < 0) {
+ newImage.setRGB(i, j, oriImage.getRGB(x + i, y + j));
+
+ //抠图区域高斯模糊
+ readPixel(oriImage, x + i, y + j, values);
+ fillMatrix(martrix, values);
+ oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
+ }
+
+ //防止数组越界判断
+ if (i == (xLength - 1) || j == (yLength - 1)) {
+ continue;
+ }
+ int rightRgb = templateImage.getRGB(i + 1, j);
+ int downRgb = templateImage.getRGB(i, j + 1);
+ //描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
+ if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0) || (rgb < 0 && downRgb >= 0)) {
+ newImage.setRGB(i, j, Color.white.getRGB());
+ oriImage.setRGB(x + i, y + j, Color.white.getRGB());
+ }
+ }
+ }
+
+ }
+
+
+ /**
+ * 干扰抠图处理
+ *
+ * @param oriImage 原图
+ * @param templateImage 模板图
+ * @param x 随机扣取坐标X
+ * @param y 随机扣取坐标y
+ * @throws Exception
+ */
+ private static void interferenceByTemplate(BufferedImage oriImage, BufferedImage templateImage, int x, int y) {
+ //临时数组遍历用于高斯模糊存周边像素值
+ int[][] martrix = new int[3][3];
+ int[] values = new int[9];
+
+ int xLength = templateImage.getWidth();
+ int yLength = templateImage.getHeight();
+ // 模板图像宽度
+ for (int i = 0; i < xLength; i++) {
+ // 模板图片高度
+ for (int j = 0; j < yLength; j++) {
+ // 如果模板图像当前像素点不是透明色 copy源文件信息到目标图片中
+ int rgb = templateImage.getRGB(i, j);
+ if (rgb < 0) {
+ //抠图区域高斯模糊
+ readPixel(oriImage, x + i, y + j, values);
+ fillMatrix(martrix, values);
+ oriImage.setRGB(x + i, y + j, avgMatrix(martrix));
+ }
+ //防止数组越界判断
+ if (i == (xLength - 1) || j == (yLength - 1)) {
+ continue;
+ }
+ int rightRgb = templateImage.getRGB(i + 1, j);
+ int downRgb = templateImage.getRGB(i, j + 1);
+ //描边处理,,取带像素和无像素的界点,判断该点是不是临界轮廓点,如果是设置该坐标像素是白色
+ if ((rgb >= 0 && rightRgb < 0) || (rgb < 0 && rightRgb >= 0) || (rgb >= 0 && downRgb < 0) || (rgb < 0 && downRgb >= 0)) {
+ oriImage.setRGB(x + i, y + j, Color.white.getRGB());
+ }
+ }
+ }
+
+ }
+
+ private static void readPixel(BufferedImage img, int x, int y, int[] pixels) {
+ int xStart = x - 1;
+ int yStart = y - 1;
+ int current = 0;
+ for (int i = xStart; i < 3 + xStart; i++) {
+ for (int j = yStart; j < 3 + yStart; j++) {
+ int tx = i;
+ if (tx < 0) {
+ tx = -tx;
+
+ } else if (tx >= img.getWidth()) {
+ tx = x;
+ }
+ int ty = j;
+ if (ty < 0) {
+ ty = -ty;
+ } else if (ty >= img.getHeight()) {
+ ty = y;
+ }
+ pixels[current++] = img.getRGB(tx, ty);
+
+ }
+ }
+ }
+
+ private static void fillMatrix(int[][] matrix, int[] values) {
+ int filled = 0;
+ for (int i = 0; i < matrix.length; i++) {
+ int[] x = matrix[i];
+ for (int j = 0; j < x.length; j++) {
+ x[j] = values[filled++];
+ }
+ }
+ }
+
+ private static int avgMatrix(int[][] matrix) {
+ int r = 0;
+ int g = 0;
+ int b = 0;
+ for (int i = 0; i < matrix.length; i++) {
+ int[] x = matrix[i];
+ for (int j = 0; j < x.length; j++) {
+ if (j == 1) {
+ continue;
+ }
+ Color c = new Color(x[j]);
+ r += c.getRed();
+ g += c.getGreen();
+ b += c.getBlue();
+ }
+ }
+ return new Color(r / 8, g / 8, b / 8).getRGB();
+ }
+
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java
new file mode 100644
index 000000000..7739b1a4b
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java
@@ -0,0 +1,50 @@
+package com.anji.captcha.service.impl;
+
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.util.CacheUtil;
+
+import java.util.Objects;
+
+/**
+ * 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。
+ * 如果应用是单点的,也没有使用redis,那默认使用内存。
+ * 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
+ *
+ * @author lide1202@hotmail.com
+ * @Title: 默认使用内存当缓存
+ * @date 2020-05-12
+ */
+public class CaptchaCacheServiceMemImpl implements CaptchaCacheService {
+ @Override
+ public void set(String key, String value, long expiresInSeconds) {
+
+ CacheUtil.set(key, value, expiresInSeconds);
+ }
+
+ @Override
+ public boolean exists(String key) {
+ return CacheUtil.exists(key);
+ }
+
+ @Override
+ public void delete(String key) {
+ CacheUtil.delete(key);
+ }
+
+ @Override
+ public String get(String key) {
+ return CacheUtil.get(key);
+ }
+
+ @Override
+ public Long increment(String key, long val) {
+ Long ret = Long.parseLong(Objects.requireNonNull(CacheUtil.get(key))) + val;
+ CacheUtil.set(key, ret + "", 0);
+ return ret;
+ }
+
+ @Override
+ public String type() {
+ return "local";
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java
new file mode 100644
index 000000000..ceb91391a
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java
@@ -0,0 +1,58 @@
+package com.anji.captcha.service.impl;
+
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.CaptchaService;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+/**
+ * Created by raodeming on 2020/5/26.
+ */
+@Slf4j
+public class CaptchaServiceFactory {
+
+ public static CaptchaService getInstance(Properties config) {
+ //先把所有CaptchaService初始化,通过init方法,实例字体等,add by lide1202@hotmail.com
+ /*try{
+ for(CaptchaService item: instances.values()){
+ item.init(config);
+ }
+ }catch (Exception e){
+ logger.warn("init captchaService fail:{}", e);
+ }*/
+
+ String captchaType = config.getProperty(Const.CAPTCHA_TYPE, "default");
+ CaptchaService ret = instances.get(captchaType);
+ if (ret == null) {
+ throw new RuntimeException("unsupported-[captcha.type]=" + captchaType);
+ }
+ ret.init(config);
+ return ret;
+ }
+
+ public static CaptchaCacheService getCache(String cacheType) {
+ return cacheService.get(cacheType);
+ }
+
+ public volatile static Map instances = new HashMap<>();
+ public volatile static Map cacheService = new HashMap<>();
+
+ static {
+ ServiceLoader cacheServices = ServiceLoader.load(CaptchaCacheService.class);
+ for (CaptchaCacheService item : cacheServices) {
+ cacheService.put(item.type(), item);
+ }
+ log.info("supported-captchaCache-service:{}", cacheService.keySet().toString());
+ ServiceLoader services = ServiceLoader.load(CaptchaService.class);
+ for (CaptchaService item : services) {
+ instances.put(item.captchaType(), item);
+ }
+ ;
+ log.info("supported-captchaTypes-service:{}", instances.keySet().toString());
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java
new file mode 100644
index 000000000..c5d338ffa
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java
@@ -0,0 +1,321 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.model.vo.PointVO;
+import com.anji.captcha.util.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.util.List;
+import java.util.*;
+
+/**
+ * 点选文字验证码
+ *
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public class ClickWordCaptchaServiceImpl extends AbstractCaptchaService {
+
+ public static String HAN_ZI = "\u7684\u4e00\u4e86\u662f\u6211\u4e0d\u5728\u4eba\u4eec\u6709\u6765\u4ed6\u8fd9\u4e0a\u7740\u4e2a\u5730\u5230\u5927\u91cc\u8bf4\u5c31\u53bb\u5b50\u5f97\u4e5f\u548c\u90a3\u8981\u4e0b\u770b\u5929\u65f6\u8fc7\u51fa\u5c0f\u4e48\u8d77\u4f60\u90fd\u628a\u597d\u8fd8\u591a\u6ca1\u4e3a\u53c8\u53ef\u5bb6\u5b66\u53ea\u4ee5\u4e3b\u4f1a\u6837\u5e74\u60f3\u751f\u540c\u8001\u4e2d\u5341\u4ece\u81ea\u9762\u524d\u5934\u9053\u5b83\u540e\u7136\u8d70\u5f88\u50cf\u89c1\u4e24\u7528\u5979\u56fd\u52a8\u8fdb\u6210\u56de\u4ec0\u8fb9\u4f5c\u5bf9\u5f00\u800c\u5df1\u4e9b\u73b0\u5c71\u6c11\u5019\u7ecf\u53d1\u5de5\u5411\u4e8b\u547d\u7ed9\u957f\u6c34\u51e0\u4e49\u4e09\u58f0\u4e8e\u9ad8\u624b\u77e5\u7406\u773c\u5fd7\u70b9\u5fc3\u6218\u4e8c\u95ee\u4f46\u8eab\u65b9\u5b9e\u5403\u505a\u53eb\u5f53\u4f4f\u542c\u9769\u6253\u5462\u771f\u5168\u624d\u56db\u5df2\u6240\u654c\u4e4b\u6700\u5149\u4ea7\u60c5\u8def\u5206\u603b\u6761\u767d\u8bdd\u4e1c\u5e2d\u6b21\u4eb2\u5982\u88ab\u82b1\u53e3\u653e\u513f\u5e38\u6c14\u4e94\u7b2c\u4f7f\u5199\u519b\u5427\u6587\u8fd0\u518d\u679c\u600e\u5b9a\u8bb8\u5feb\u660e\u884c\u56e0\u522b\u98de\u5916\u6811\u7269\u6d3b\u90e8\u95e8\u65e0\u5f80\u8239\u671b\u65b0\u5e26\u961f\u5148\u529b\u5b8c\u5374\u7ad9\u4ee3\u5458\u673a\u66f4\u4e5d\u60a8\u6bcf\u98ce\u7ea7\u8ddf\u7b11\u554a\u5b69\u4e07\u5c11\u76f4\u610f\u591c\u6bd4\u9636\u8fde\u8f66\u91cd\u4fbf\u6597\u9a6c\u54ea\u5316\u592a\u6307\u53d8\u793e\u4f3c\u58eb\u8005\u5e72\u77f3\u6ee1\u65e5\u51b3\u767e\u539f\u62ff\u7fa4\u7a76\u5404\u516d\u672c\u601d\u89e3\u7acb\u6cb3\u6751\u516b\u96be\u65e9\u8bba\u5417\u6839\u5171\u8ba9\u76f8\u7814\u4eca\u5176\u4e66\u5750\u63a5\u5e94\u5173\u4fe1\u89c9\u6b65\u53cd\u5904\u8bb0\u5c06\u5343\u627e\u4e89\u9886\u6216\u5e08\u7ed3\u5757\u8dd1\u8c01\u8349\u8d8a\u5b57\u52a0\u811a\u7d27\u7231\u7b49\u4e60\u9635\u6015\u6708\u9752\u534a\u706b\u6cd5\u9898\u5efa\u8d76\u4f4d\u5531\u6d77\u4e03\u5973\u4efb\u4ef6\u611f\u51c6\u5f20\u56e2\u5c4b\u79bb\u8272\u8138\u7247\u79d1\u5012\u775b\u5229\u4e16\u521a\u4e14\u7531\u9001\u5207\u661f\u5bfc\u665a\u8868\u591f\u6574\u8ba4\u54cd\u96ea\u6d41\u672a\u573a\u8be5\u5e76\u5e95\u6df1\u523b\u5e73\u4f1f\u5fd9\u63d0\u786e\u8fd1\u4eae\u8f7b\u8bb2\u519c\u53e4\u9ed1\u544a\u754c\u62c9\u540d\u5440\u571f\u6e05\u9633\u7167\u529e\u53f2\u6539\u5386\u8f6c\u753b\u9020\u5634\u6b64\u6cbb\u5317\u5fc5\u670d\u96e8\u7a7f\u5185\u8bc6\u9a8c\u4f20\u4e1a\u83dc\u722c\u7761\u5174\u5f62\u91cf\u54b1\u89c2\u82e6\u4f53\u4f17\u901a\u51b2\u5408\u7834\u53cb\u5ea6\u672f\u996d\u516c\u65c1\u623f\u6781\u5357\u67aa\u8bfb\u6c99\u5c81\u7ebf\u91ce\u575a\u7a7a\u6536\u7b97\u81f3\u653f\u57ce\u52b3\u843d\u94b1\u7279\u56f4\u5f1f\u80dc\u6559\u70ed\u5c55\u5305\u6b4c\u7c7b\u6e10\u5f3a\u6570\u4e61\u547c\u6027\u97f3\u7b54\u54e5\u9645\u65e7\u795e\u5ea7\u7ae0\u5e2e\u5566\u53d7\u7cfb\u4ee4\u8df3\u975e\u4f55\u725b\u53d6\u5165\u5cb8\u6562\u6389\u5ffd\u79cd\u88c5\u9876\u6025\u6797\u505c\u606f\u53e5\u533a\u8863\u822c\u62a5\u53f6\u538b\u6162\u53d4\u80cc\u7ec6";
+
+ protected static String clickWordFontStr = "NotoSerif-Light.ttf";
+
+ protected Font clickWordFont;//点选文字字体
+
+ @Override
+ public String captchaType() {
+ return CaptchaTypeEnum.CLICKWORD.getCodeValue();
+ }
+
+ @Override
+ public void init(Properties config) {
+ super.init(config);
+ clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "SourceHanSansCN-Normal.otf");
+ try {
+ int size = Integer.parseInt(config.getProperty(Const.CAPTCHA_FONT_SIZE,HAN_ZI_SIZE+""));
+
+ if (clickWordFontStr.toLowerCase().endsWith(".ttf")
+ || clickWordFontStr.toLowerCase().endsWith(".ttc")
+ || clickWordFontStr.toLowerCase().endsWith(".otf")) {
+ this.clickWordFont = Font.createFont(Font.TRUETYPE_FONT,
+ Objects.requireNonNull(getClass().getResourceAsStream("/fonts/" + clickWordFontStr)))
+ .deriveFont(Font.BOLD, size);
+ } else {
+ int style = Integer.parseInt(config.getProperty(Const.CAPTCHA_FONT_STYLE,Font.BOLD+""));
+ this.clickWordFont = new Font(clickWordFontStr, style, size);
+ }
+ } catch (Exception ex) {
+ log.error("load font error:{}", ex);
+ }
+ this.wordTotalCount = Integer.parseInt(config.getProperty(Const.CAPTCHA_WORD_COUNT,"4"));
+ }
+
+ @Override
+ public void destroy(Properties config) {
+ log.info("start-clear-history-data-", captchaType());
+ }
+
+ @Override
+ public ResponseModel get(CaptchaVO captchaVO) {
+ ResponseModel r = super.get(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ BufferedImage bufferedImage = ImageUtils.getPicClick();
+ if (null == bufferedImage) {
+ log.error("滑动底图未初始化成功,请检查路径");
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+ }
+ CaptchaVO imageData = getImageData(bufferedImage);
+ if (imageData == null
+ || StrUtil.isBlank(imageData.getOriginalImageBase64())) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR);
+ }
+ return ResponseModel.successData(imageData);
+ }
+
+ @Override
+ public ResponseModel check(CaptchaVO captchaVO) {
+ ResponseModel r = super.check(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ //取坐标信息
+ String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
+ if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+ }
+ String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
+ //验证码只用一次,即刻失效
+ CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+ List point = null;
+ List point1 = null;
+ String pointJson = null;
+ /**
+ * [
+ * {
+ * "x": 85.0,
+ * "y": 34.0
+ * },
+ * {
+ * "x": 129.0,
+ * "y": 56.0
+ * },
+ * {
+ * "x": 233.0,
+ * "y": 27.0
+ * }
+ * ]
+ */
+ try {
+ point = JsonUtil.parseArray(s, PointVO.class);
+ //aes解密
+ pointJson = decrypt(captchaVO.getPointJson(), point.get(0).getSecretKey());
+ point1 = JsonUtil.parseArray(pointJson, PointVO.class);
+ } catch (Exception e) {
+ log.error("验证码坐标解析失败", e);
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ for (int i = 0; i < point.size(); i++) {
+ if (point.get(i).x - HAN_ZI_SIZE > point1.get(i).x
+ || point1.get(i).x > point.get(i).x + HAN_ZI_SIZE
+ || point.get(i).y - HAN_ZI_SIZE > point1.get(i).y
+ || point1.get(i).y > point.get(i).y + HAN_ZI_SIZE) {
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
+ }
+ }
+ //校验成功,将信息存入缓存
+ String secretKey = point.get(0).getSecretKey();
+ String value = null;
+ try {
+ value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey);
+ } catch (Exception e) {
+ log.error("AES加密失败", e);
+ afterValidateFail(captchaVO);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ String secondKey = String.format(REDIS_SECOND_CAPTCHA_KEY, value);
+ CaptchaServiceFactory.getCache(cacheType).set(secondKey, captchaVO.getToken(), EXPIRESIN_THREE);
+ captchaVO.setResult(true);
+ captchaVO.resetClientFlag();
+ return ResponseModel.successData(captchaVO);
+ }
+
+ @Override
+ public ResponseModel verification(CaptchaVO captchaVO) {
+ /*if (captchaVO == null) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+ }
+ if (StrUtil.isEmpty(captchaVO.getCaptchaVerification())) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVerification");
+ }*/
+ ResponseModel r = super.verification(captchaVO);
+ if (!validatedReq(r)) {
+ return r;
+ }
+ try {
+ String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+ if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+ }
+ //二次校验取值后,即刻失效
+ CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+ } catch (Exception e) {
+ log.error("验证码坐标解析失败", e);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ return ResponseModel.success();
+ }
+
+ public int getWordTotalCount() {
+ return wordTotalCount;
+ }
+
+ public void setWordTotalCount(int wordTotalCount) {
+ this.wordTotalCount = wordTotalCount;
+ }
+
+ public boolean isFontColorRandom() {
+ return fontColorRandom;
+ }
+
+ public void setFontColorRandom(boolean fontColorRandom) {
+ this.fontColorRandom = fontColorRandom;
+ }
+
+ /**
+ * 点选文字 字体总个数
+ */
+ private int wordTotalCount = 4;
+ /**
+ * 点选文字 字体颜色是否随机
+ */
+ private boolean fontColorRandom = Boolean.TRUE;
+
+ private CaptchaVO getImageData(BufferedImage backgroundImage) {
+ CaptchaVO dataVO = new CaptchaVO();
+ List wordList = new ArrayList();
+ List pointList = new ArrayList();
+
+ Graphics backgroundGraphics = backgroundImage.getGraphics();
+ int width = backgroundImage.getWidth();
+ int height = backgroundImage.getHeight();
+
+ int wordCount = getWordTotalCount();
+ //定义随机1到arr.length某一个字不参与校验
+ int num = RandomUtils.getRandomInt(1, wordCount);
+ Set currentWords = getRandomWords(wordCount);
+ String secretKey = null;
+ if (captchaAesStatus) {
+ secretKey = AESUtil.getKey();
+ }
+ /*for (int i = 0; i < wordCount; i++) {
+ String word;
+ do {
+ word = RandomUtils.getRandomHan(HAN_ZI);
+ currentWords.add(word);
+ } while (!currentWords.contains(word));*/
+ int i = 0;
+ for (String word : currentWords) {
+ //随机字体坐标
+ PointVO point = randomWordPoint(width, height, i, wordCount);
+ point.setSecretKey(secretKey);
+ //随机字体颜色
+ if (isFontColorRandom()) {
+ backgroundGraphics.setColor(new Color(RandomUtils.getRandomInt(1, 255),
+ RandomUtils.getRandomInt(1, 255), RandomUtils.getRandomInt(1, 255)));
+ } else {
+ backgroundGraphics.setColor(Color.BLACK);
+ }
+ //设置角度
+ AffineTransform affineTransform = new AffineTransform();
+ affineTransform.rotate(Math.toRadians(RandomUtils.getRandomInt(-45, 45)), 0, 0);
+ Font rotatedFont = clickWordFont.deriveFont(affineTransform);
+ backgroundGraphics.setFont(rotatedFont);
+ backgroundGraphics.drawString(word, point.getX(), point.getY());
+
+ if ((num - 1) != i) {
+ wordList.add(word);
+ pointList.add(point);
+ }
+ i++;
+ }
+
+ backgroundGraphics.setFont(waterMarkFont);
+ backgroundGraphics.setColor(Color.white);
+ backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7);
+
+ //创建合并图片
+ BufferedImage combinedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ Graphics combinedGraphics = combinedImage.getGraphics();
+ combinedGraphics.drawImage(backgroundImage, 0, 0, null);
+
+ dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(backgroundImage).replaceAll("\r|\n", ""));
+ //pointList信息不传到前端,只做后端check校验
+ //dataVO.setPointList(pointList);
+ dataVO.setWordList(wordList);
+ dataVO.setToken(RandomUtils.getUUID());
+ dataVO.setSecretKey(secretKey);
+ //将坐标信息存入redis中
+ String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
+ CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(pointList), EXPIRESIN_SECONDS);
+// base64StrToImage(getImageToBase64Str(backgroundImage), "D:\\点击.png");
+ return dataVO;
+ }
+
+ private Set getRandomWords(int wordCount) {
+ Set words = new HashSet<>();
+ int size = HAN_ZI.length();
+ for (; ; ) {
+ String t = HAN_ZI.charAt(RandomUtils.getRandomInt(size)) + "";
+ words.add(t);
+ if (words.size() >= wordCount) {
+ break;
+ }
+ }
+ return words;
+ }
+
+ /**
+ * 随机字体循环排序下标
+ *
+ * @param imageWidth 图片宽度
+ * @param imageHeight 图片高度
+ * @param wordSortIndex 字体循环排序下标(i)
+ * @param wordCount 字数量
+ * @return
+ */
+ private static PointVO randomWordPoint(int imageWidth, int imageHeight, int wordSortIndex, int wordCount) {
+ int avgWidth = imageWidth / (wordCount + 1);
+ int x, y;
+ if (avgWidth < HAN_ZI_SIZE_HALF) {
+ x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, imageWidth);
+ } else {
+ if (wordSortIndex == 0) {
+ x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF);
+ } else {
+ x = RandomUtils.getRandomInt(avgWidth * wordSortIndex + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF);
+ }
+ }
+ y = RandomUtils.getRandomInt(HAN_ZI_SIZE, imageHeight - HAN_ZI_SIZE);
+ return new PointVO(x, y, null);
+ }
+
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java
new file mode 100644
index 000000000..e9843765a
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java
@@ -0,0 +1,100 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+@Slf4j
+public class DefaultCaptchaServiceImpl extends AbstractCaptchaService{
+
+ @Override
+ public String captchaType() {
+ return "default";
+ }
+
+ @Override
+ public void init(Properties config) {
+ for (String s : CaptchaServiceFactory.instances.keySet()) {
+ if(captchaType().equals(s)){
+ continue;
+ }
+ getService(s).init(config);
+ }
+ }
+
+ @Override
+ public void destroy(Properties config) {
+ for (String s : CaptchaServiceFactory.instances.keySet()) {
+ if(captchaType().equals(s)){
+ continue;
+ }
+ getService(s).destroy(config);
+ }
+ }
+
+ private CaptchaService getService(String captchaType){
+ return CaptchaServiceFactory.instances.get(captchaType);
+ }
+
+ @Override
+ public ResponseModel get(CaptchaVO captchaVO) {
+ if (captchaVO == null) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+ }
+ if (StrUtil.isEmpty(captchaVO.getCaptchaType())) {
+ return RepCodeEnum.NULL_ERROR.parseError("类型");
+ }
+ return getService(captchaVO.getCaptchaType()).get(captchaVO);
+ }
+
+ @Override
+ public ResponseModel check(CaptchaVO captchaVO) {
+ if (captchaVO == null) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+ }
+ if (StrUtil.isEmpty(captchaVO.getCaptchaType())) {
+ return RepCodeEnum.NULL_ERROR.parseError("类型");
+ }
+ if (StrUtil.isEmpty(captchaVO.getToken())) {
+ return RepCodeEnum.NULL_ERROR.parseError("token");
+ }
+ return getService(captchaVO.getCaptchaType()).check(captchaVO);
+ }
+
+ @Override
+ public ResponseModel verification(CaptchaVO captchaVO) {
+ if (captchaVO == null) {
+ return RepCodeEnum.NULL_ERROR.parseError("captchaVO");
+ }
+ if (StrUtil.isEmpty(captchaVO.getCaptchaVerification())) {
+ return RepCodeEnum.NULL_ERROR.parseError("二次校验参数");
+ }
+ try {
+ String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification());
+ if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
+ }
+ //二次校验取值后,即刻失效
+ CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
+ } catch (Exception e) {
+ log.error("验证码坐标解析失败", e);
+ return ResponseModel.errorMsg(e.getMessage());
+ }
+ return ResponseModel.success();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java
new file mode 100644
index 000000000..a477db2f9
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java
@@ -0,0 +1,154 @@
+package com.anji.captcha.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.model.common.RepCodeEnum;
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaCacheService;
+
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * @author WongBin
+ * @date 2021/1/21
+ */
+public interface FrequencyLimitHandler {
+
+ String LIMIT_KEY = "AJ.CAPTCHA.REQ.LIMIT-%s-%s";
+
+ /**
+ * get 接口限流
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel validateGet(CaptchaVO captchaVO);
+
+ /**
+ * check接口限流
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel validateCheck(CaptchaVO captchaVO);
+
+ /**
+ * verify接口限流
+ *
+ * @param captchaVO
+ * @return
+ */
+ ResponseModel validateVerify(CaptchaVO captchaVO);
+
+
+ /***
+ * 验证码接口限流:
+ * 客户端ClientUid 组件实例化时设置一次,如:场景码+UUID,客户端可以本地缓存,保证一个组件只有一个值
+ *
+ * 针对同一个客户端的请求,做如下限制:
+ * get
+ * 1分钟内check失败5次,锁定5分钟
+ * 1分钟内不能超过120次。
+ * check:
+ * 1分钟内不超过600次
+ * verify:
+ * 1分钟内不超过600次
+ */
+ class DefaultLimitHandler implements FrequencyLimitHandler {
+ private Properties config;
+ private CaptchaCacheService cacheService;
+
+ public DefaultLimitHandler(Properties config, CaptchaCacheService cacheService) {
+ this.config = config;
+ this.cacheService = cacheService;
+ }
+
+ private String getClientCId(CaptchaVO input, String type) {
+ return String.format(LIMIT_KEY, type, input.getClientUid());
+ }
+
+ @Override
+ public ResponseModel validateGet(CaptchaVO d) {
+ // 无客户端身份标识,不限制
+ if (StrUtil.isEmpty(d.getClientUid())) {
+ return null;
+ }
+ String getKey = getClientCId(d, "GET");
+ String lockKey = getClientCId(d, "LOCK");
+ // 失败次数过多,锁定
+ if (Objects.nonNull(cacheService.get(lockKey))) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LOCK_GET_ERROR);
+ }
+ String getCnts = cacheService.get(getKey);
+ if (Objects.isNull(getCnts)) {
+ cacheService.set(getKey, "1", 60);
+ getCnts = "1";
+ }
+ cacheService.increment(getKey, 1);
+ // 1分钟内请求次数过多
+ if (Long.parseLong(getCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_MINUTE_LIMIT, "120"))) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_GET_ERROR);
+ }
+
+ // 失败次数验证
+ String failKey = getClientCId(d, "FAIL");
+ String failCnts = cacheService.get(failKey);
+ // 没有验证失败,通过校验
+ if (Objects.isNull(failCnts)) {
+ return null;
+ }
+ // 1分钟内失败5次
+ if (Long.parseLong(failCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_LIMIT, "5"))) {
+ // get接口锁定5分钟
+ cacheService.set(lockKey, "1", Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_SECONDS, "300")));
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LOCK_GET_ERROR);
+ }
+ return null;
+ }
+
+ @Override
+ public ResponseModel validateCheck(CaptchaVO d) {
+ // 无客户端身份标识,不限制
+ if (StrUtil.isEmpty(d.getClientUid())) {
+ return null;
+ }
+ /*String getKey = getClientCId(d, "GET");
+ if(Objects.isNull(cacheService.get(getKey))){
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_INVALID);
+ }*/
+ String key = getClientCId(d, "CHECK");
+ String v = cacheService.get(key);
+ if (Objects.isNull(v)) {
+ cacheService.set(key, "1", 60);
+ v = "1";
+ }
+ cacheService.increment(key, 1);
+ if (Long.parseLong(v) > Long.parseLong(config.getProperty(Const.REQ_CHECK_MINUTE_LIMIT, "600"))) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_CHECK_ERROR);
+ }
+ return null;
+ }
+
+ @Override
+ public ResponseModel validateVerify(CaptchaVO d) {
+ /*String getKey = getClientCId(d, "GET");
+ if(Objects.isNull(cacheService.get(getKey))){
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_INVALID);
+ }*/
+ String key = getClientCId(d, "VERIFY");
+ String v = cacheService.get(key);
+ if (Objects.isNull(v)) {
+ cacheService.set(key, "1", 60);
+ v = "1";
+ }
+ cacheService.increment(key, 1);
+ if (Long.parseLong(v) > Long.parseLong(config.getProperty(Const.REQ_VALIDATE_MINUTE_LIMIT, "600"))) {
+ return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_VERIFY_ERROR);
+ }
+ return null;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java
new file mode 100644
index 000000000..07cc86f54
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java
@@ -0,0 +1,150 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+
+import cn.hutool.core.util.StrUtil;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+
+public class AESUtil {
+ //算法
+ private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";
+
+ /**
+ * 获取随机key
+ *
+ * @return
+ */
+ public static String getKey() {
+ return RandomUtils.getRandomString(16);
+ }
+
+
+ /**
+ * 将byte[]转为各种进制的字符串
+ *
+ * @param bytes byte[]
+ * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
+ * @return 转换后的字符串
+ */
+ public static String binary(byte[] bytes, int radix) {
+ return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
+ }
+
+ /**
+ * base 64 encode
+ *
+ * @param bytes 待编码的byte[]
+ * @return 编码后的base 64 code
+ */
+ public static String base64Encode(byte[] bytes) {
+ //return Base64.encodeBase64String(bytes);
+ return Base64.getEncoder().encodeToString(bytes);
+ }
+
+ /**
+ * base 64 decode
+ *
+ * @param base64Code 待解码的base 64 code
+ * @return 解码后的byte[]
+ * @throws Exception
+ */
+ public static byte[] base64Decode(String base64Code) throws Exception {
+ Base64.Decoder decoder = Base64.getDecoder();
+ return StrUtil.isEmpty(base64Code) ? null : decoder.decode(base64Code);
+ }
+
+
+ /**
+ * AES加密
+ *
+ * @param content 待加密的内容
+ * @param encryptKey 加密密钥
+ * @return 加密后的byte[]
+ * @throws Exception
+ */
+ public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {
+ KeyGenerator kgen = KeyGenerator.getInstance("AES");
+ kgen.init(128);
+ Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
+
+ return cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+
+ /**
+ * AES加密为base 64 code
+ *
+ * @param content 待加密的内容
+ * @param encryptKey 加密密钥
+ * @return 加密后的base 64 code
+ * @throws Exception
+ */
+ public static String aesEncrypt(String content, String encryptKey) throws Exception {
+ if (StrUtil.isBlank(encryptKey)) {
+ return content;
+ }
+ return base64Encode(aesEncryptToBytes(content, encryptKey));
+ }
+
+ /**
+ * AES解密
+ *
+ * @param encryptBytes 待解密的byte[]
+ * @param decryptKey 解密密钥
+ * @return 解密后的String
+ * @throws Exception
+ */
+ public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {
+ KeyGenerator kgen = KeyGenerator.getInstance("AES");
+ kgen.init(128);
+
+ Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
+ byte[] decryptBytes = cipher.doFinal(encryptBytes);
+ return new String(decryptBytes);
+ }
+
+
+ /**
+ * 将base 64 code AES解密
+ *
+ * @param encryptStr 待解密的base 64 code
+ * @param decryptKey 解密密钥
+ * @return 解密后的string
+ * @throws Exception
+ */
+ public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
+ if (StrUtil.isBlank(decryptKey)) {
+ return encryptStr;
+ }
+ return StrUtil.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);
+ }
+
+ /**
+ * 测试
+ */
+ public static void main(String[] args) throws Exception {
+ String randomString = RandomUtils.getRandomString(16);
+ String content = "hahhahaahhahni";
+ System.out.println("加密前:" + content);
+ System.out.println("加密密钥和解密密钥:" + randomString);
+ String encrypt = aesEncrypt(content, randomString);
+ System.out.println("加密后:" + encrypt);
+ String decrypt = aesDecrypt(encrypt, randomString);
+ System.out.println("解密后:" + decrypt);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java
new file mode 100644
index 000000000..617993526
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java
@@ -0,0 +1,111 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.*;
+
+public final class CacheUtil {
+ private static final Logger logger = LoggerFactory.getLogger(CacheUtil.class);
+
+ private static final Map CACHE_MAP = new ConcurrentHashMap();
+
+ /**
+ * 缓存最大个数
+ */
+ private static Integer CACHE_MAX_NUMBER = 1000;
+
+ /**
+ * 初始化
+ *
+ * @param cacheMaxNumber 缓存最大个数
+ * @param second 定时任务 秒执行清除过期缓存
+ */
+ public static void init(int cacheMaxNumber, long second) {
+ CACHE_MAX_NUMBER = cacheMaxNumber;
+ if (second > 0L) {
+ /*Timer timer = new Timer();
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ refresh();
+ }
+ }, 0, second * 1000);*/
+ ScheduledExecutorService scheduledExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, "thd-captcha-cache-clean");
+ }
+ }, new ThreadPoolExecutor.CallerRunsPolicy());
+ scheduledExecutor.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ refresh();
+ }
+ }, 10, second, TimeUnit.SECONDS);
+ }
+ }
+
+ /**
+ * 缓存刷新,清除过期数据
+ */
+ public static void refresh() {
+ logger.debug("local缓存刷新,清除过期数据");
+ for (String key : CACHE_MAP.keySet()) {
+ exists(key);
+ }
+ }
+
+
+ public static void set(String key, String value, long expiresInSeconds) {
+ //设置阈值,达到即clear缓存
+ if (CACHE_MAP.size() > CACHE_MAX_NUMBER * 2) {
+ logger.info("CACHE_MAP达到阈值,clear map");
+ clear();
+ }
+ CACHE_MAP.put(key, value);
+ if (expiresInSeconds > 0) {
+ CACHE_MAP.put(key + "_HoldTime", System.currentTimeMillis() + expiresInSeconds * 1000);//缓存失效时间
+ }
+ }
+
+ public static void delete(String key) {
+ CACHE_MAP.remove(key);
+ CACHE_MAP.remove(key + "_HoldTime");
+ }
+
+ public static boolean exists(String key) {
+ Long cacheHoldTime = (Long) CACHE_MAP.get(key + "_HoldTime");
+ if (cacheHoldTime == null || cacheHoldTime == 0L) {
+ return false;
+ }
+ if (cacheHoldTime < System.currentTimeMillis()) {
+ delete(key);
+ return false;
+ }
+ return true;
+ }
+
+
+ public static String get(String key) {
+ if (exists(key)) {
+ return (String) CACHE_MAP.get(key);
+ }
+ return null;
+ }
+
+ /**
+ * 删除所有缓存
+ */
+ public static void clear() {
+ logger.debug("have clean all key !");
+ CACHE_MAP.clear();
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java
new file mode 100644
index 000000000..057caa921
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java
@@ -0,0 +1,120 @@
+package com.anji.captcha.util;
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+import java.io.*;
+import java.nio.file.Files;
+
+public abstract class FileCopyUtils {
+ public static final int BUFFER_SIZE = 4096;
+
+ public FileCopyUtils() {
+ }
+
+ public static int copy(File in, File out) throws IOException {
+ return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath()));
+ }
+
+ public static void copy(byte[] in, File out) throws IOException {
+ copy((InputStream) (new ByteArrayInputStream(in)), (OutputStream) Files.newOutputStream(out.toPath()));
+ }
+
+ public static byte[] copyToByteArray(File in) throws IOException {
+ return copyToByteArray(Files.newInputStream(in.toPath()));
+ }
+
+ public static int copy(InputStream in, OutputStream out) throws IOException {
+ int var2;
+ try {
+ var2 = StreamUtils.copy(in, out);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException var12) {
+ }
+
+ try {
+ out.close();
+ } catch (IOException var11) {
+ }
+
+ }
+
+ return var2;
+ }
+
+ public static void copy(byte[] in, OutputStream out) throws IOException {
+ try {
+ out.write(in);
+ } finally {
+ try {
+ out.close();
+ } catch (IOException var8) {
+ }
+
+ }
+
+ }
+
+ public static byte[] copyToByteArray(InputStream in) throws IOException {
+ if (in == null) {
+ return new byte[0];
+ } else {
+ ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+ copy((InputStream) in, (OutputStream) out);
+ return out.toByteArray();
+ }
+ }
+
+ public static int copy(Reader in, Writer out) throws IOException {
+ try {
+ int byteCount = 0;
+ char[] buffer = new char[4096];
+
+ int bytesRead;
+ for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+ out.write(buffer, 0, bytesRead);
+ }
+
+ out.flush();
+ return byteCount;
+ } finally {
+ try {
+ in.close();
+ } catch (IOException var15) {
+ }
+
+ try {
+ out.close();
+ } catch (IOException var14) {
+ }
+
+ }
+ }
+
+ public static void copy(String in, Writer out) throws IOException {
+ try {
+ out.write(in);
+ } finally {
+ try {
+ out.close();
+ } catch (IOException var8) {
+ }
+
+ }
+
+ }
+
+ public static String copyToString(Reader in) throws IOException {
+ if (in == null) {
+ return "";
+ } else {
+ StringWriter out = new StringWriter();
+ copy((Reader) in, (Writer) out);
+ return out.toString();
+ }
+ }
+}
+
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java
new file mode 100644
index 000000000..0e822cccd
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java
@@ -0,0 +1,169 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import cn.hutool.core.util.StrUtil;
+import com.anji.captcha.model.common.CaptchaBaseMapEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.Base64Utils;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+public class ImageUtils {
+ private static Map originalCacheMap = new ConcurrentHashMap(); //滑块底图
+ private static Map slidingBlockCacheMap = new ConcurrentHashMap(); //滑块
+ private static Map picClickCacheMap = new ConcurrentHashMap(); //点选文字
+ private static Map fileNameMap = new ConcurrentHashMap<>();
+
+ public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) {
+ //滑动拼图
+ if (StrUtil.isBlank(captchaOriginalPathJigsaw)) {
+ originalCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/original"));
+ slidingBlockCacheMap.putAll(getResourcesImagesFile("defaultImages/jigsaw/slidingBlock"));
+ } else {
+ originalCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "original"));
+ slidingBlockCacheMap.putAll(getImagesFile(captchaOriginalPathJigsaw + File.separator + "slidingBlock"));
+ }
+ //点选文字
+ if (StrUtil.isBlank(captchaOriginalPathClick)) {
+ picClickCacheMap.putAll(getResourcesImagesFile("defaultImages/pic-click"));
+ } else {
+ picClickCacheMap.putAll(getImagesFile(captchaOriginalPathClick));
+ }
+ fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
+ fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
+ fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
+ log.info("初始化底图:{}", JsonUtil.toJSONString(fileNameMap));
+ }
+
+ public static void cacheBootImage(Map originalMap, Map slidingBlockMap, Map picClickMap) {
+ originalCacheMap.putAll(originalMap);
+ slidingBlockCacheMap.putAll(slidingBlockMap);
+ picClickCacheMap.putAll(picClickMap);
+ fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
+ fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
+ fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
+ log.info("自定义resource底图:{}", JsonUtil.toJSONString(fileNameMap));
+ }
+
+
+ public static BufferedImage getOriginal() {
+ String[] strings = fileNameMap.get(CaptchaBaseMapEnum.ORIGINAL.getCodeValue());
+ if (null == strings || strings.length == 0) {
+ return null;
+ }
+ Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+ String s = originalCacheMap.get(strings[randomInt]);
+ return getBase64StrToImage(s);
+ }
+
+ public static String getslidingBlock() {
+ String[] strings = fileNameMap.get(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue());
+ if (null == strings || strings.length == 0) {
+ return null;
+ }
+ Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+ return slidingBlockCacheMap.get(strings[randomInt]);
+ }
+
+ public static BufferedImage getPicClick() {
+ String[] strings = fileNameMap.get(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue());
+ if (null == strings || strings.length == 0) {
+ return null;
+ }
+ Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
+ String s = picClickCacheMap.get(strings[randomInt]);
+ return getBase64StrToImage(s);
+ }
+
+ /**
+ * 图片转base64 字符串
+ *
+ * @param templateImage
+ * @return
+ */
+ public static String getImageToBase64Str(BufferedImage templateImage) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ ImageIO.write(templateImage, "png", baos);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ byte[] bytes = baos.toByteArray();
+
+ Base64.Encoder encoder = Base64.getEncoder();
+
+ return encoder.encodeToString(bytes).trim();
+ }
+
+ /**
+ * base64 字符串转图片
+ *
+ * @param base64String
+ * @return
+ */
+ public static BufferedImage getBase64StrToImage(String base64String) {
+ try {
+ Base64.Decoder decoder = Base64.getDecoder();
+ byte[] bytes = decoder.decode(base64String);
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+ return ImageIO.read(inputStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+
+ private static Map getResourcesImagesFile(String path) {
+ //默认提供六张底图
+ Map imgMap = new HashMap<>();
+ ClassLoader classLoader = ImageUtils.class.getClassLoader();
+ for (int i = 1; i <= 6; i++) {
+ InputStream resourceAsStream = classLoader.getResourceAsStream(path.concat("/").concat(String.valueOf(i).concat(".png")));
+ byte[] bytes = new byte[0];
+ try {
+ bytes = FileCopyUtils.copyToByteArray(resourceAsStream);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ String string = Base64Utils.encodeToString(bytes);
+ String filename = String.valueOf(i).concat(".png");
+ imgMap.put(filename, string);
+ }
+ return imgMap;
+ }
+
+ private static Map getImagesFile(String path) {
+ Map imgMap = new HashMap<>();
+ File file = new File(path);
+ if (!file.exists()) {
+ return new HashMap<>();
+ }
+ File[] files = file.listFiles();
+ Arrays.stream(files).forEach(item -> {
+ try {
+ FileInputStream fileInputStream = new FileInputStream(item);
+ byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream);
+ String string = Base64Utils.encodeToString(bytes);
+ imgMap.put(item.getName(), string);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ return imgMap;
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java
new file mode 100644
index 000000000..ee2e34894
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java
@@ -0,0 +1,73 @@
+package com.anji.captcha.util;
+
+import com.anji.captcha.model.vo.PointVO;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 替换掉fastjson,自定义实现相关方法
+ * note: 该实现不具有通用性,仅用于本项目。
+ *
+ * @author WongBin
+ * @date 2021/1/8
+ */
+@Slf4j
+public class JsonUtil {
+ public static List parseArray(String text, Class clazz) {
+ if (text == null) {
+ return null;
+ } else {
+ String[] arr = text.replaceFirst("\\[", "")
+ .replaceFirst("\\]", "").split("\\}");
+ List ret = new ArrayList<>(arr.length);
+ for (String s : arr) {
+ ret.add(parseObject(s, PointVO.class));
+ }
+ return ret;
+ }
+ }
+
+ public static PointVO parseObject(String text, Class clazz) {
+ if (text == null) {
+ return null;
+ }
+ /*if(!clazz.isAssignableFrom(PointVO.class)) {
+ throw new UnsupportedOperationException("不支持的输入类型:"
+ + clazz.getSimpleName());
+ }*/
+ try {
+ PointVO ret = clazz.newInstance();
+ return ret.parse(text);
+ } catch (Exception ex) {
+ log.error("json解析异常", ex);
+
+ }
+ return null;
+ }
+
+ public static String toJSONString(Object object) {
+ if (object == null) {
+ return "{}";
+ }
+ if (object instanceof PointVO) {
+ PointVO t = (PointVO) object;
+ return t.toJsonString();
+ }
+ if (object instanceof List) {
+ List list = (List) object;
+ StringBuilder buf = new StringBuilder("[");
+ list.forEach(t -> {
+ buf.append(t.toJsonString()).append(",");
+ });
+ return buf.deleteCharAt(buf.lastIndexOf(",")).append("]").toString();
+ }
+ if (object instanceof Map) {
+ return ((Map) object).entrySet().toString();
+ }
+ throw new UnsupportedOperationException("不支持的输入类型:"
+ + object.getClass().getSimpleName());
+ }
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java
new file mode 100644
index 000000000..3bb2f6ef6
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java
@@ -0,0 +1,42 @@
+package com.anji.captcha.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * @Title: MD5工具类
+ */
+public abstract class MD5Util {
+ /**
+ * 获取指定字符串的md5值
+ *
+ * @param dataStr 明文
+ * @return String
+ */
+ public static String md5(String dataStr) {
+ try {
+ MessageDigest m = MessageDigest.getInstance("MD5");
+ m.update(dataStr.getBytes(StandardCharsets.UTF_8));
+ byte[] s = m.digest();
+ StringBuilder result = new StringBuilder();
+ for (byte b : s) {
+ result.append(Integer.toHexString((0x000000FF & b) | 0xFFFFFF00).substring(6));
+ }
+ return result.toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /**
+ * 获取指定字符串的md5值, md5(str+salt)
+ *
+ * @param dataStr 明文
+ * @return String
+ */
+ public static String md5WithSalt(String dataStr, String salt) {
+ return md5(dataStr + salt);
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java
new file mode 100644
index 000000000..819cf2f30
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java
@@ -0,0 +1,96 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+
+public class RandomUtils {
+
+ /**
+ * 生成UUID
+ *
+ * @return
+ */
+ public static String getUUID() {
+ String uuid = UUID.randomUUID().toString();
+ uuid = uuid.replace("-", "");
+ return uuid;
+ }
+
+ /**
+ * 获取指定文字的随机中文
+ *
+ * @return
+ */
+ public static String getRandomHan(String hanZi) {
+ return hanZi.charAt(new Random().nextInt(hanZi.length())) + "";
+ }
+
+ public static int getRandomInt(int bound) {
+ return ThreadLocalRandom.current().nextInt(bound);
+ }
+
+ /**
+ * 获取随机中文
+ *
+ * @return
+ */
+ public static String getRandomHan() {
+ String str = "";
+ int highCode;
+ int lowCode;
+
+ Random random = new Random();
+
+ highCode = (176 + Math.abs(random.nextInt(39))); //B0 + 0~39(16~55) 一级汉字所占区
+ lowCode = (161 + Math.abs(random.nextInt(93))); //A1 + 0~93 每区有94个汉字
+
+ byte[] b = new byte[2];
+ b[0] = (Integer.valueOf(highCode)).byteValue();
+ b[1] = (Integer.valueOf(lowCode)).byteValue();
+
+ try {
+ str = new String(b, "GBK");
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ return str;
+ }
+
+ /**
+ * 随机范围内数字
+ *
+ * @param startNum
+ * @param endNum
+ * @return
+ */
+ public static Integer getRandomInt(int startNum, int endNum) {
+ return ThreadLocalRandom.current().nextInt(endNum - startNum) + startNum;
+ }
+
+ /**
+ * 获取随机字符串
+ *
+ * @param length
+ * @return
+ */
+ public static String getRandomString(int length) {
+ String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ Random random = new Random();
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < length; i++) {
+ int number = random.nextInt(62);
+ sb.append(str.charAt(number));
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java
new file mode 100644
index 000000000..df83a3254
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java
@@ -0,0 +1,138 @@
+package com.anji.captcha.util;
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+import java.io.*;
+import java.nio.charset.Charset;
+
+public abstract class StreamUtils {
+ public static final int BUFFER_SIZE = 4096;
+ private static final byte[] EMPTY_CONTENT = new byte[0];
+
+ public StreamUtils() {
+ }
+
+ public static byte[] copyToByteArray(InputStream in) throws IOException {
+ if (in == null) {
+ return new byte[0];
+ } else {
+ ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
+ copy((InputStream) in, out);
+ return out.toByteArray();
+ }
+ }
+
+ public static String copyToString(InputStream in, Charset charset) throws IOException {
+ if (in == null) {
+ return "";
+ } else {
+ StringBuilder out = new StringBuilder();
+ InputStreamReader reader = new InputStreamReader(in, charset);
+ char[] buffer = new char[4096];
+
+ int bytesRead;
+ while ((bytesRead = reader.read(buffer)) != -1) {
+ out.append(buffer, 0, bytesRead);
+ }
+
+ return out.toString();
+ }
+ }
+
+ public static void copy(byte[] in, OutputStream out) throws IOException {
+ out.write(in);
+ }
+
+ public static void copy(String in, Charset charset, OutputStream out) throws IOException {
+ Writer writer = new OutputStreamWriter(out, charset);
+ writer.write(in);
+ writer.flush();
+ }
+
+ public static int copy(InputStream in, OutputStream out) throws IOException {
+ int byteCount = 0;
+ byte[] buffer = new byte[4096];
+
+ int bytesRead;
+ for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+ out.write(buffer, 0, bytesRead);
+ }
+
+ out.flush();
+ return byteCount;
+ }
+
+ public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
+ long skipped = in.skip(start);
+ if (skipped < start) {
+ throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required");
+ } else {
+ long bytesToCopy = end - start + 1L;
+ byte[] buffer = new byte[4096];
+
+ while (bytesToCopy > 0L) {
+ int bytesRead = in.read(buffer);
+ if (bytesRead == -1) {
+ break;
+ }
+
+ if ((long) bytesRead <= bytesToCopy) {
+ out.write(buffer, 0, bytesRead);
+ bytesToCopy -= (long) bytesRead;
+ } else {
+ out.write(buffer, 0, (int) bytesToCopy);
+ bytesToCopy = 0L;
+ }
+ }
+
+ return end - start + 1L - bytesToCopy;
+ }
+ }
+
+ public static int drain(InputStream in) throws IOException {
+ byte[] buffer = new byte[4096];
+ int byteCount;
+ int bytesRead;
+ for (byteCount = 0; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
+ }
+
+ return byteCount;
+ }
+
+ public static InputStream emptyInput() {
+ return new ByteArrayInputStream(EMPTY_CONTENT);
+ }
+
+ public static InputStream nonClosing(InputStream in) {
+ return new NonClosingInputStream(in);
+ }
+
+ public static OutputStream nonClosing(OutputStream out) {
+ return new NonClosingOutputStream(out);
+ }
+
+ private static class NonClosingOutputStream extends FilterOutputStream {
+ public NonClosingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public void write(byte[] b, int off, int let) throws IOException {
+ this.out.write(b, off, let);
+ }
+
+ public void close() throws IOException {
+ }
+ }
+
+ private static class NonClosingInputStream extends FilterInputStream {
+ public NonClosingInputStream(InputStream in) {
+ super(in);
+ }
+
+ public void close() throws IOException {
+ }
+ }
+}
+
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService
new file mode 100644
index 000000000..f31fbb43b
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService
@@ -0,0 +1,3 @@
+com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl
+com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl
+com.anji.captcha.service.impl.DefaultCaptchaServiceImpl
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 8411d2cc3..12cf6229c 100644
--- a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -1 +1,2 @@
+com.anji.captcha.config.AjCaptchaAutoConfiguration
cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png
new file mode 100644
index 000000000..022aabf93
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png
new file mode 100644
index 000000000..914908e89
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png
new file mode 100644
index 000000000..f0f3ce581
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png
new file mode 100644
index 000000000..c5697f3cb
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png
new file mode 100644
index 000000000..e29e7a3c1
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png
new file mode 100644
index 000000000..2425f412d
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png
new file mode 100644
index 000000000..5ea54d482
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png
new file mode 100644
index 000000000..190502660
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png
new file mode 100644
index 000000000..b1482d48b
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png
new file mode 100644
index 000000000..cdbb0b18c
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png
new file mode 100644
index 000000000..bc69c9622
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png
new file mode 100644
index 000000000..0080a5465
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png
new file mode 100644
index 000000000..b07c3b404
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png
new file mode 100644
index 000000000..50dfe28ef
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png
new file mode 100644
index 000000000..15b38ad27
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png
new file mode 100644
index 000000000..e2e487bd4
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png
new file mode 100644
index 000000000..c34baa404
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png
new file mode 100644
index 000000000..0b3d11a27
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png
new file mode 100644
index 000000000..67797a11d
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png
new file mode 100644
index 000000000..c99fbcb03
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png
new file mode 100644
index 000000000..6a951d326
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png
new file mode 100644
index 000000000..a38ada504
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png
new file mode 100644
index 000000000..07af86a86
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png
new file mode 100644
index 000000000..95593759d
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png
new file mode 100644
index 000000000..cb1ebb63e
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png
new file mode 100644
index 000000000..106b4562b
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png
new file mode 100644
index 000000000..bcdbe7655
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png
new file mode 100644
index 000000000..ae94e09cf
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png
new file mode 100644
index 000000000..bef9318b5
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png
new file mode 100644
index 000000000..36cfbdec6
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf
new file mode 100644
index 000000000..f84e9feb3
Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf differ
diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt
new file mode 100644
index 000000000..719f68f0b
--- /dev/null
+++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt
@@ -0,0 +1,55 @@
+文泉驿是一个开源汉字字体项目
+
+由旅美学者房骞骞(FangQ)
+
+于2004年10月创建
+
+集中力量解决GNU/Linux
+
+高质量中文字体匮乏的状况
+
+目前,文泉驿已经开发并发布了
+
+第一个完整覆盖GB18030汉字
+
+(包含27000多个汉字)
+
+的多规格点阵汉字字型文件
+
+第一个覆盖GBK字符集的
+
+开源矢量字型文件(文泉驿正黑)
+
+并提供了目前包含字符数目最多的
+
+开源字体——GNU Unifont——中
+
+绝大多数中日韩文相关的符号
+
+这些字型文件已经逐渐成为
+
+主流Linux/Unix发行版
+
+中文桌面的首选中文字体
+
+目前Ubuntu、Fedora、Slackware
+
+Magic Linux、CDLinux
+
+使用文泉驿作为默认中文字体
+
+Debian、Gentoo、Mandriva
+
+ArchLinux、Frugalware
+
+则提供了官方源支持
+
+而FreeBSD则在其ports中有提供
+
+所以,今天我们所要分享的就是
+
+文泉驿正黑体
+
+可在Linux/UNIX,Windows
+
+Mac OS和嵌入式操作系统中使用
\ No newline at end of file
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
index e220d011d..e0b5e4bb5 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
@@ -92,8 +92,22 @@ public interface BaseMapperX extends BaseMapper {
entities.forEach(this::insert);
}
+ /**
+ * 批量插入,适合大量数据插入
+ *
+ * @param entities 实体们
+ * @param size 插入数量 Db.saveBatch 默认为1000
+ */
+ default void insertBatch(Collection entities, int size) {
+ Db.saveBatch(entities, size);
+ }
+
default void updateBatch(T update) {
update(update, new QueryWrapper<>());
}
+ default void updateBatch(Collection entities, int size) {
+ Db.updateBatchById(entities, size);
+ }
+
}
diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
index fbdd665e8..c10e602a7 100644
--- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
+++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
@@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.system.controller.admin.captcha;
+import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
@@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
@@ -24,24 +27,46 @@ import javax.servlet.http.HttpServletRequest;
@Api(tags = "管理后台 - 验证码")
@RestController("adminCaptchaController")
@RequestMapping("/system/captcha")
-public class CaptchaController extends com.anji.captcha.controller.CaptchaController {
+public class CaptchaController {
+
+ @Resource
+ private CaptchaService captchaService;
@PostMapping({"/get"})
@ApiOperation("获得验证码")
@PermitAll
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
- @Override
public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
- return super.get(data, request);
+ assert request.getRemoteHost() != null;
+ data.setBrowserInfo(getRemoteId(request));
+ return captchaService.get(data);
}
@PostMapping("/check")
@ApiOperation("校验验证码")
@PermitAll
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
- @Override
public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
- return super.check(data, request);
+ data.setBrowserInfo(getRemoteId(request));
+ return captchaService.check(data);
+ }
+
+ public static String getRemoteId(HttpServletRequest request) {
+ String xfwd = request.getHeader("X-Forwarded-For");
+ String ip = getRemoteIpFromXfwd(xfwd);
+ String ua = request.getHeader("user-agent");
+ if (StrUtil.isNotBlank(ip)) {
+ return ip + ua;
+ }
+ return request.getRemoteAddr() + ua;
+ }
+
+ private static String getRemoteIpFromXfwd(String xfwd) {
+ if (StrUtil.isNotBlank(xfwd)) {
+ String[] ipList = xfwd.split(",");
+ return StrUtil.trim(ipList[0]);
+ }
+ return null;
}
}
diff --git a/yudao-ui-admin-vue3/README.md b/yudao-ui-admin-vue3/README.md
index 73561ec9b..79a9505ac 100644
--- a/yudao-ui-admin-vue3/README.md
+++ b/yudao-ui-admin-vue3/README.md
@@ -34,7 +34,7 @@
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.28 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 9.10.0 |
-| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7 |
+| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.9 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6 |
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
diff --git a/yudao-ui-admin-vue3/package.json b/yudao-ui-admin-vue3/package.json
index ac2e72ded..20f02859f 100644
--- a/yudao-ui-admin-vue3/package.json
+++ b/yudao-ui-admin-vue3/package.json
@@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
- "version": "1.6.6-snapshot.1901",
+ "version": "1.6.6-snapshot.1911",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@@ -50,14 +50,14 @@
"vue-i18n": "9.2.2",
"vue-router": "^4.1.6",
"vue-types": "^5.0.2",
- "vxe-table": "^4.3.7",
+ "vxe-table": "^4.3.9",
"web-storage-cache": "^1.1.1",
"xe-utils": "^3.5.7"
},
"devDependencies": {
- "@commitlint/cli": "^17.4.0",
+ "@commitlint/cli": "^17.4.1",
"@commitlint/config-conventional": "^17.4.0",
- "@iconify/json": "^2.2.2",
+ "@iconify/json": "^2.2.5",
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0",
@@ -66,8 +66,8 @@
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
- "@typescript-eslint/eslint-plugin": "^5.48.0",
- "@typescript-eslint/parser": "^5.48.0",
+ "@typescript-eslint/eslint-plugin": "^5.48.1",
+ "@typescript-eslint/parser": "^5.48.1",
"@vitejs/plugin-legacy": "^3.0.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
@@ -75,14 +75,14 @@
"consola": "^2.15.3",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
- "eslint-define-config": "^1.13.0",
+ "eslint-define-config": "^1.14.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.8.0",
"lint-staged": "^13.1.0",
- "postcss": "^8.4.20",
+ "postcss": "^8.4.21",
"postcss-html": "^1.5.0",
"postcss-scss": "^4.0.6",
- "prettier": "^2.8.1",
+ "prettier": "^2.8.2",
"rimraf": "^3.0.2",
"rollup": "^3.9.1",
"sass": "^1.57.1",
@@ -91,7 +91,7 @@
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-recommended": "^9.0.0",
"stylelint-config-standard": "^29.0.0",
- "stylelint-order": "^5.0.0",
+ "stylelint-order": "^6.0.1",
"terser": "^5.16.1",
"typescript": "4.9.4",
"vite": "4.0.4",
@@ -104,7 +104,7 @@
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vite-plugin-windicss": "^1.8.10",
- "vue-tsc": "^1.0.22",
+ "vue-tsc": "^1.0.24",
"windicss": "^3.5.6"
},
"engines": {
diff --git a/yudao-ui-admin-vue3/pnpm-lock.yaml b/yudao-ui-admin-vue3/pnpm-lock.yaml
index cb31c2c89..af51c2bc8 100644
--- a/yudao-ui-admin-vue3/pnpm-lock.yaml
+++ b/yudao-ui-admin-vue3/pnpm-lock.yaml
@@ -1,10 +1,10 @@
lockfileVersion: 5.4
specifiers:
- '@commitlint/cli': ^17.4.0
+ '@commitlint/cli': ^17.4.1
'@commitlint/config-conventional': ^17.4.0
'@iconify/iconify': ^3.0.1
- '@iconify/json': ^2.2.2
+ '@iconify/json': ^2.2.5
'@intlify/unplugin-vue-i18n': ^0.8.1
'@purge-icons/generated': ^0.9.0
'@types/intro.js': ^5.1.0
@@ -13,8 +13,8 @@ specifiers:
'@types/nprogress': ^0.2.0
'@types/qrcode': ^1.5.0
'@types/qs': ^6.9.7
- '@typescript-eslint/eslint-plugin': ^5.48.0
- '@typescript-eslint/parser': ^5.48.0
+ '@typescript-eslint/eslint-plugin': ^5.48.1
+ '@typescript-eslint/parser': ^5.48.1
'@vitejs/plugin-legacy': ^3.0.1
'@vitejs/plugin-vue': ^4.0.0
'@vitejs/plugin-vue-jsx': ^3.0.0
@@ -34,7 +34,7 @@ specifiers:
element-plus: 2.2.28
eslint: ^8.31.0
eslint-config-prettier: ^8.6.0
- eslint-define-config: ^1.13.0
+ eslint-define-config: ^1.14.0
eslint-plugin-prettier: ^4.2.1
eslint-plugin-vue: ^9.8.0
intro.js: ^6.0.0
@@ -44,10 +44,10 @@ specifiers:
mitt: ^3.0.0
nprogress: ^0.2.0
pinia: ^2.0.28
- postcss: ^8.4.20
+ postcss: ^8.4.21
postcss-html: ^1.5.0
postcss-scss: ^4.0.6
- prettier: ^2.8.1
+ prettier: ^2.8.2
qrcode: ^1.5.1
qs: ^6.11.0
rimraf: ^3.0.2
@@ -58,7 +58,7 @@ specifiers:
stylelint-config-prettier: ^9.0.4
stylelint-config-recommended: ^9.0.0
stylelint-config-standard: ^29.0.0
- stylelint-order: ^5.0.0
+ stylelint-order: ^6.0.1
terser: ^5.16.1
typescript: 4.9.4
url: ^0.11.0
@@ -75,9 +75,9 @@ specifiers:
vue: 3.2.45
vue-i18n: 9.2.2
vue-router: ^4.1.6
- vue-tsc: ^1.0.22
+ vue-tsc: ^1.0.24
vue-types: ^5.0.2
- vxe-table: ^4.3.7
+ vxe-table: ^4.3.9
web-storage-cache: ^1.1.1
windicss: ^3.5.6
xe-utils: ^3.5.7
@@ -109,14 +109,14 @@ dependencies:
vue-i18n: 9.2.2_vue@3.2.45
vue-router: 4.1.6_vue@3.2.45
vue-types: 5.0.2_vue@3.2.45
- vxe-table: 4.3.7_vue@3.2.45+xe-utils@3.5.7
+ vxe-table: 4.3.9_vue@3.2.45+xe-utils@3.5.7
web-storage-cache: 1.1.1
xe-utils: 3.5.7
devDependencies:
- '@commitlint/cli': 17.4.0_@types+node@18.11.18
+ '@commitlint/cli': 17.4.1
'@commitlint/config-conventional': 17.4.0
- '@iconify/json': 2.2.2
+ '@iconify/json': 2.2.5
'@intlify/unplugin-vue-i18n': 0.8.1_vue-i18n@9.2.2
'@purge-icons/generated': 0.9.0
'@types/intro.js': 5.1.0
@@ -125,23 +125,23 @@ devDependencies:
'@types/nprogress': 0.2.0
'@types/qrcode': 1.5.0
'@types/qs': 6.9.7
- '@typescript-eslint/eslint-plugin': 5.48.0_k73wpmdolxikpyqun3p36akaaq
- '@typescript-eslint/parser': 5.48.0_iukboom6ndih5an6iafl45j2fe
+ '@typescript-eslint/eslint-plugin': 5.48.1_3jon24igvnqaqexgwtxk6nkpse
+ '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
'@vitejs/plugin-legacy': 3.0.1_terser@5.16.1+vite@4.0.4
'@vitejs/plugin-vue': 4.0.0_vite@4.0.4+vue@3.2.45
'@vitejs/plugin-vue-jsx': 3.0.0_vite@4.0.4+vue@3.2.45
- autoprefixer: 10.4.13_postcss@8.4.20
+ autoprefixer: 10.4.13_postcss@8.4.21
consola: 2.15.3
eslint: 8.31.0
eslint-config-prettier: 8.6.0_eslint@8.31.0
- eslint-define-config: 1.13.0
- eslint-plugin-prettier: 4.2.1_32m5uc2milwdw3tnkcq5del26y
+ eslint-define-config: 1.14.0
+ eslint-plugin-prettier: 4.2.1_iu5s7nk6dw7o3tajefwfiqfmge
eslint-plugin-vue: 9.8.0_eslint@8.31.0
lint-staged: 13.1.0
- postcss: 8.4.20
+ postcss: 8.4.21
postcss-html: 1.5.0
- postcss-scss: 4.0.6_postcss@8.4.20
- prettier: 2.8.1
+ postcss-scss: 4.0.6_postcss@8.4.21
+ prettier: 2.8.2
rimraf: 3.0.2
rollup: 3.9.1
sass: 1.57.1
@@ -150,7 +150,7 @@ devDependencies:
stylelint-config-prettier: 9.0.4_stylelint@14.16.1
stylelint-config-recommended: 9.0.0_stylelint@14.16.1
stylelint-config-standard: 29.0.0_stylelint@14.16.1
- stylelint-order: 5.0.0_stylelint@14.16.1
+ stylelint-order: 6.0.1_stylelint@14.16.1
terser: 5.16.1
typescript: 4.9.4
vite: 4.0.4_zxbrnrc4iyldik6mikh3pswz4i
@@ -163,7 +163,7 @@ devDependencies:
vite-plugin-svg-icons: 2.0.1_vite@4.0.4
vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.4
vite-plugin-windicss: 1.8.10_vite@4.0.4
- vue-tsc: 1.0.22_typescript@4.9.4
+ vue-tsc: 1.0.24_typescript@4.9.4
windicss: 3.5.6
packages:
@@ -506,14 +506,14 @@ packages:
'@babel/helper-validator-identifier': 7.19.1
to-fast-properties: 2.0.0
- /@commitlint/cli/17.4.0_@types+node@18.11.18:
- resolution: {integrity: sha512-SEY4sYe8yVlgxPP7X0wJb96DBAGBPsCsy6QbqJt/UECbIAjDeDV5xXBV4jnS7T/qMC10sk6Ub9kDhEX0VWvblw==}
+ /@commitlint/cli/17.4.1:
+ resolution: {integrity: sha512-W8OJwz+izY+fVwyUt1HveCDmABMZNRVZHSVPw/Bh9Y62tp11SmmQaycgbsYLMiMy7JGn4mAJqEGlSHS9Uti9ZQ==}
engines: {node: '>=v14'}
hasBin: true
dependencies:
'@commitlint/format': 17.4.0
'@commitlint/lint': 17.4.0
- '@commitlint/load': 17.4.0_@types+node@18.11.18
+ '@commitlint/load': 17.4.1
'@commitlint/read': 17.4.0
'@commitlint/types': 17.4.0
execa: 5.1.1
@@ -524,7 +524,6 @@ packages:
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
- - '@types/node'
dev: true
/@commitlint/config-conventional/17.4.0:
@@ -585,14 +584,15 @@ packages:
'@commitlint/types': 17.4.0
dev: true
- /@commitlint/load/17.4.0_@types+node@18.11.18:
- resolution: {integrity: sha512-wDKNvAJqukqZqKmhRlf3KNo/12QGo1AQcd80EbV01SxtGvyHOsJ/g+/IbrZpopZv8rvzmEVktcpfDYH6ITepFA==}
+ /@commitlint/load/17.4.1:
+ resolution: {integrity: sha512-6A7/LhIaQpL4ieciIDcVvK2d5z/UI1GBrtDaHm6sQSCL0265clB2/F7XKQNTJHXv9yG4LByT2r+QCpM4GugIfw==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/config-validator': 17.4.0
'@commitlint/execute-rule': 17.4.0
'@commitlint/resolve-extends': 17.4.0
'@commitlint/types': 17.4.0
+ '@types/node': 18.11.18
chalk: 4.1.2
cosmiconfig: 8.0.0
cosmiconfig-typescript-loader: 4.2.0_bxtyj3et3xbsdyxhh3oblnfbj4
@@ -605,7 +605,6 @@ packages:
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
- - '@types/node'
dev: true
/@commitlint/message/17.4.0:
@@ -682,14 +681,14 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: true
- /@csstools/selector-specificity/2.0.2_2xshye3abirqjlplmebvmaxyna:
+ /@csstools/selector-specificity/2.0.2_wajs5nedgkikc5pcuwett7legi:
resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==}
engines: {node: ^12 || ^14 || >=16}
peerDependencies:
postcss: ^8.2
postcss-selector-parser: ^6.0.10
dependencies:
- postcss: 8.4.20
+ postcss: 8.4.21
postcss-selector-parser: 6.0.11
dev: true
@@ -964,8 +963,8 @@ packages:
dependencies:
'@iconify/types': 2.0.0
- /@iconify/json/2.2.2:
- resolution: {integrity: sha512-G9HVJz3uvQGNEirk9oI7xYnWb7ygEfTUZ+PVp81qgNp8bu5UOtXaxjTGw78NyNAC2OlryH5tSEp95Dqbt4LLQQ==}
+ /@iconify/json/2.2.5:
+ resolution: {integrity: sha512-AIbJTRF9HJz7FJ8t7058huwDXHaFEY4a4f6uuPtctEQ8/I3ybrX66iZMgoLD1kLaamU4tzZPlZ6tSpA3pM8LZg==}
dependencies:
'@iconify/types': 2.0.0
pathe: 1.0.0
@@ -986,8 +985,8 @@ packages:
vue-i18n:
optional: true
dependencies:
- '@intlify/message-compiler': 9.3.0-beta.12
- '@intlify/shared': 9.3.0-beta.12
+ '@intlify/message-compiler': 9.3.0-beta.14
+ '@intlify/shared': 9.3.0-beta.14
jsonc-eslint-parser: 1.4.1
source-map: 0.6.1
vue-i18n: 9.2.2_vue@3.2.45
@@ -1016,11 +1015,11 @@ packages:
'@intlify/shared': 9.2.2
source-map: 0.6.1
- /@intlify/message-compiler/9.3.0-beta.12:
- resolution: {integrity: sha512-A8/s7pb3v8nf6HG77qFPJntxgQKI9GXxGnkn7aO+b03/X/GkF/4WceDSAIk3i+yLeIgszeBn9GZ23tSg4sTEHA==}
+ /@intlify/message-compiler/9.3.0-beta.14:
+ resolution: {integrity: sha512-PlZ3pl+YYuql54Nq+26wv6ohIa8Ig6ALrvQI+f2zZKUtkupb49M4wyVN3bDQbFlgYVE7/u3n19BJSY8lEuX5Eg==}
engines: {node: '>= 14'}
dependencies:
- '@intlify/shared': 9.3.0-beta.11
+ '@intlify/shared': 9.3.0-beta.14
source-map: 0.6.1
dev: true
@@ -1028,13 +1027,8 @@ packages:
resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==}
engines: {node: '>= 14'}
- /@intlify/shared/9.3.0-beta.11:
- resolution: {integrity: sha512-CtbotesxTRiC3bRyXyv1NG39fkqJ790f8z8xFaeIXSZpOdiyxoh5BIyypCzSFQZDGLwz0Q9gyWbW1XpxQJm68Q==}
- engines: {node: '>= 14'}
- dev: true
-
- /@intlify/shared/9.3.0-beta.12:
- resolution: {integrity: sha512-WsmaS54sA8xuwezPKpa/OMoaX1v2VF2fCgAmYS6prDr2ir0CkUFWPm9A8ilmxzv4nkS61/v8+vf4lGGkn5uBdA==}
+ /@intlify/shared/9.3.0-beta.14:
+ resolution: {integrity: sha512-mJ/rFan+4uVsBAQSCAJnpQaPvSjQ49mJMNmGelTUbTDAmgf0oexYxwqtKOlFFyY3hmQ8lUDYaGQKuYrFgRuHnA==}
engines: {node: '>= 14'}
dev: true
@@ -1054,7 +1048,7 @@ packages:
optional: true
dependencies:
'@intlify/bundle-utils': 3.4.0_vue-i18n@9.2.2
- '@intlify/shared': 9.3.0-beta.12
+ '@intlify/shared': 9.3.0-beta.14
'@rollup/pluginutils': 4.2.1
'@vue/compiler-sfc': 3.2.45
debug: 4.3.4
@@ -1285,8 +1279,8 @@ packages:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
dev: false
- /@typescript-eslint/eslint-plugin/5.48.0_k73wpmdolxikpyqun3p36akaaq:
- resolution: {integrity: sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ==}
+ /@typescript-eslint/eslint-plugin/5.48.1_3jon24igvnqaqexgwtxk6nkpse:
+ resolution: {integrity: sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
'@typescript-eslint/parser': ^5.0.0
@@ -1296,10 +1290,10 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.48.0_iukboom6ndih5an6iafl45j2fe
- '@typescript-eslint/scope-manager': 5.48.0
- '@typescript-eslint/type-utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
- '@typescript-eslint/utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
+ '@typescript-eslint/parser': 5.48.1_iukboom6ndih5an6iafl45j2fe
+ '@typescript-eslint/scope-manager': 5.48.1
+ '@typescript-eslint/type-utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
+ '@typescript-eslint/utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
debug: 4.3.4
eslint: 8.31.0
ignore: 5.2.1
@@ -1312,8 +1306,8 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/parser/5.48.0_iukboom6ndih5an6iafl45j2fe:
- resolution: {integrity: sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg==}
+ /@typescript-eslint/parser/5.48.1_iukboom6ndih5an6iafl45j2fe:
+ resolution: {integrity: sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -1322,9 +1316,9 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/scope-manager': 5.48.0
- '@typescript-eslint/types': 5.48.0
- '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
+ '@typescript-eslint/scope-manager': 5.48.1
+ '@typescript-eslint/types': 5.48.1
+ '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
debug: 4.3.4
eslint: 8.31.0
typescript: 4.9.4
@@ -1332,16 +1326,16 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/scope-manager/5.48.0:
- resolution: {integrity: sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow==}
+ /@typescript-eslint/scope-manager/5.48.1:
+ resolution: {integrity: sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- '@typescript-eslint/types': 5.48.0
- '@typescript-eslint/visitor-keys': 5.48.0
+ '@typescript-eslint/types': 5.48.1
+ '@typescript-eslint/visitor-keys': 5.48.1
dev: true
- /@typescript-eslint/type-utils/5.48.0_iukboom6ndih5an6iafl45j2fe:
- resolution: {integrity: sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g==}
+ /@typescript-eslint/type-utils/5.48.1_iukboom6ndih5an6iafl45j2fe:
+ resolution: {integrity: sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: '*'
@@ -1350,8 +1344,8 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
- '@typescript-eslint/utils': 5.48.0_iukboom6ndih5an6iafl45j2fe
+ '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
+ '@typescript-eslint/utils': 5.48.1_iukboom6ndih5an6iafl45j2fe
debug: 4.3.4
eslint: 8.31.0
tsutils: 3.21.0_typescript@4.9.4
@@ -1360,13 +1354,13 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/types/5.48.0:
- resolution: {integrity: sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw==}
+ /@typescript-eslint/types/5.48.1:
+ resolution: {integrity: sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
- /@typescript-eslint/typescript-estree/5.48.0_typescript@4.9.4:
- resolution: {integrity: sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw==}
+ /@typescript-eslint/typescript-estree/5.48.1_typescript@4.9.4:
+ resolution: {integrity: sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
typescript: '*'
@@ -1374,8 +1368,8 @@ packages:
typescript:
optional: true
dependencies:
- '@typescript-eslint/types': 5.48.0
- '@typescript-eslint/visitor-keys': 5.48.0
+ '@typescript-eslint/types': 5.48.1
+ '@typescript-eslint/visitor-keys': 5.48.1
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
@@ -1386,17 +1380,17 @@ packages:
- supports-color
dev: true
- /@typescript-eslint/utils/5.48.0_iukboom6ndih5an6iafl45j2fe:
- resolution: {integrity: sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ==}
+ /@typescript-eslint/utils/5.48.1_iukboom6ndih5an6iafl45j2fe:
+ resolution: {integrity: sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
'@types/json-schema': 7.0.11
'@types/semver': 7.3.13
- '@typescript-eslint/scope-manager': 5.48.0
- '@typescript-eslint/types': 5.48.0
- '@typescript-eslint/typescript-estree': 5.48.0_typescript@4.9.4
+ '@typescript-eslint/scope-manager': 5.48.1
+ '@typescript-eslint/types': 5.48.1
+ '@typescript-eslint/typescript-estree': 5.48.1_typescript@4.9.4
eslint: 8.31.0
eslint-scope: 5.1.1
eslint-utils: 3.0.0_eslint@8.31.0
@@ -1406,11 +1400,11 @@ packages:
- typescript
dev: true
- /@typescript-eslint/visitor-keys/5.48.0:
- resolution: {integrity: sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q==}
+ /@typescript-eslint/visitor-keys/5.48.1:
+ resolution: {integrity: sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
- '@typescript-eslint/types': 5.48.0
+ '@typescript-eslint/types': 5.48.1
eslint-visitor-keys: 3.3.0
dev: true
@@ -1498,30 +1492,30 @@ packages:
vue: 3.2.45
dev: true
- /@volar/language-core/1.0.22:
- resolution: {integrity: sha512-hiJeCOqxNdtG/04FRGLGI9H9DVz2l6cTqPDBzwqplHXAWfMxjzUaGUrn9sfTG7YMFNZUgK4EYxJnRfhqdtbSFQ==}
+ /@volar/language-core/1.0.24:
+ resolution: {integrity: sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==}
dependencies:
- '@volar/source-map': 1.0.22
+ '@volar/source-map': 1.0.24
muggle-string: 0.1.0
dev: true
- /@volar/source-map/1.0.22:
- resolution: {integrity: sha512-cv4gypHSP4MWVR82ed/+1IpI6794qAl0Q0+KJ+VGMVF8rVugsiF9QbyMCgjel9wNRsssQsazzsf6txOR9vHQiw==}
+ /@volar/source-map/1.0.24:
+ resolution: {integrity: sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==}
dependencies:
muggle-string: 0.1.0
dev: true
- /@volar/typescript/1.0.22:
- resolution: {integrity: sha512-VPyEicealSD4gqlE5/UQ1j3ietsO6Hfat40KtUEh/K+XEZ7h02b1KgFV64YEuBkBOaZ5hgvRW/WXKtQgXCl7Iw==}
+ /@volar/typescript/1.0.24:
+ resolution: {integrity: sha512-f8hCSk+PfKR1/RQHxZ79V1NpDImHoivqoizK+mstphm25tn/YJ/JnKNjZHB+o21fuW0yKlI26NV3jkVb2Cc/7A==}
dependencies:
- '@volar/language-core': 1.0.22
+ '@volar/language-core': 1.0.24
dev: true
- /@volar/vue-language-core/1.0.22:
- resolution: {integrity: sha512-Ki0G/ZdBj2/GLw+/VVH3n9XR/JL6krMIth02EekFn6JV4PGN3mNxbvoh6lOPSDZLR6biOU5nJPnnjpKy8nuXhw==}
+ /@volar/vue-language-core/1.0.24:
+ resolution: {integrity: sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg==}
dependencies:
- '@volar/language-core': 1.0.22
- '@volar/source-map': 1.0.22
+ '@volar/language-core': 1.0.24
+ '@volar/source-map': 1.0.24
'@vue/compiler-dom': 3.2.45
'@vue/compiler-sfc': 3.2.45
'@vue/reactivity': 3.2.45
@@ -1530,11 +1524,11 @@ packages:
vue-template-compiler: 2.7.14
dev: true
- /@volar/vue-typescript/1.0.22:
- resolution: {integrity: sha512-2T1o5z86PAev31OMtVOv/qp4P3ZVl9ln/2KTmykQE8Fh4A5F+868MW4nf5J7XQ6RNyx7RH9LhzgjvbqJpAfiYw==}
+ /@volar/vue-typescript/1.0.24:
+ resolution: {integrity: sha512-9a25oHDvGaNC0okRS47uqJI6FxY4hUQZUsxeOUFHcqVxZEv8s17LPuP/pMMXyz7jPygrZubB/qXqHY5jEu/akA==}
dependencies:
- '@volar/typescript': 1.0.22
- '@volar/vue-language-core': 1.0.22
+ '@volar/typescript': 1.0.24
+ '@volar/vue-language-core': 1.0.24
dev: true
/@vue/babel-helper-vue-transform-on/1.0.2:
@@ -1583,7 +1577,7 @@ packages:
'@vue/shared': 3.2.45
estree-walker: 2.0.2
magic-string: 0.25.9
- postcss: 8.4.20
+ postcss: 8.4.21
source-map: 0.6.1
/@vue/compiler-ssr/3.2.45:
@@ -2066,7 +2060,7 @@ packages:
hasBin: true
dev: true
- /autoprefixer/10.4.13_postcss@8.4.20:
+ /autoprefixer/10.4.13_postcss@8.4.21:
resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
@@ -2078,7 +2072,7 @@ packages:
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
- postcss: 8.4.20
+ postcss: 8.4.21
postcss-value-parser: 4.2.0
dev: true
@@ -3013,12 +3007,12 @@ packages:
eslint: 8.31.0
dev: true
- /eslint-define-config/1.13.0:
- resolution: {integrity: sha512-d0BfmPGBcMusfiz6QY/piAhWaEyJriJtvk9SHfuJzI7am9k4ce07SfmPkpcR0ckXNyu4FBons10akOO2Tx+X+Q==}
+ /eslint-define-config/1.14.0:
+ resolution: {integrity: sha512-NREt5SzMwKmLAY28YdaqIiTSJxfPpuZ+1ZLJxY2Wbj02dYF4QX81z0q9MPMjZB8C+SlCu66qAhcPpFJyhXOiuA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13', pnpm: '>= 7.0.0'}
dev: true
- /eslint-plugin-prettier/4.2.1_32m5uc2milwdw3tnkcq5del26y:
+ /eslint-plugin-prettier/4.2.1_iu5s7nk6dw7o3tajefwfiqfmge:
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
@@ -3031,7 +3025,7 @@ packages:
dependencies:
eslint: 8.31.0
eslint-config-prettier: 8.6.0_eslint@8.31.0
- prettier: 2.8.1
+ prettier: 2.8.2
prettier-linter-helpers: 1.0.0
dev: true
@@ -4506,7 +4500,7 @@ packages:
dev: false
/nanoid/3.3.4:
- resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/nanoid/-/nanoid-3.3.4.tgz}
+ resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -4848,8 +4842,8 @@ packages:
dependencies:
htmlparser2: 8.0.1
js-tokens: 8.0.0
- postcss: 8.4.20
- postcss-safe-parser: 6.0.0_postcss@8.4.20
+ postcss: 8.4.21
+ postcss-safe-parser: 6.0.0_postcss@8.4.21
dev: true
/postcss-media-query-parser/0.2.3:
@@ -4868,22 +4862,22 @@ packages:
resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==}
dev: true
- /postcss-safe-parser/6.0.0_postcss@8.4.20:
+ /postcss-safe-parser/6.0.0_postcss@8.4.21:
resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.3.3
dependencies:
- postcss: 8.4.20
+ postcss: 8.4.21
dev: true
- /postcss-scss/4.0.6_postcss@8.4.20:
+ /postcss-scss/4.0.6_postcss@8.4.21:
resolution: {integrity: sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.4.19
dependencies:
- postcss: 8.4.20
+ postcss: 8.4.21
dev: true
/postcss-selector-parser/6.0.10:
@@ -4902,12 +4896,12 @@ packages:
util-deprecate: 1.0.2
dev: true
- /postcss-sorting/7.0.1_postcss@8.4.20:
- resolution: {integrity: sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==}
+ /postcss-sorting/8.0.1_postcss@8.4.21:
+ resolution: {integrity: sha512-go9Zoxx7KQH+uLrJ9xa5wRErFeXu01ydA6O8m7koPXkmAN7Ts//eRcIqjo0stBR4+Nir2gMYDOWAOx7O5EPUZA==}
peerDependencies:
- postcss: ^8.3.9
+ postcss: ^8.4.20
dependencies:
- postcss: 8.4.20
+ postcss: 8.4.21
dev: true
/postcss-value-parser/4.2.0:
@@ -4924,8 +4918,8 @@ packages:
supports-color: 3.2.3
dev: true
- /postcss/8.4.20:
- resolution: {integrity: sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/postcss/-/postcss-8.4.20.tgz}
+ /postcss/8.4.21:
+ resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.4
@@ -4983,8 +4977,8 @@ packages:
fast-diff: 1.2.0
dev: true
- /prettier/2.8.1:
- resolution: {integrity: sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==}
+ /prettier/2.8.2:
+ resolution: {integrity: sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==}
engines: {node: '>=10.13.0'}
hasBin: true
dev: true
@@ -5428,7 +5422,7 @@ packages:
dev: true
/source-map-js/1.0.2:
- resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/source-map-js/-/source-map-js-1.0.2.tgz}
+ resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
/source-map-resolve/0.5.3:
@@ -5636,13 +5630,13 @@ packages:
stylelint-config-recommended: 9.0.0_stylelint@14.16.1
dev: true
- /stylelint-order/5.0.0_stylelint@14.16.1:
- resolution: {integrity: sha512-OWQ7pmicXufDw5BlRqzdz3fkGKJPgLyDwD1rFY3AIEfIH/LQY38Vu/85v8/up0I+VPiuGRwbc2Hg3zLAsJaiyw==}
+ /stylelint-order/6.0.1_stylelint@14.16.1:
+ resolution: {integrity: sha512-C9gJDZArRBZvn+4MPgggwYTp7dK49WPnYa5+6tBEkZnW/YWj4xBVNJdQjIik14w5orlF9RqFpYDHN0FPWIFOSQ==}
peerDependencies:
stylelint: ^14.0.0
dependencies:
- postcss: 8.4.20
- postcss-sorting: 7.0.1_postcss@8.4.20
+ postcss: 8.4.21
+ postcss-sorting: 8.0.1_postcss@8.4.21
stylelint: 14.16.1
dev: true
@@ -5651,7 +5645,7 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
dependencies:
- '@csstools/selector-specificity': 2.0.2_2xshye3abirqjlplmebvmaxyna
+ '@csstools/selector-specificity': 2.0.2_wajs5nedgkikc5pcuwett7legi
balanced-match: 2.0.0
colord: 2.9.3
cosmiconfig: 7.1.0
@@ -5674,10 +5668,10 @@ packages:
micromatch: 4.0.5
normalize-path: 3.0.0
picocolors: 1.0.0
- postcss: 8.4.20
+ postcss: 8.4.21
postcss-media-query-parser: 0.2.3
postcss-resolve-nested-selector: 0.1.1
- postcss-safe-parser: 6.0.0_postcss@8.4.20
+ postcss-safe-parser: 6.0.0_postcss@8.4.21
postcss-selector-parser: 6.0.11
postcss-value-parser: 4.2.0
resolve-from: 5.0.0
@@ -6211,7 +6205,7 @@ packages:
dependencies:
'@types/node': 18.11.18
esbuild: 0.16.5
- postcss: 8.4.20
+ postcss: 8.4.21
resolve: 1.22.1
rollup: 3.9.1
sass: 1.57.1
@@ -6281,14 +6275,14 @@ packages:
he: 1.2.0
dev: true
- /vue-tsc/1.0.22_typescript@4.9.4:
- resolution: {integrity: sha512-xSxwgWR3czhv7sLKHWu6lzj9Xq6AtsCURVL45AY4TLGFszv2L2YlMgygXvqslyCM5bz9cyoIKSaZnzHqHTHjzA==}
+ /vue-tsc/1.0.24_typescript@4.9.4:
+ resolution: {integrity: sha512-mmU1s5SAqE1nByQAiQnao9oU4vX+mSdsgI8H57SfKH6UVzq/jP9+Dbi2GaV+0b4Cn361d2ln8m6xeU60ApiEXg==}
hasBin: true
peerDependencies:
typescript: '*'
dependencies:
- '@volar/vue-language-core': 1.0.22
- '@volar/vue-typescript': 1.0.22
+ '@volar/vue-language-core': 1.0.24
+ '@volar/vue-typescript': 1.0.24
typescript: 4.9.4
dev: true
@@ -6314,8 +6308,8 @@ packages:
'@vue/server-renderer': 3.2.45_vue@3.2.45
'@vue/shared': 3.2.45
- /vxe-table/4.3.7_vue@3.2.45+xe-utils@3.5.7:
- resolution: {integrity: sha512-v+d7eEQ5uqtVTQCts4xkW0S15LZcIuEukYHGXI53SdoUj2gLFggPYAxQr1y659CM/ESRWPz9LNVHpd97KkjGHw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/vxe-table/-/vxe-table-4.3.7.tgz}
+ /vxe-table/4.3.9_vue@3.2.45+xe-utils@3.5.7:
+ resolution: {integrity: sha512-Ns7Ooa7lOHBpks90i0k0BMNyxfMpUo39ryxTgKE41X3xVnI9tGQs2U6+klfDlsuqYfmG3ibyzHN3OCrWbbKo4Q==}
peerDependencies:
vue: ^3.2.28
xe-utils: ^3.5.0
diff --git a/yudao-ui-admin-vue3/src/views/Profile/Index.vue b/yudao-ui-admin-vue3/src/views/Profile/Index.vue
index 46a218563..7c8b49dc3 100644
--- a/yudao-ui-admin-vue3/src/views/Profile/Index.vue
+++ b/yudao-ui-admin-vue3/src/views/Profile/Index.vue
@@ -8,7 +8,7 @@
-
+