Преглед изворни кода

!10 修改 使用hutool验证码替换原验证码
Merge pull request !10 from 月夜/feature/Captcha

疯狂的狮子Li пре 4 година
родитељ
комит
123ee3f17d

+ 9 - 1
config/ruoyi-gateway-dev.yml

@@ -49,8 +49,16 @@ spring:
 security:
   # 验证码
   captcha:
+    # 是否开启验证码
     enabled: true
-    type: math
+    # 验证码类型 math 数组计算 char 字符验证
+    type: MATH
+    # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
+    category: CIRCLE
+    # 数字验证码位数
+    numberLength: 1
+    # 字符验证码长度
+    charLength: 4
   # 防止XSS攻击
   xss:
     enabled: true

+ 7 - 56
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/utils/reflect/ReflectUtils.java

@@ -1,19 +1,16 @@
 package com.ruoyi.common.core.utils.reflect;
 
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.util.Date;
+import cn.hutool.core.util.ReflectUtil;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.core.utils.DateUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Validate;
 import org.apache.poi.ss.usermodel.DateUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import com.ruoyi.common.core.text.Convert;
-import com.ruoyi.common.core.utils.DateUtils;
+
+import java.lang.reflect.*;
+import java.util.Date;
 
 /**
  * 反射工具类. 提供调用getter/setter方法, 访问私有变量, 调用私有方法, 获取泛型类型Class, 被AOP过的真实类等工具函数.
@@ -21,7 +18,7 @@ import com.ruoyi.common.core.utils.DateUtils;
  * @author ruoyi
  */
 @SuppressWarnings("rawtypes")
-public class ReflectUtils
+public class ReflectUtils extends ReflectUtil
 {
     private static final String SETTER_PREFIX = "set";
 
@@ -70,52 +67,6 @@ public class ReflectUtils
         }
     }
 
-    /**
-     * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
-     */
-    @SuppressWarnings("unchecked")
-    public static <E> E getFieldValue(final Object obj, final String fieldName)
-    {
-        Field field = getAccessibleField(obj, fieldName);
-        if (field == null)
-        {
-            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
-            return null;
-        }
-        E result = null;
-        try
-        {
-            result = (E) field.get(obj);
-        }
-        catch (IllegalAccessException e)
-        {
-            logger.error("不可能抛出的异常{}", e.getMessage());
-        }
-        return result;
-    }
-
-    /**
-     * 直接设置对象属性值, 无视private/protected修饰符, 不经过setter函数.
-     */
-    public static <E> void setFieldValue(final Object obj, final String fieldName, final E value)
-    {
-        Field field = getAccessibleField(obj, fieldName);
-        if (field == null)
-        {
-            // throw new IllegalArgumentException("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
-            logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + fieldName + "] 字段 ");
-            return;
-        }
-        try
-        {
-            field.set(obj, value);
-        }
-        catch (IllegalAccessException e)
-        {
-            logger.error("不可能抛出的异常: {}", e.getMessage());
-        }
-    }
-
     /**
      * 直接调用对象方法, 无视private/protected修饰符.
      * 用于一次性调用的情况,否则应使用getAccessibleMethod()函数获得Method后反复调用.

+ 0 - 6
ruoyi-gateway/pom.xml

@@ -58,12 +58,6 @@
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
 
-        <!--验证码 -->
-        <dependency>
-            <groupId>com.github.penggle</groupId>
-            <artifactId>kaptcha</artifactId>
-        </dependency>
-
         <!-- RuoYi Common Redis-->
         <dependency>
             <groupId>com.ruoyi</groupId>

+ 85 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/captcha/UnsignedMathGenerator.java

@@ -0,0 +1,85 @@
+package com.ruoyi.gateway.captcha;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.math.Calculator;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.RandomUtil;
+import com.ruoyi.common.core.utils.StringUtils;
+
+/**
+ * 无符号计算生成器
+ *
+ * @author Lion Li
+ */
+public class UnsignedMathGenerator implements CodeGenerator {
+
+	private static final long serialVersionUID = -5514819971774091076L;
+
+	private static final String operators = "+-*";
+
+	/**
+	 * 参与计算数字最大长度
+	 */
+	private final int numberLength;
+
+	/**
+	 * 构造
+	 */
+	public UnsignedMathGenerator() {
+		this(2);
+	}
+
+	/**
+	 * 构造
+	 *
+	 * @param numberLength 参与计算最大数字位数
+	 */
+	public UnsignedMathGenerator(int numberLength) {
+		this.numberLength = numberLength;
+	}
+
+	@Override
+	public String generate() {
+		final int limit = getLimit();
+		int min = RandomUtil.randomInt(limit);
+		int max = RandomUtil.randomInt(min, limit);
+		String number1 = Integer.toString(max);
+		String number2 = Integer.toString(min);
+		number1 = StringUtils.rightPad(number1, this.numberLength, CharUtil.SPACE);
+		number2 = StringUtils.rightPad(number2, this.numberLength, CharUtil.SPACE);
+
+		return number1 + RandomUtil.randomChar(operators) + number2 + '=';
+	}
+
+	@Override
+	public boolean verify(String code, String userInputCode) {
+		int result;
+		try {
+			result = Integer.parseInt(userInputCode);
+		} catch (NumberFormatException e) {
+			// 用户输入非数字
+			return false;
+		}
+
+		final int calculateResult = (int) Calculator.conversion(code);
+		return result == calculateResult;
+	}
+
+	/**
+	 * 获取验证码长度
+	 *
+	 * @return 验证码长度
+	 */
+	public int getLength() {
+		return this.numberLength * 2 + 2;
+	}
+
+	/**
+	 * 根据长度获取参与计算数字最大值
+	 *
+	 * @return 最大值
+	 */
+	private int getLimit() {
+		return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
+	}
+}

+ 50 - 71
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/CaptchaConfig.java

@@ -1,83 +1,62 @@
 package com.ruoyi.gateway.config;
 
-import java.util.Properties;
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import com.google.code.kaptcha.impl.DefaultKaptcha;
-import com.google.code.kaptcha.util.Config;
-import static com.google.code.kaptcha.Constants.*;
+import org.springframework.context.annotation.Lazy;
+
+import java.awt.*;
 
 /**
  * 验证码配置
- * 
- * @author ruoyi
+ *
+ * @author Lion Li
  */
 @Configuration
-public class CaptchaConfig
-{
-    @Bean(name = "captchaProducer")
-    public DefaultKaptcha getKaptchaBean()
-    {
-        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
-        Properties properties = new Properties();
-        // 是否有边框 默认为true 我们可以自己设置yes,no
-        properties.setProperty(KAPTCHA_BORDER, "yes");
-        // 验证码文本字符颜色 默认为Color.BLACK
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
-        // 验证码图片宽度 默认为200
-        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
-        // 验证码图片高度 默认为50
-        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
-        // 验证码文本字符大小 默认为40
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
-        // KAPTCHA_SESSION_KEY
-        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
-        // 验证码文本字符长度 默认为5
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
-        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
-        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
-        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
-        Config config = new Config(properties);
-        defaultKaptcha.setConfig(config);
-        return defaultKaptcha;
+public class CaptchaConfig {
+
+    private final int width = 160;
+    private final int height = 60;
+    private final Color background = Color.PINK;
+    private final Font font = new Font("Arial", Font.BOLD, 48);
+
+    /**
+     * 圆圈干扰验证码
+     */
+    @Lazy
+    @Bean
+    public CircleCaptcha circleCaptcha() {
+        CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(width, height);
+        captcha.setBackground(background);
+        captcha.setFont(font);
+        return captcha;
     }
-    
-    @Bean(name = "captchaProducerMath")
-    public DefaultKaptcha getKaptchaBeanMath()
-    {
-        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
-        Properties properties = new Properties();
-        // 是否有边框 默认为true 我们可以自己设置yes,no
-        properties.setProperty(KAPTCHA_BORDER, "yes");
-        // 边框颜色 默认为Color.BLACK
-        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
-        // 验证码文本字符颜色 默认为Color.BLACK
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
-        // 验证码图片宽度 默认为200
-        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
-        // 验证码图片高度 默认为50
-        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
-        // 验证码文本字符大小 默认为40
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
-        // KAPTCHA_SESSION_KEY
-        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
-        // 验证码文本生成器
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.gateway.config.KaptchaTextCreator");
-        // 验证码文本字符间距 默认为2
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
-        // 验证码文本字符长度 默认为5
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
-        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
-        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
-        // 验证码噪点颜色 默认为Color.BLACK
-        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
-        // 干扰实现类
-        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
-        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
-        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
-        Config config = new Config(properties);
-        defaultKaptcha.setConfig(config);
-        return defaultKaptcha;
+
+    /**
+     * 线段干扰的验证码
+     */
+    @Lazy
+    @Bean
+    public LineCaptcha lineCaptcha() {
+        LineCaptcha captcha = CaptchaUtil.createLineCaptcha(width, height);
+        captcha.setBackground(background);
+        captcha.setFont(font);
+        return captcha;
     }
+
+    /**
+     * 扭曲干扰验证码
+     */
+    @Lazy
+    @Bean
+    public ShearCaptcha shearCaptcha() {
+        ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(width, height);
+        captcha.setBackground(background);
+        captcha.setFont(font);
+        return captcha;
+    }
+
 }

+ 0 - 75
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/KaptchaTextCreator.java

@@ -1,75 +0,0 @@
-package com.ruoyi.gateway.config;
-
-import java.util.Random;
-import com.google.code.kaptcha.text.impl.DefaultTextCreator;
-
-/**
- * 验证码文本生成器
- * 
- * @author ruoyi
- */
-public class KaptchaTextCreator extends DefaultTextCreator
-{
-    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
-
-    @Override
-    public String getText()
-    {
-        Integer result = 0;
-        Random random = new Random();
-        int x = random.nextInt(10);
-        int y = random.nextInt(10);
-        StringBuilder suChinese = new StringBuilder();
-        int randomoperands = (int) Math.round(Math.random() * 2);
-        if (randomoperands == 0)
-        {
-            result = x * y;
-            suChinese.append(CNUMBERS[x]);
-            suChinese.append("*");
-            suChinese.append(CNUMBERS[y]);
-        }
-        else if (randomoperands == 1)
-        {
-            if (!(x == 0) && y % x == 0)
-            {
-                result = y / x;
-                suChinese.append(CNUMBERS[y]);
-                suChinese.append("/");
-                suChinese.append(CNUMBERS[x]);
-            }
-            else
-            {
-                result = x + y;
-                suChinese.append(CNUMBERS[x]);
-                suChinese.append("+");
-                suChinese.append(CNUMBERS[y]);
-            }
-        }
-        else if (randomoperands == 2)
-        {
-            if (x >= y)
-            {
-                result = x - y;
-                suChinese.append(CNUMBERS[x]);
-                suChinese.append("-");
-                suChinese.append(CNUMBERS[y]);
-            }
-            else
-            {
-                result = y - x;
-                suChinese.append(CNUMBERS[y]);
-                suChinese.append("-");
-                suChinese.append(CNUMBERS[x]);
-            }
-        }
-        else
-        {
-            result = x + y;
-            suChinese.append(CNUMBERS[x]);
-            suChinese.append("+");
-            suChinese.append(CNUMBERS[y]);
-        }
-        suChinese.append("=?@" + result);
-        return suChinese.toString();
-    }
-}

+ 21 - 4
ruoyi-gateway/src/main/java/com/ruoyi/gateway/config/properties/CaptchaProperties.java

@@ -1,5 +1,7 @@
 package com.ruoyi.gateway.config.properties;
 
+import com.ruoyi.gateway.enums.CaptchaCategory;
+import com.ruoyi.gateway.enums.CaptchaType;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
@@ -17,13 +19,28 @@ import org.springframework.context.annotation.Configuration;
 public class CaptchaProperties
 {
     /**
-     * 验证码开关
+     * 验证码类型
      */
-    private Boolean enabled;
+    private CaptchaType type;
 
     /**
-     * 验证码类型(math 数组计算 char 字符)
+     * 验证码类
      */
-    private String type;
+    private CaptchaCategory category;
+
+    /**
+     * 数字验证码位数
+     */
+    private Integer numberLength;
+
+    /**
+     * 字符验证码长度
+     */
+    private Integer charLength;
+
+    /**
+     * 验证码开关
+     */
+    private Boolean enabled;
 
 }

+ 35 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/enums/CaptchaCategory.java

@@ -0,0 +1,35 @@
+package com.ruoyi.gateway.enums;
+
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.CircleCaptcha;
+import cn.hutool.captcha.LineCaptcha;
+import cn.hutool.captcha.ShearCaptcha;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 验证码类别
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaCategory {
+
+    /**
+     * 线段干扰
+     */
+    LINE(LineCaptcha.class),
+
+    /**
+     * 圆圈干扰
+     */
+    CIRCLE(CircleCaptcha.class),
+
+    /**
+     * 扭曲干扰
+     */
+    SHEAR(ShearCaptcha.class);
+
+    private final Class<? extends AbstractCaptcha> clazz;
+}

+ 29 - 0
ruoyi-gateway/src/main/java/com/ruoyi/gateway/enums/CaptchaType.java

@@ -0,0 +1,29 @@
+package com.ruoyi.gateway.enums;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.RandomGenerator;
+import com.ruoyi.gateway.captcha.UnsignedMathGenerator;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 验证码类型
+ *
+ * @author Lion Li
+ */
+@Getter
+@AllArgsConstructor
+public enum CaptchaType {
+
+    /**
+     * 数字
+     */
+    MATH(UnsignedMathGenerator.class),
+
+    /**
+     * 字符
+     */
+    CHAR(RandomGenerator.class);
+
+    private final Class<? extends CodeGenerator> clazz;
+}

+ 32 - 42
ruoyi-gateway/src/main/java/com/ruoyi/gateway/service/impl/ValidateCodeServiceImpl.java

@@ -1,22 +1,22 @@
 package com.ruoyi.gateway.service.impl;
 
-import com.google.code.kaptcha.Producer;
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.convert.Convert;
 import com.ruoyi.common.core.constant.Constants;
 import com.ruoyi.common.core.exception.CaptchaException;
 import com.ruoyi.common.core.utils.IdUtils;
+import com.ruoyi.common.core.utils.SpringUtils;
 import com.ruoyi.common.core.utils.StringUtils;
-import com.ruoyi.common.core.utils.sign.Base64;
+import com.ruoyi.common.core.utils.reflect.ReflectUtils;
 import com.ruoyi.common.core.web.domain.AjaxResult;
 import com.ruoyi.common.redis.utils.RedisUtils;
 import com.ruoyi.gateway.config.properties.CaptchaProperties;
+import com.ruoyi.gateway.enums.CaptchaType;
 import com.ruoyi.gateway.service.ValidateCodeService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.util.FastByteArrayOutputStream;
 
-import javax.annotation.Resource;
-import javax.imageio.ImageIO;
-import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
@@ -28,12 +28,6 @@ import java.util.concurrent.TimeUnit;
 @Service
 public class ValidateCodeServiceImpl implements ValidateCodeService
 {
-    @Resource(name = "captchaProducer")
-    private Producer captchaProducer;
-
-    @Resource(name = "captchaProducerMath")
-    private Producer captchaProducerMath;
-
     @Autowired
     private CaptchaProperties captchaProperties;
 
@@ -54,42 +48,38 @@ public class ValidateCodeServiceImpl implements ValidateCodeService
         // 保存验证码信息
         String uuid = IdUtils.simpleUUID();
         String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
-
-        String capStr = null, code = null;
-        BufferedImage image = null;
-
-        String captchaType = captchaProperties.getType();
         // 生成验证码
-        if ("math".equals(captchaType))
-        {
-            String capText = captchaProducerMath.createText();
-            capStr = capText.substring(0, capText.lastIndexOf("@"));
-            code = capText.substring(capText.lastIndexOf("@") + 1);
-            image = captchaProducerMath.createImage(capStr);
-        }
-        else if ("char".equals(captchaType))
-        {
-            capStr = code = captchaProducer.createText();
-            image = captchaProducer.createImage(capStr);
-        }
-
+        CaptchaType captchaType = captchaProperties.getType();
+        boolean isMath = CaptchaType.MATH == captchaType;
+        Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
+        CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
+        AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
+        captcha.setGenerator(codeGenerator);
+        captcha.createCode();
+        String code = isMath ? getCodeResult(captcha.getCode()) : captcha.getCode();
         RedisUtils.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
-        // 转换流信息写出
-        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
-        try
-        {
-            ImageIO.write(image, "jpg", os);
-        }
-        catch (IOException e)
-        {
-            return AjaxResult.error(e.getMessage());
-        }
-
         ajax.put("uuid", uuid);
-        ajax.put("img", Base64.encode(os.toByteArray()));
+        ajax.put("img", captcha.getImageBase64());
         return ajax;
     }
 
+    private String getCodeResult(String capStr) {
+        int numberLength = captchaProperties.getNumberLength();
+        int a = Convert.toInt(StringUtils.substring(capStr, 0, numberLength).trim());
+        char operator = capStr.charAt(numberLength);
+        int b = Convert.toInt(StringUtils.substring(capStr, numberLength + 1, numberLength + 1 + numberLength).trim());
+        switch (operator) {
+            case '*':
+                return Convert.toStr(a * b);
+            case '+':
+                return Convert.toStr(a + b);
+            case '-':
+                return Convert.toStr(a - b);
+            default:
+                return StringUtils.EMPTY;
+        }
+    }
+
     /**
      * 校验验证码
      */