diff --git a/core/captcha-plus/pom.xml b/core/captcha-plus/pom.xml index 2338436f0f58889289ccfdb5c9da01073f1a5dcd..c03a41004eda601fe316fffa2c5a7dd2cfd60620 100644 --- a/core/captcha-plus/pom.xml +++ b/core/captcha-plus/pom.xml @@ -39,7 +39,12 @@ 1.7.36 provided - + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/Const.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/Const.java index 602aa4362d7428d3b565064c65f9aa04eac56bb4..6992584f0c073d85db2f5c614dbd91928cf14687 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/Const.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/model/common/Const.java @@ -25,6 +25,8 @@ public interface Const { */ String CAPTCHA_CACHE_TYPE = "captcha.cacheType"; + String CAPTCHA_DECRYPT_TYPE = "captcha.decrypt.type"; + /** * 右下角水印文字(我的水印) */ @@ -59,7 +61,9 @@ public interface Const { /** * aes加密开关 + * @deprecated 不再需要这个参数,当解密算法为 AES 时,会自动开启加密开关 */ + @Deprecated String CAPTCHA_AES_STATUS = "captcha.aes.status"; /** diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/CaptchaDecryptService.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/CaptchaDecryptService.java new file mode 100644 index 0000000000000000000000000000000000000000..1af27a67dd97d94f8e1700d939ca79ecb21d8449 --- /dev/null +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/CaptchaDecryptService.java @@ -0,0 +1,17 @@ +package com.xingyuv.captcha.service; + +/** + * 用于验证码安全验证 + */ +public interface CaptchaDecryptService { + /** + * 解密 + */ + String decrypt(String point, String key) throws Exception; + + /** + * 用于区别算法,不区分大小写 + * @return + */ + String getType(); +} diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/AbstractCaptchaService.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/AbstractCaptchaService.java index 4da93c5e975d191679b3a98a71a26d752641d243..53b916b671408dddfca954e0592ae666ba0a01e1 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/AbstractCaptchaService.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/AbstractCaptchaService.java @@ -11,6 +11,7 @@ import com.xingyuv.captcha.model.common.RepCodeEnum; import com.xingyuv.captcha.model.common.ResponseModel; import com.xingyuv.captcha.model.vo.CaptchaVO; import com.xingyuv.captcha.service.CaptchaCacheService; +import com.xingyuv.captcha.service.CaptchaDecryptService; import com.xingyuv.captcha.service.CaptchaService; import com.xingyuv.captcha.util.*; import org.slf4j.Logger; @@ -25,6 +26,8 @@ import java.util.Base64; import java.util.Objects; import java.util.Properties; +import static com.xingyuv.captcha.model.common.Const.CAPTCHA_DECRYPT_TYPE; + /** * Created by raodeming on 2019/12/25. */ @@ -71,6 +74,8 @@ public abstract class AbstractCaptchaService implements CaptchaService { protected static int captchaInterferenceOptions = 0; + private CaptchaDecryptService captchaDecryptService; + /** * 判断应用是否实现了自定义缓存,没有就使用内存 * @@ -88,13 +93,15 @@ public abstract class AbstractCaptchaService implements CaptchaService { waterMark = config.getProperty(Const.CAPTCHA_WATER_MARK, waterMark); slipOffset = config.getProperty(Const.CAPTCHA_SLIP_OFFSET, slipOffset); waterMarkFontStr = config.getProperty(Const.CAPTCHA_WATER_FONT, clickWordFontStr); - captchaAesStatus = Boolean.parseBoolean(config.getProperty(Const.CAPTCHA_AES_STATUS, "true")); clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, clickWordFontStr); //clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "SourceHanSansCN-Normal.otf"); cacheType = config.getProperty(Const.CAPTCHA_CACHE_TYPE, cacheType); captchaInterferenceOptions = Integer.parseInt( config.getProperty(Const.CAPTCHA_INTERFERENCE_OPTIONS, "0")); + String decryptType = config.getProperty(CAPTCHA_DECRYPT_TYPE, "aes"); + captchaAesStatus = decryptType.equalsIgnoreCase("aes"); + captchaDecryptService = CaptchaDecryptServiceFactory.getDecryptService(decryptType); // 部署在linux中,如果没有安装中文字段,水印和点选文字,中文无法显示, // 通过加载resources下的font字体解决,无需在linux中安装字体 loadWaterMarkFont(); @@ -257,8 +264,8 @@ public abstract class AbstractCaptchaService implements CaptchaService { * @return 前端坐标aes加密 * @throws Exception E */ - public static String decrypt(String point, String key) throws Exception { - return AESUtil.aesDecrypt(point, key); + public String decrypt(String point, String key) throws Exception { + return captchaDecryptService.decrypt(point, key); } protected static int getEnOrChLength(String s) { diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java index 32c9a833ce18013bff1768547dfb66c2ac7e3f99..905aefb1dbd9b277222d0dc072a55797a3841ec3 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java @@ -99,6 +99,7 @@ public class BlockPuzzleCaptchaServiceImpl extends AbstractCaptchaService { String pointJson = null; try { point = JsonUtil.parseObject(s, PointVO.class); + //将原有的固定 aes 解密过程改为接口解密,支持自定义实现解密流程 //aes解密 pointJson = decrypt(captchaVO.getPointJson(), point.getSecretKey()); point1 = JsonUtil.parseObject(pointJson, PointVO.class); diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaAesDecryptService.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaAesDecryptService.java new file mode 100644 index 0000000000000000000000000000000000000000..0ead5da736ab75915b95ce66e56f99eab0c4cc47 --- /dev/null +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaAesDecryptService.java @@ -0,0 +1,19 @@ +package com.xingyuv.captcha.service.impl; + +import com.xingyuv.captcha.service.CaptchaDecryptService; +import com.xingyuv.captcha.util.AESUtil; + +/** + * AES 解密 + */ +public class CaptchaAesDecryptService implements CaptchaDecryptService { + @Override + public String decrypt(String point, String key) throws Exception { + return AESUtil.aesDecrypt(point, key); + } + + @Override + public String getType() { + return "AES"; + } +} diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaBlankDecryptService.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaBlankDecryptService.java new file mode 100644 index 0000000000000000000000000000000000000000..2a53400de1c8f56aac151c05fb710d7f3fff80b2 --- /dev/null +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaBlankDecryptService.java @@ -0,0 +1,18 @@ +package com.xingyuv.captcha.service.impl; + +import com.xingyuv.captcha.service.CaptchaDecryptService; + +/** + * 无需解密 + */ +public class CaptchaBlankDecryptService implements CaptchaDecryptService { + @Override + public String decrypt(String point, String key) throws Exception { + return point; + } + + @Override + public String getType() { + return "blank"; + } +} diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaDecryptServiceFactory.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaDecryptServiceFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8e767e4cac615be1f93928ff03cb285f69765d22 --- /dev/null +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/service/impl/CaptchaDecryptServiceFactory.java @@ -0,0 +1,28 @@ +package com.xingyuv.captcha.service.impl; + +import com.xingyuv.captcha.service.CaptchaDecryptService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ServiceLoader; + +public class CaptchaDecryptServiceFactory { + private static final Logger log = LoggerFactory.getLogger(CaptchaDecryptServiceFactory.class); + private static final Map DECRYPT_SERVICE_HASH_MAP = new HashMap<>(); + + static { + ServiceLoader decryptServices = ServiceLoader.load(CaptchaDecryptService.class); + for (CaptchaDecryptService decryptService : decryptServices) { + DECRYPT_SERVICE_HASH_MAP.put(decryptService.getType().toLowerCase(Locale.ROOT), decryptService); + } + log.debug("supported-captchaDecrypt-service:{}", DECRYPT_SERVICE_HASH_MAP.keySet()); + + } + + public static CaptchaDecryptService getDecryptService(String type) { + return DECRYPT_SERVICE_HASH_MAP.get(type.toLowerCase(Locale.ROOT)); + } +} diff --git a/core/captcha-plus/src/main/java/com/xingyuv/captcha/util/AESUtil.java b/core/captcha-plus/src/main/java/com/xingyuv/captcha/util/AESUtil.java index ecf695bcef6d76cbce955adbee5604eb56233506..eea71f6b4b5271330268f1cf6362d58bed90414e 100644 --- a/core/captcha-plus/src/main/java/com/xingyuv/captcha/util/AESUtil.java +++ b/core/captcha-plus/src/main/java/com/xingyuv/captcha/util/AESUtil.java @@ -7,11 +7,12 @@ package com.xingyuv.captcha.util; -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; +import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import java.math.BigInteger; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Base64; public class AESUtil { @@ -57,9 +58,8 @@ public class AESUtil { * * @param base64Code 待解码的base 64 code * @return 解码后的byte[] - * @throws Exception e */ - public static byte[] base64Decode(String base64Code) throws Exception { + public static byte[] base64Decode(String base64Code) { Base64.Decoder decoder = Base64.getDecoder(); return StringUtils.isEmpty(base64Code) ? null : decoder.decode(base64Code); } @@ -106,7 +106,8 @@ public class AESUtil { * @return 解密后的String * @throws Exception e */ - public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception { + + public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); @@ -125,7 +126,7 @@ public class AESUtil { * @return 解密后的string * @throws Exception e */ - public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception { + public static String aesDecrypt(String encryptStr, String decryptKey) throws IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException { if (StringUtils.isBlank(decryptKey)) { return encryptStr; } diff --git a/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService b/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService new file mode 100644 index 0000000000000000000000000000000000000000..d13908e205a1f1cd196b920f562f1fc15263dfbd --- /dev/null +++ b/core/captcha-plus/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService @@ -0,0 +1,2 @@ +com.xingyuv.captcha.service.impl.CaptchaAesDecryptService +com.xingyuv.captcha.service.impl.CaptchaBlankDecryptService \ No newline at end of file diff --git a/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaDecryptServiceTest.java b/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaDecryptServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3869380747de51d4fa8947c9370a99860ddb4d78 --- /dev/null +++ b/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaDecryptServiceTest.java @@ -0,0 +1,28 @@ +package com.xingyuv.captcha.util; + +import com.xingyuv.captcha.service.CaptchaDecryptService; +import com.xingyuv.captcha.service.impl.CaptchaDecryptServiceFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class CaptchaDecryptServiceTest { + @Test + void testGetInstance() { + CaptchaDecryptService aes = CaptchaDecryptServiceFactory.getDecryptService("AES"); + assertNotNull(aes); + CaptchaDecryptService blank = CaptchaDecryptServiceFactory.getDecryptService("blank"); + assertNotNull(blank); + CaptchaDecryptService test = CaptchaDecryptServiceFactory.getDecryptService("test"); + assertNotNull(test); + } + + @Test + void testCustomDecrypt() throws Exception { + CaptchaDecryptService test = CaptchaDecryptServiceFactory.getDecryptService("test"); + assertEquals(test.decrypt("test", null), ""); + assertThrows(NullPointerException.class, () -> test.decrypt(null, "")); + assertThrows(Exception.class, () -> test.decrypt("", "")); + } + +} diff --git a/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaTestDecryptService.java b/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaTestDecryptService.java new file mode 100644 index 0000000000000000000000000000000000000000..d8c499c72d67066321ca0b03345929916d98ade1 --- /dev/null +++ b/core/captcha-plus/src/test/java/com/xingyuv/captcha/util/CaptchaTestDecryptService.java @@ -0,0 +1,24 @@ +package com.xingyuv.captcha.util; + +import com.xingyuv.captcha.service.CaptchaDecryptService; + +import java.util.Objects; + +/** + * 测试解密 + */ +public class CaptchaTestDecryptService implements CaptchaDecryptService { + @Override + public String decrypt(String point, String key) throws Exception { + Objects.requireNonNull(point); + if (key!=null) { + throw new Exception(); + } + return ""; + } + + @Override + public String getType() { + return "TEST"; + } +} diff --git a/core/captcha-plus/src/test/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService b/core/captcha-plus/src/test/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService new file mode 100644 index 0000000000000000000000000000000000000000..e0aeb2fe007baf419d0ae8d36848249c35f28a65 --- /dev/null +++ b/core/captcha-plus/src/test/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaDecryptService @@ -0,0 +1,2 @@ +com.xingyuv.captcha.service.impl.CaptchaAesDecryptService +com.xingyuv.captcha.util.CaptchaTestDecryptService diff --git a/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/config/AjCaptchaServiceAutoConfiguration.java b/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/config/AjCaptchaServiceAutoConfiguration.java index d6f98699e3f41e67db5b15b849f27c6e509de921..b182b992022715fb1e8d1bf59082e377487558ea 100644 --- a/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/config/AjCaptchaServiceAutoConfiguration.java +++ b/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/config/AjCaptchaServiceAutoConfiguration.java @@ -35,6 +35,7 @@ public class AjCaptchaServiceAutoConfiguration { config.put(Const.ORIGINAL_PATH_PIC_CLICK, prop.getPicClick()); config.put(Const.CAPTCHA_SLIP_OFFSET, prop.getSlipOffset()); config.put(Const.CAPTCHA_AES_STATUS, String.valueOf(prop.getAesStatus())); + config.put(Const.CAPTCHA_DECRYPT_TYPE, prop.getDecryptType()); config.put(Const.CAPTCHA_WATER_FONT, prop.getWaterFont()); config.put(Const.CAPTCHA_CACHE_MAX_NUMBER, prop.getCacheNumber()); config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, prop.getTimingClear()); diff --git a/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/properties/AjCaptchaProperties.java b/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/properties/AjCaptchaProperties.java index 313f4710278a43b60b029782063b7914291295d5..d4d95d89ab127b9811e6a287ab59479afac7e537 100644 --- a/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/properties/AjCaptchaProperties.java +++ b/core/spring-boot-starter-captcha-plus/src/main/java/com/xingyuv/captcha/properties/AjCaptchaProperties.java @@ -51,6 +51,7 @@ public class AjCaptchaProperties { /** * aes加密坐标开启或者禁用(true|false). */ + @Deprecated private Boolean aesStatus = true; /** @@ -113,6 +114,8 @@ public class AjCaptchaProperties { */ private int clickWordCount = 4; + private String decryptType = "aes"; + public int getFontStyle() { return fontStyle; } @@ -272,10 +275,12 @@ public class AjCaptchaProperties { this.slipOffset = slipOffset; } + @Deprecated public Boolean getAesStatus() { return aesStatus; } + @Deprecated public void setAesStatus(Boolean aesStatus) { this.aesStatus = aesStatus; } @@ -312,9 +317,17 @@ public class AjCaptchaProperties { this.timingClear = timingClear; } + public String getDecryptType() { + return decryptType; + } + + public void setDecryptType(String decryptType) { + this.decryptType = decryptType; + } + @Override public String toString() { - return "\nAjCaptchaProperties{" + + return "AjCaptchaProperties{" + "type=" + type + ", jigsaw='" + jigsaw + '\'' + ", picClick='" + picClick + '\'' + @@ -327,12 +340,17 @@ public class AjCaptchaProperties { ", cacheNumber='" + cacheNumber + '\'' + ", timingClear='" + timingClear + '\'' + ", cacheType=" + cacheType + + ", historyDataClearEnable=" + historyDataClearEnable + ", reqFrequencyLimitEnable=" + reqFrequencyLimitEnable + ", reqGetLockLimit=" + reqGetLockLimit + ", reqGetLockSeconds=" + reqGetLockSeconds + ", reqGetMinuteLimit=" + reqGetMinuteLimit + ", reqCheckMinuteLimit=" + reqCheckMinuteLimit + ", reqVerifyMinuteLimit=" + reqVerifyMinuteLimit + + ", fontStyle=" + fontStyle + + ", fontSize=" + fontSize + + ", clickWordCount=" + clickWordCount + + ", decryptType='" + decryptType + '\'' + '}'; } }