Parcourir la source

feat: 将aj-captcha换为源码导入

xingyu il y a 2 ans
Parent
commit
865f101070
68 fichiers modifiés avec 5364 ajouts et 12 suppressions
  1. 0 7
      yudao-dependencies/pom.xml
  2. 6 5
      yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
  3. 14 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java
  4. 92 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java
  5. 21 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java
  6. 65 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/controller/CaptchaController.java
  7. 50 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java
  8. 56 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java
  9. 112 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java
  10. 73 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java
  11. 157 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java
  12. 111 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java
  13. 251 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java
  14. 95 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java
  15. 338 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java
  16. 41 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java
  17. 58 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java
  18. 269 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java
  19. 422 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java
  20. 47 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java
  21. 60 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java
  22. 29 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java
  23. 98 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java
  24. 154 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java
  25. 139 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java
  26. 53 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/Base64Utils.java
  27. 112 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java
  28. 121 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java
  29. 172 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java
  30. 74 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java
  31. 39 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java
  32. 95 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java
  33. 139 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java
  34. 1742 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StringUtils.java
  35. 3 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService
  36. 1 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  37. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png
  38. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png
  39. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png
  40. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png
  41. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png
  42. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png
  43. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png
  44. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png
  45. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png
  46. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png
  47. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png
  48. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png
  49. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png
  50. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png
  51. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png
  52. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png
  53. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png
  54. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png
  55. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png
  56. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png
  57. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png
  58. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png
  59. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png
  60. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png
  61. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png
  62. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png
  63. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png
  64. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png
  65. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png
  66. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png
  67. BIN
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf
  68. 55 0
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt

+ 0 - 7
yudao-dependencies/pom.xml

@@ -55,7 +55,6 @@
         <commons-net.version>3.8.0</commons-net.version>
         <jsch.version>0.1.55</jsch.version>
         <tika-core.version>2.6.0</tika-core.version>
-        <aj-captcha.version>1.3.0</aj-captcha.version>
         <netty-all.version>4.1.86.Final</netty-all.version>
         <ip2region.version>2.6.6</ip2region.version>
         <!-- 三方云服务相关 -->
@@ -447,12 +446,6 @@
                 <version>${tika-core.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>com.anji-plus</groupId>
-                <artifactId>spring-boot-starter-captcha</artifactId>
-                <version>${aj-captcha.version}</version>
-            </dependency>
-
             <dependency>
                 <groupId>org.apache.velocity</groupId>
                 <artifactId>velocity-engine-core</artifactId>

+ 6 - 5
yudao-framework/yudao-spring-boot-starter-captcha/pom.xml

@@ -23,17 +23,18 @@
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
         <!-- DB 相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-redis</artifactId>
         </dependency>
 
-        <!-- 验证码相关 -->
-        <dependency>
-            <groupId>com.anji-plus</groupId>
-            <artifactId>spring-boot-starter-captcha</artifactId>
-        </dependency>
     </dependencies>
 
 </project>

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java

@@ -0,0 +1,14 @@
+package com.anji.captcha.config;
+
+import com.anji.captcha.properties.AjCaptchaProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+@Configuration
+@EnableConfigurationProperties(AjCaptchaProperties.class)
+@ComponentScan("com.anji.captcha")
+@Import({AjCaptchaServiceAutoConfiguration.class, AjCaptchaStorageAutoConfiguration.class})
+public class AjCaptchaAutoConfiguration {
+}

+ 92 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java

@@ -0,0 +1,92 @@
+package com.anji.captcha.config;
+
+import com.anji.captcha.model.common.Const;
+import com.anji.captcha.properties.AjCaptchaProperties;
+import com.anji.captcha.service.CaptchaService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import com.anji.captcha.util.ImageUtils;
+import com.anji.captcha.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.util.Base64Utils;
+import org.springframework.util.FileCopyUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+@Configuration
+public class AjCaptchaServiceAutoConfiguration {
+
+    private static Logger logger = LoggerFactory.getLogger(AjCaptchaServiceAutoConfiguration.class);
+
+    @Bean
+    @ConditionalOnMissingBean
+    public CaptchaService captchaService(AjCaptchaProperties prop) {
+        logger.info("自定义配置项:{}", prop.toString());
+        Properties config = new Properties();
+        config.put(Const.CAPTCHA_CACHETYPE, prop.getCacheType().name());
+        config.put(Const.CAPTCHA_WATER_MARK, prop.getWaterMark());
+        config.put(Const.CAPTCHA_FONT_TYPE, prop.getFontType());
+        config.put(Const.CAPTCHA_TYPE, prop.getType().getCodeValue());
+        config.put(Const.CAPTCHA_INTERFERENCE_OPTIONS, prop.getInterferenceOptions());
+        config.put(Const.ORIGINAL_PATH_JIGSAW, prop.getJigsaw());
+        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_WATER_FONT, prop.getWaterFont());
+        config.put(Const.CAPTCHA_CACAHE_MAX_NUMBER, prop.getCacheNumber());
+        config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, prop.getTimingClear());
+
+        config.put(Const.HISTORY_DATA_CLEAR_ENABLE, prop.isHistoryDataClearEnable() ? "1" : "0");
+
+        config.put(Const.REQ_FREQUENCY_LIMIT_ENABLE, prop.getReqFrequencyLimitEnable() ? "1" : "0");
+        config.put(Const.REQ_GET_LOCK_LIMIT, prop.getReqGetLockLimit() + "");
+        config.put(Const.REQ_GET_LOCK_SECONDS, prop.getReqGetLockSeconds() + "");
+        config.put(Const.REQ_GET_MINUTE_LIMIT, prop.getReqGetMinuteLimit() + "");
+        config.put(Const.REQ_CHECK_MINUTE_LIMIT, prop.getReqCheckMinuteLimit() + "");
+        config.put(Const.REQ_VALIDATE_MINUTE_LIMIT, prop.getReqVerifyMinuteLimit() + "");
+
+        config.put(Const.CAPTCHA_FONT_SIZE, prop.getFontSize() + "");
+        config.put(Const.CAPTCHA_FONT_STYLE, prop.getFontStyle() + "");
+        config.put(Const.CAPTCHA_WORD_COUNT, prop.getClickWordCount() + "");
+
+        if ((StringUtils.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:"))
+                || (StringUtils.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:"))) {
+            //自定义resources目录下初始化底图
+            config.put(Const.CAPTCHA_INIT_ORIGINAL, "true");
+            initializeBaseMap(prop.getJigsaw(), prop.getPicClick());
+        }
+        CaptchaService s = CaptchaServiceFactory.getInstance(config);
+        return s;
+    }
+
+    private static void initializeBaseMap(String jigsaw, String picClick) {
+        ImageUtils.cacheBootImage(getResourcesImagesFile(jigsaw + "/original/*.png"),
+                getResourcesImagesFile(jigsaw + "/slidingBlock/*.png"),
+                getResourcesImagesFile(picClick + "/*.png"));
+    }
+
+    public static Map<String, String> getResourcesImagesFile(String path) {
+        Map<String, String> imgMap = new HashMap<>();
+        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
+        try {
+            Resource[] resources = resolver.getResources(path);
+            for (Resource resource : resources) {
+                byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
+                String string = Base64Utils.encodeToString(bytes);
+                String filename = resource.getFilename();
+                imgMap.put(filename, string);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return imgMap;
+    }
+}

+ 21 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java

@@ -0,0 +1,21 @@
+package com.anji.captcha.config;
+
+import com.anji.captcha.properties.AjCaptchaProperties;
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.service.impl.CaptchaServiceFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 存储策略自动配置.
+ *
+ */
+@Configuration
+public class AjCaptchaStorageAutoConfiguration {
+
+    @Bean(name = "AjCaptchaCacheService")
+    public CaptchaCacheService captchaCacheService(AjCaptchaProperties ajCaptchaProperties){
+        //缓存类型redis/local/....
+        return CaptchaServiceFactory.getCache(ajCaptchaProperties.getCacheType().name());
+    }
+}

+ 65 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/controller/CaptchaController.java

@@ -0,0 +1,65 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.controller;
+
+import com.anji.captcha.model.common.ResponseModel;
+import com.anji.captcha.model.vo.CaptchaVO;
+import com.anji.captcha.service.CaptchaService;
+import com.anji.captcha.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+@RestController
+@RequestMapping("/captcha")
+public class CaptchaController {
+
+    @Autowired
+    private CaptchaService captchaService;
+
+    @PostMapping("/get")
+    public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
+        assert request.getRemoteHost()!=null;
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.get(data);
+    }
+
+    @PostMapping("/check")
+    public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.check(data);
+    }
+
+    //@PostMapping("/verify")
+    public ResponseModel verify(@RequestBody CaptchaVO data, HttpServletRequest request) {
+        return captchaService.verification(data);
+    }
+
+    public static final String getRemoteId(HttpServletRequest request) {
+        String xfwd = request.getHeader("X-Forwarded-For");
+        String ip = getRemoteIpFromXfwd(xfwd);
+        String ua = request.getHeader("user-agent");
+        if (StringUtils.isNotBlank(ip)) {
+            return ip + ua;
+        }
+        return request.getRemoteAddr() + ua;
+    }
+
+    private static String getRemoteIpFromXfwd(String xfwd) {
+        if (StringUtils.isNotBlank(xfwd)) {
+            String[] ipList = xfwd.split(",");
+            return StringUtils.trim(ipList[0]);
+        }
+        return null;
+    }
+
+}

+ 50 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java

@@ -0,0 +1,50 @@
+package com.anji.captcha.model.common;
+
+/**
+ * 底图类型枚举
+ */
+public enum CaptchaBaseMapEnum {
+    ORIGINAL("ORIGINAL","滑动拼图底图"),
+    SLIDING_BLOCK("SLIDING_BLOCK","滑动拼图滑块底图"),
+    PIC_CLICK("PIC_CLICK","文字点选底图");
+
+    private String codeValue;
+    private String codeDesc;
+
+    private CaptchaBaseMapEnum(String  codeValue, String codeDesc) {
+        this.codeValue = codeValue;
+        this.codeDesc = codeDesc;
+    }
+
+    public String   getCodeValue(){ return this.codeValue;}
+
+    public String getCodeDesc(){ return this.codeDesc;}
+
+    //根据codeValue获取枚举
+    public static CaptchaBaseMapEnum parseFromCodeValue(String codeValue){
+        for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()){
+            if(e.codeValue.equals(codeValue)){ return e;}
+        }
+        return null;
+    }
+
+    //根据codeValue获取描述
+    public static String getCodeDescByCodeBalue(String codeValue){
+        CaptchaBaseMapEnum enumItem = parseFromCodeValue(codeValue);
+        return enumItem == null ? "" : enumItem.getCodeDesc();
+    }
+
+    //验证codeValue是否有效
+    public static boolean validateCodeValue(String codeValue){ return parseFromCodeValue(codeValue)!=null;}
+
+    //列出所有值字符串
+    public static String getString(){
+        StringBuffer buffer = new StringBuffer();
+        for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()){
+            buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", ");
+        }
+        buffer.deleteCharAt(buffer.lastIndexOf(","));
+        return buffer.toString().trim();
+    }
+
+}

+ 56 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java

@@ -0,0 +1,56 @@
+package com.anji.captcha.model.common;
+
+public enum CaptchaTypeEnum {
+    /**
+     * 滑块拼图.
+     */
+    BLOCKPUZZLE("blockPuzzle","滑块拼图"),
+    /**
+     * 文字点选.
+     */
+    CLICKWORD("clickWord","文字点选"),
+    /**
+     * 默认.
+     */
+    DEFAULT("default","默认");
+
+    private String codeValue;
+    private String codeDesc;
+
+    private CaptchaTypeEnum(String  codeValue, String codeDesc) {
+        this.codeValue = codeValue;
+        this.codeDesc = codeDesc;
+    }
+
+    public String   getCodeValue(){ return this.codeValue;}
+
+    public String getCodeDesc(){ return this.codeDesc;}
+
+    //根据codeValue获取枚举
+    public static CaptchaTypeEnum parseFromCodeValue(String codeValue){
+        for (CaptchaTypeEnum e : CaptchaTypeEnum.values()){
+            if(e.codeValue.equals(codeValue)){ return e;}
+        }
+        return null;
+    }
+
+    //根据codeValue获取描述
+    public static String getCodeDescByCodeBalue(String codeValue){
+        CaptchaTypeEnum enumItem = parseFromCodeValue(codeValue);
+        return enumItem == null ? "" : enumItem.getCodeDesc();
+    }
+
+    //验证codeValue是否有效
+    public static boolean validateCodeValue(String codeValue){ return parseFromCodeValue(codeValue)!=null;}
+
+    //列出所有值字符串
+    public static String getString(){
+        StringBuffer buffer = new StringBuffer();
+        for (CaptchaTypeEnum e : CaptchaTypeEnum.values()){
+            buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", ");
+        }
+        buffer.deleteCharAt(buffer.lastIndexOf(","));
+        return buffer.toString().trim();
+    }
+
+}

+ 112 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java

@@ -0,0 +1,112 @@
+package com.anji.captcha.model.common;
+
+/***
+ * @author wongbin
+ */
+public interface Const {
+
+    /**
+     *滑块底图路径
+     */
+    String ORIGINAL_PATH_JIGSAW = "captcha.captchaOriginalPath.jigsaw";
+
+    /***
+     *点选底图路径
+     */
+    String ORIGINAL_PATH_PIC_CLICK = "captcha.captchaOriginalPath.pic-click";
+
+    /**
+     * 缓存local/redis...
+     */
+    String CAPTCHA_CACHETYPE = "captcha.cacheType";
+
+    /**
+     * 右下角水印文字(我的水印)
+     */
+    String CAPTCHA_WATER_MARK = "captcha.water.mark";
+
+    /**
+     * 点选文字验证码的文字字体(宋体)
+     */
+    String CAPTCHA_FONT_TYPE = "captcha.font.type";
+	String CAPTCHA_FONT_STYLE = "captcha.font.style";
+	String CAPTCHA_FONT_SIZE = "captcha.font.size";
+
+    /**
+     * 验证码类型default两种都实例化。
+     */
+    String CAPTCHA_TYPE = "captcha.type";
+
+    /**
+     * 滑动干扰项(0/1/2)
+     */
+    String CAPTCHA_INTERFERENCE_OPTIONS = "captcha.interference.options";
+
+    /**
+     * 底图自定义初始化
+     */
+    String CAPTCHA_INIT_ORIGINAL = "captcha.init.original";
+
+    /**
+     * 滑动误差偏移量
+     */
+    String CAPTCHA_SLIP_OFFSET = "captcha.slip.offset";
+
+    /**
+     * aes加密开关
+     */
+    String CAPTCHA_AES_STATUS = "captcha.aes.status";
+
+    /**
+     * 右下角水印字体(宋体)
+     */
+    String CAPTCHA_WATER_FONT = "captcha.water.font";
+
+    /**
+     * local缓存的阈值
+     */
+    String CAPTCHA_CACAHE_MAX_NUMBER = "captcha.cache.number";
+
+    /**
+     * 定时清理过期local缓存,秒
+     */
+    String CAPTCHA_TIMING_CLEAR_SECOND = "captcha.timing.clear";
+
+	/**
+	 * 历史资源清除开关 0禁用,1 开启
+	 */
+	String HISTORY_DATA_CLEAR_ENABLE = "captcha.history.data.clear.enable";
+
+	/**
+	 * 接口限流开关 0禁用 1启用
+	 */
+	String REQ_FREQUENCY_LIMIT_ENABLE = "captcha.req.frequency.limit.enable";
+
+	/**
+	 * get 接口 一分钟请求次数限制
+	 */
+	String REQ_GET_MINUTE_LIMIT = "captcha.req.get.minute.limit";
+
+	/**
+	 * 验证失败后,get接口锁定时间
+	 */
+	String REQ_GET_LOCK_LIMIT = "captcha.req.get.lock.limit";
+	/**
+	 * 验证失败后,get接口锁定时间
+	 */
+	String REQ_GET_LOCK_SECONDS = "captcha.req.get.lock.seconds";
+
+	/**
+	 * verify 接口 一分钟请求次数限制
+	 */
+	String REQ_VALIDATE_MINUTE_LIMIT = "captcha.req.verify.minute.limit";
+	/**
+	 * check接口 一分钟请求次数限制
+	 */
+	String REQ_CHECK_MINUTE_LIMIT = "captcha.req.check.minute.limit";
+
+	/***
+	 * 点选文字个数
+	 */
+	String CAPTCHA_WORD_COUNT = "captcha.word.count";
+}

+ 73 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java

@@ -0,0 +1,73 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+import java.text.MessageFormat;
+
+/**
+ * 返回应答码
+ * @author
+ *
+ */
+public enum RepCodeEnum {
+
+    /** 0001 - 0099 网关应答码 */
+    SUCCESS("0000", "成功"),
+    ERROR("0001", "操作失败"),
+    EXCEPTION("9999", "服务器内部异常"),
+
+    BLANK_ERROR("0011", "{0}不能为空"),
+    NULL_ERROR("0011", "{0}不能为空"),
+    NOT_NULL_ERROR("0012", "{0}必须为空"),
+    NOT_EXIST_ERROR("0013", "{0}数据库中不存在"),
+    EXIST_ERROR("0014", "{0}数据库中已存在"),
+    PARAM_TYPE_ERROR("0015", "{0}类型错误"),
+    PARAM_FORMAT_ERROR("0016", "{0}格式错误"),
+
+    API_CAPTCHA_INVALID("6110", "验证码已失效,请重新获取"),
+    API_CAPTCHA_COORDINATE_ERROR("6111", "验证失败"),
+    API_CAPTCHA_ERROR("6112", "获取验证码失败,请联系管理员"),
+    API_CAPTCHA_BASEMAP_NULL("6113", "底图未初始化成功,请检查路径"),
+
+	API_REQ_LIMIT_GET_ERROR("6201", "get接口请求次数超限,请稍后再试!"),
+    API_REQ_INVALID("6206", "无效请求,请重新获取验证码"),
+	API_REQ_LOCK_GET_ERROR("6202", "接口验证失败数过多,请稍后再试"),
+	API_REQ_LIMIT_CHECK_ERROR("6204", "check接口请求次数超限,请稍后再试!"),
+	API_REQ_LIMIT_VERIFY_ERROR("6205", "verify请求次数超限!"),
+    ;
+    private String code;
+    private String desc;
+
+    RepCodeEnum(String code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+    public String getCode() {
+        return code;
+    }
+    public String getDesc() {
+        return desc;
+    }
+    public String getName(){
+        return this.name();
+    }
+
+    /** 将入参fieldNames与this.desc组合成错误信息
+     *  {fieldName}不能为空
+     * @param fieldNames
+     * @return
+     */
+    public ResponseModel parseError(Object... fieldNames) {
+        ResponseModel errorMessage=new ResponseModel();
+        String newDesc = MessageFormat.format(this.desc, fieldNames);
+
+        errorMessage.setRepCode(this.code);
+        errorMessage.setRepMsg(newDesc);
+        return errorMessage;
+    }
+
+}

+ 157 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java

@@ -0,0 +1,157 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+import com.anji.captcha.util.StringUtils;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+
+public class RequestModel implements Serializable {
+
+    private static final long serialVersionUID = -5800786065305114784L;
+
+    /**当前请求接口路径 /business/accessUser/login */
+    private String servletPath;
+
+    /** {"reqData":{"password":"*****","userName":"admin"},"sign":"a304a7f296f565b6d2009797f68180f0","time":"1542456453355","token":""} */
+    private String requestString;
+
+    /** {"password":"****","userName":"admin"} */
+    private HashMap reqData;
+
+    private String token;
+
+    private Long userId;
+
+    private String userName;
+
+    private List<Long> projectList;
+
+    //拥有哪些分组
+    private List<Long> groupIdList;
+
+    private String target;
+
+    private String sign;
+
+    private String time;
+
+    private String sourceIP;
+
+    /**
+     * 校验自身参数合法性
+     * @return
+     */
+    public boolean isVaildateRequest() {
+        if (StringUtils.isBlank(sign) || StringUtils.isBlank(time)) {
+            return false;
+        }
+        return true;
+    }
+
+    public String getServletPath() {
+        return servletPath;
+    }
+
+    public void setServletPath(String servletPath) {
+        this.servletPath = servletPath;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public static long getSerialVersionUID() {
+        return serialVersionUID;
+    }
+
+    public List<Long> getProjectList() {
+        return projectList;
+    }
+
+    public void setProjectList(List<Long> projectList) {
+        this.projectList = projectList;
+    }
+
+    public List<Long> getGroupIdList() {
+        return groupIdList;
+    }
+
+    public void setGroupIdList(List<Long> groupIdList) {
+        this.groupIdList = groupIdList;
+    }
+
+    public String getSign() {
+        return sign;
+    }
+
+    public void setSign(String sign) {
+        this.sign = sign;
+    }
+
+    public String getTime() {
+        return time;
+    }
+
+    public void setTime(String time) {
+        this.time = time;
+    }
+
+    public String getSourceIP() {
+        return sourceIP;
+    }
+
+    public void setSourceIP(String sourceIP) {
+        this.sourceIP = sourceIP;
+    }
+
+    public String getRequestString() {
+        return requestString;
+    }
+
+    public void setRequestString(String requestString) {
+        this.requestString = requestString;
+    }
+
+    public HashMap getReqData() {
+        return reqData;
+    }
+
+    public void setReqData(HashMap reqData) {
+        this.reqData = reqData;
+    }
+
+    public String getTarget() {
+        return target;
+    }
+
+    public void setTarget(String target) {
+        this.target = target;
+    }
+}

+ 111 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java

@@ -0,0 +1,111 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.common;
+
+import com.anji.captcha.util.StringUtils;
+
+import java.io.Serializable;
+
+public class ResponseModel implements Serializable {
+
+    private static final long serialVersionUID = 8445617032523881407L;
+
+    private String            repCode;
+
+    private String            repMsg;
+
+    private Object            repData;
+
+    public ResponseModel() {
+        this.repCode = RepCodeEnum.SUCCESS.getCode();
+    }
+
+    public ResponseModel(RepCodeEnum repCodeEnum) {
+       this.setRepCodeEnum(repCodeEnum);
+    }
+
+    //成功
+    public static ResponseModel success(){
+        return ResponseModel.successMsg("成功");
+    }
+    public static ResponseModel successMsg(String message){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+    public static ResponseModel successData(Object data){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.SUCCESS.getCode());
+        responseModel.setRepData(data);
+        return responseModel;
+    }
+
+    //失败
+    public static ResponseModel errorMsg(RepCodeEnum message){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCodeEnum(message);
+        return responseModel;
+    }
+    public static ResponseModel errorMsg(String message){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.ERROR.getCode());
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+    public static ResponseModel errorMsg(RepCodeEnum repCodeEnum, String message){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(repCodeEnum.getCode());
+        responseModel.setRepMsg(message);
+        return responseModel;
+    }
+    public static ResponseModel exceptionMsg(String message){
+        ResponseModel responseModel = new ResponseModel();
+        responseModel.setRepCode(RepCodeEnum.EXCEPTION.getCode());
+        responseModel.setRepMsg(RepCodeEnum.EXCEPTION.getDesc() + ": " + message);
+        return responseModel;
+    }
+
+	@Override
+	public String toString() {
+		return "ResponseModel{" + "repCode='" + repCode + '\'' + ", repMsg='"
+				+ repMsg + '\'' + ", repData=" + repData + '}';
+	}
+
+	public boolean isSuccess(){
+        return StringUtils.equals(repCode, RepCodeEnum.SUCCESS.getCode());
+    }
+
+    public String getRepCode() {
+        return repCode;
+    }
+
+    public void setRepCode(String repCode) {
+        this.repCode = repCode;
+    }
+    public void setRepCodeEnum(RepCodeEnum repCodeEnum) {
+        this.repCode=repCodeEnum.getCode();
+        this.repMsg=repCodeEnum.getDesc();
+    }
+
+    public String getRepMsg() {
+        return repMsg;
+    }
+
+    public void setRepMsg(String repMsg) {
+        this.repMsg = repMsg;
+    }
+
+    public Object getRepData() {
+        return repData;
+    }
+
+    public void setRepData(Object repData) {
+        this.repData = repData;
+    }
+
+
+}

+ 251 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java

@@ -0,0 +1,251 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.model.vo;
+
+import java.awt.*;
+import java.io.Serializable;
+import java.util.List;
+
+public class CaptchaVO implements Serializable {
+
+    /**
+     * 验证码id(后台申请)
+     */
+    private String captchaId;
+
+    private String projectCode;
+
+    /**
+     * 验证码类型:(clickWord,blockPuzzle)
+     */
+    private String captchaType;
+
+    private String captchaOriginalPath;
+
+    private String captchaFontType;
+
+    private Integer captchaFontSize;
+
+    private String secretKey;
+
+    /**
+     * 原生图片base64
+     */
+    private String originalImageBase64;
+
+    /**
+     * 滑块点选坐标
+     */
+    private PointVO point;
+
+    /**
+     * 滑块图片base64
+     */
+    private String jigsawImageBase64;
+
+    /**
+     * 点选文字
+     */
+    private List<String> wordList;
+
+    /**
+     * 点选坐标
+     */
+    private List<Point> pointList;
+
+
+    /**
+     * 点坐标(base64加密传输)
+     */
+    private String pointJson;
+
+
+    /**
+     * UUID(每次请求的验证码唯一标识)
+     */
+    private String token;
+
+    /**
+     * 校验结果
+     */
+    private Boolean result = false;
+
+    /**
+     * 后台二次校验参数
+     */
+    private String captchaVerification;
+
+	/***
+	 * 客户端UI组件id,组件初始化时设置一次,UUID
+	 */
+	private String clientUid;
+	/***
+	 * 客户端的请求时间,预留字段
+	 */
+	private Long ts;
+
+    /***
+     * 客户端ip+userAgent
+     */
+    private String browserInfo;
+    public void resetClientFlag(){
+        this.browserInfo = null;
+        this.clientUid = null;
+    }
+
+    public String getCaptchaId() {
+        return captchaId;
+    }
+
+    public void setCaptchaId(String captchaId) {
+        this.captchaId = captchaId;
+    }
+
+    public String getProjectCode() {
+        return projectCode;
+    }
+
+    public void setProjectCode(String projectCode) {
+        this.projectCode = projectCode;
+    }
+
+    public String getCaptchaType() {
+        return captchaType;
+    }
+
+    public void setCaptchaType(String captchaType) {
+        this.captchaType = captchaType;
+    }
+
+    public String getCaptchaOriginalPath() {
+        return captchaOriginalPath;
+    }
+
+    public void setCaptchaOriginalPath(String captchaOriginalPath) {
+        this.captchaOriginalPath = captchaOriginalPath;
+    }
+
+    public String getCaptchaFontType() {
+        return captchaFontType;
+    }
+
+    public void setCaptchaFontType(String captchaFontType) {
+        this.captchaFontType = captchaFontType;
+    }
+
+    public Integer getCaptchaFontSize() {
+        return captchaFontSize;
+    }
+
+    public void setCaptchaFontSize(Integer captchaFontSize) {
+        this.captchaFontSize = captchaFontSize;
+    }
+
+    public String getOriginalImageBase64() {
+        return originalImageBase64;
+    }
+
+    public void setOriginalImageBase64(String originalImageBase64) {
+        this.originalImageBase64 = originalImageBase64;
+    }
+
+    public PointVO getPoint() {
+        return point;
+    }
+
+    public void setPoint(PointVO point) {
+        this.point = point;
+    }
+
+    public String getJigsawImageBase64() {
+        return jigsawImageBase64;
+    }
+
+    public void setJigsawImageBase64(String jigsawImageBase64) {
+        this.jigsawImageBase64 = jigsawImageBase64;
+    }
+
+    public List<String> getWordList() {
+        return wordList;
+    }
+
+    public void setWordList(List<String> wordList) {
+        this.wordList = wordList;
+    }
+
+    public List<Point> getPointList() {
+        return pointList;
+    }
+
+    public void setPointList(List<Point> pointList) {
+        this.pointList = pointList;
+    }
+
+    public String getPointJson() {
+        return pointJson;
+    }
+
+    public void setPointJson(String pointJson) {
+        this.pointJson = pointJson;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public Boolean getResult() {
+        return result;
+    }
+
+    public void setResult(Boolean result) {
+        this.result = result;
+    }
+
+    public String getCaptchaVerification() {
+        return captchaVerification;
+    }
+
+    public void setCaptchaVerification(String captchaVerification) {
+        this.captchaVerification = captchaVerification;
+    }
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+	public String getClientUid() {
+		return clientUid;
+	}
+
+	public void setClientUid(String clientUid) {
+		this.clientUid = clientUid;
+	}
+
+	public Long getTs() {
+		return ts;
+	}
+
+	public void setTs(Long ts) {
+		this.ts = ts;
+	}
+
+    public String getBrowserInfo() {
+        return browserInfo;
+    }
+
+    public void setBrowserInfo(String browserInfo) {
+        this.browserInfo = browserInfo;
+    }
+}

+ 95 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java

@@ -0,0 +1,95 @@
+package com.anji.captcha.model.vo;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Created by raodeming on 2020/5/16.
+ */
+public class PointVO {
+    private String secretKey;
+
+    public int x;
+
+    public int y;
+
+    public String getSecretKey() {
+        return secretKey;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    public int getX() {
+        return x;
+    }
+
+    public void setX(int x) {
+        this.x = x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public void setY(int y) {
+        this.y = y;
+    }
+
+    public PointVO(int x, int y, String secretKey) {
+        this.secretKey = secretKey;
+        this.x = x;
+        this.y = y;
+    }
+
+    public PointVO() {
+    }
+
+    public PointVO(int x, int y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public String toJsonString() {
+        return String.format("{\"secretKey\":\"%s\",\"x\":%d,\"y\":%d}", secretKey, x, y);
+    }
+
+    public PointVO parse(String jsonStr) {
+        Map<String, Object> 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);
+    }
+}

+ 338 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java

@@ -0,0 +1,338 @@
+package com.anji.captcha.properties;
+
+import com.anji.captcha.model.common.CaptchaTypeEnum;
+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;
+
+@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 int getFontStyle() {
+        return fontStyle;
+    }
+
+    public void setFontStyle(int fontStyle) {
+        this.fontStyle = fontStyle;
+    }
+
+    public int getFontSize() {
+        return fontSize;
+    }
+
+    public void setFontSize(int fontSize) {
+        this.fontSize = fontSize;
+    }
+
+    public int getClickWordCount() {
+        return clickWordCount;
+    }
+
+    public void setClickWordCount(int clickWordCount) {
+        this.clickWordCount = clickWordCount;
+    }
+
+    public boolean isHistoryDataClearEnable() {
+        return historyDataClearEnable;
+    }
+
+    public void setHistoryDataClearEnable(boolean historyDataClearEnable) {
+        this.historyDataClearEnable = historyDataClearEnable;
+    }
+
+    public boolean isReqFrequencyLimitEnable() {
+        return reqFrequencyLimitEnable;
+    }
+
+    public boolean getReqFrequencyLimitEnable() {
+        return reqFrequencyLimitEnable;
+    }
+
+    public void setReqFrequencyLimitEnable(boolean reqFrequencyLimitEnable) {
+        this.reqFrequencyLimitEnable = reqFrequencyLimitEnable;
+    }
+
+    public int getReqGetLockLimit() {
+        return reqGetLockLimit;
+    }
+
+    public void setReqGetLockLimit(int reqGetLockLimit) {
+        this.reqGetLockLimit = reqGetLockLimit;
+    }
+
+    public int getReqGetLockSeconds() {
+        return reqGetLockSeconds;
+    }
+
+    public void setReqGetLockSeconds(int reqGetLockSeconds) {
+        this.reqGetLockSeconds = reqGetLockSeconds;
+    }
+
+    public int getReqGetMinuteLimit() {
+        return reqGetMinuteLimit;
+    }
+
+    public void setReqGetMinuteLimit(int reqGetMinuteLimit) {
+        this.reqGetMinuteLimit = reqGetMinuteLimit;
+    }
+
+    public int getReqCheckMinuteLimit() {
+        return reqGetMinuteLimit;
+    }
+
+    public void setReqCheckMinuteLimit(int reqCheckMinuteLimit) {
+        this.reqCheckMinuteLimit = reqCheckMinuteLimit;
+    }
+
+    public int getReqVerifyMinuteLimit() {
+        return reqVerifyMinuteLimit;
+    }
+
+    public void setReqVerifyMinuteLimit(int reqVerifyMinuteLimit) {
+        this.reqVerifyMinuteLimit = reqVerifyMinuteLimit;
+    }
+
+    public enum StorageType {
+        /**
+         * 内存.
+         */
+        local,
+        /**
+         * redis.
+         */
+        redis,
+        /**
+         * 其他.
+         */
+        other,
+    }
+
+    public static String getPrefix() {
+        return PREFIX;
+    }
+
+    public CaptchaTypeEnum getType() {
+        return type;
+    }
+
+    public void setType(CaptchaTypeEnum type) {
+        this.type = type;
+    }
+
+    public String getJigsaw() {
+        return jigsaw;
+    }
+
+    public void setJigsaw(String jigsaw) {
+        this.jigsaw = jigsaw;
+    }
+
+    public String getPicClick() {
+        return picClick;
+    }
+
+    public void setPicClick(String picClick) {
+        this.picClick = picClick;
+    }
+
+    public String getWaterMark() {
+        return waterMark;
+    }
+
+    public void setWaterMark(String waterMark) {
+        this.waterMark = waterMark;
+    }
+
+    public String getWaterFont() {
+        return waterFont;
+    }
+
+    public void setWaterFont(String waterFont) {
+        this.waterFont = waterFont;
+    }
+
+    public String getFontType() {
+        return fontType;
+    }
+
+    public void setFontType(String fontType) {
+        this.fontType = fontType;
+    }
+
+    public String getSlipOffset() {
+        return slipOffset;
+    }
+
+    public void setSlipOffset(String slipOffset) {
+        this.slipOffset = slipOffset;
+    }
+
+    public Boolean getAesStatus() {
+        return aesStatus;
+    }
+
+    public void setAesStatus(Boolean aesStatus) {
+        this.aesStatus = aesStatus;
+    }
+
+    public StorageType getCacheType() {
+        return cacheType;
+    }
+
+    public void setCacheType(StorageType cacheType) {
+        this.cacheType = cacheType;
+    }
+
+    public String getInterferenceOptions() {
+        return interferenceOptions;
+    }
+
+    public void setInterferenceOptions(String interferenceOptions) {
+        this.interferenceOptions = interferenceOptions;
+    }
+
+    public String getCacheNumber() {
+        return cacheNumber;
+    }
+
+    public void setCacheNumber(String cacheNumber) {
+        this.cacheNumber = cacheNumber;
+    }
+
+    public String getTimingClear() {
+        return timingClear;
+    }
+
+    public void setTimingClear(String timingClear) {
+        this.timingClear = timingClear;
+    }
+
+    @Override
+    public String toString() {
+        return "\nAjCaptchaProperties{" +
+                "type=" + type +
+                ", jigsaw='" + jigsaw + '\'' +
+                ", picClick='" + picClick + '\'' +
+                ", waterMark='" + waterMark + '\'' +
+                ", waterFont='" + waterFont + '\'' +
+                ", fontType='" + fontType + '\'' +
+                ", slipOffset='" + slipOffset + '\'' +
+                ", aesStatus=" + aesStatus +
+                ", interferenceOptions='" + interferenceOptions + '\'' +
+                ", cacheNumber='" + cacheNumber + '\'' +
+                ", timingClear='" + timingClear + '\'' +
+                ", cacheType=" + cacheType +
+                ", reqFrequencyLimitEnable=" + reqFrequencyLimitEnable +
+                ", reqGetLockLimit=" + reqGetLockLimit +
+                ", reqGetLockSeconds=" + reqGetLockSeconds +
+                ", reqGetMinuteLimit=" + reqGetMinuteLimit +
+                ", reqCheckMinuteLimit=" + reqCheckMinuteLimit +
+                ", reqVerifyMinuteLimit=" + reqVerifyMinuteLimit +
+                '}';
+    }
+}

+ 41 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java

@@ -0,0 +1,41 @@
+/*
+ *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;
+	};
+
+}

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java

@@ -0,0 +1,58 @@
+/*
+ *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);
+}

+ 269 - 0
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 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+public abstract class AbstractCaptchaService implements CaptchaService {
+
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    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));
+        }
+        logger.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 (cacheType.equals("local")) {
+            logger.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 (config.getProperty(Const.HISTORY_DATA_CLEAR_ENABLE, "0").equals("1")) {
+            logger.info("历史资源清除开关...开启..." + captchaType());
+            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    destroy(config);
+                }
+            }));
+        }
+        if (config.getProperty(Const.REQ_FREQUENCY_LIMIT_ENABLE, "0").equals("1")) {
+            if (limitHandler == null) {
+                logger.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 (StringUtils.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(StringUtils.isNotEmpty(req.getBrowserInfo())){
+			return MD5Util.md5(req.getBrowserInfo());
+		}
+		// 以客户端Ui组件id做识别标志
+		if(StringUtils.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,
+                        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) {
+            logger.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 = new FileOutputStream(tempFile);
+            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;
+    }
+
+
+}

+ 422 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java

@@ -0,0 +1,422 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+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 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;
+
+/**
+ * 滑动验证码
+ * <p>
+ * Created by raodeming on 2019/12/25.
+ */
+public class BlockPuzzleCaptchaServiceImpl extends AbstractCaptchaService {
+
+    @Override
+    public void init(Properties config) {
+        super.init(config);
+    }
+
+	@Override
+	public void destroy(Properties config) {
+        logger.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) {
+            logger.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) {
+            logger.error("滑动底图未初始化成功,请检查路径");
+            return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
+        }
+        CaptchaVO captcha = pictureTemplatesCut(originalImage, jigsawImage, jigsawImageBase64);
+        if (captcha == null
+                || StringUtils.isBlank(captcha.getJigsawImageBase64())
+                || StringUtils.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) {
+            logger.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) {
+            logger.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) {
+            logger.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);
+            logger.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();
+    }
+
+
+}

+ 47 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java

@@ -0,0 +1,47 @@
+package com.anji.captcha.service.impl;
+
+import com.anji.captcha.service.CaptchaCacheService;
+import com.anji.captcha.util.CacheUtil;
+
+/**
+ * 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。
+ * 如果应用是单点的,也没有使用redis,那默认使用内存。
+ * 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
+ * @Title: 默认使用内存当缓存
+ * @author lide1202@hotmail.com
+ * @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.valueOf(CacheUtil.get(key))+val;
+		CacheUtil.set(key,ret+"",0);
+		return ret;
+	}
+
+	@Override
+    public String type() {
+        return "local";
+    }
+}

+ 60 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java

@@ -0,0 +1,60 @@
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+/**
+ * Created by raodeming on 2020/5/26.
+ */
+public class CaptchaServiceFactory {
+
+    private static Logger logger = LoggerFactory.getLogger(CaptchaServiceFactory.class);
+
+    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<String, CaptchaService> instances = new HashMap();
+    public volatile static Map<String, CaptchaCacheService> cacheService = new HashMap();
+
+    static {
+        ServiceLoader<CaptchaCacheService> cacheServices = ServiceLoader.load(CaptchaCacheService.class);
+        for (CaptchaCacheService item : cacheServices) {
+            cacheService.put(item.type(), item);
+        }
+        logger.info("supported-captchaCache-service:{}", cacheService.keySet().toString());
+        ServiceLoader<CaptchaService> services = ServiceLoader.load(CaptchaService.class);
+        for (CaptchaService item : services) {
+            instances.put(item.captchaType(), item);
+        }
+        ;
+        logger.info("supported-captchaTypes-service:{}", instances.keySet().toString());
+    }
+}

Fichier diff supprimé car celui-ci est trop grand
+ 29 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java


+ 98 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java

@@ -0,0 +1,98 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.service.impl;
+
+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 com.anji.captcha.util.StringUtils;
+
+import java.util.Properties;
+
+/**
+ * Created by raodeming on 2019/12/25.
+ */
+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 (StringUtils.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 (StringUtils.isEmpty(captchaVO.getCaptchaType())) {
+            return RepCodeEnum.NULL_ERROR.parseError("类型");
+        }
+        if (StringUtils.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 (StringUtils.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) {
+            logger.error("验证码坐标解析失败", e);
+            return ResponseModel.errorMsg(e.getMessage());
+        }
+        return ResponseModel.success();
+    }
+
+}

+ 154 - 0
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 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.util.StringUtils;
+
+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(StringUtils.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.valueOf(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.valueOf(failCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_LIMIT, "5"))) {
+                // get接口锁定5分钟
+                cacheService.set(lockKey, "1", Long.valueOf(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(StringUtils.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.valueOf(v) > Long.valueOf(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.valueOf(v) > Long.valueOf(config.getProperty(Const.REQ_VALIDATE_MINUTE_LIMIT, "600"))) {
+                return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_VERIFY_ERROR);
+            }
+            return null;
+        }
+    }
+
+}

+ 139 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java

@@ -0,0 +1,139 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.SecretKeySpec;
+import java.math.BigInteger;
+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 StringUtils.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("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 (StringUtils.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 (StringUtils.isBlank(decryptKey)) {
+            return encryptStr;
+        }
+        return StringUtils.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);
+    }
+
+}

+ 53 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/Base64Utils.java

@@ -0,0 +1,53 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by Fernflower decompiler)
+//
+
+package com.anji.captcha.util;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public abstract class Base64Utils {
+    private static final Charset DEFAULT_CHARSET;
+
+    public Base64Utils() {
+    }
+
+    public static byte[] encode(byte[] src) {
+        return src.length == 0 ? src : Base64.getEncoder().encode(src);
+    }
+
+    public static byte[] decode(byte[] src) {
+        return src.length == 0 ? src : Base64.getDecoder().decode(src);
+    }
+
+    public static byte[] encodeUrlSafe(byte[] src) {
+        return src.length == 0 ? src : Base64.getUrlEncoder().encode(src);
+    }
+
+    public static byte[] decodeUrlSafe(byte[] src) {
+        return src.length == 0 ? src : Base64.getUrlDecoder().decode(src);
+    }
+
+    public static String encodeToString(byte[] src) {
+        return src.length == 0 ? "" : new String(encode(src), DEFAULT_CHARSET);
+    }
+
+    public static byte[] decodeFromString(String src) {
+        return src.isEmpty() ? new byte[0] : decode(src.getBytes(DEFAULT_CHARSET));
+    }
+
+    public static String encodeToUrlSafeString(byte[] src) {
+        return new String(encodeUrlSafe(src), DEFAULT_CHARSET);
+    }
+
+    public static byte[] decodeFromUrlSafeString(String src) {
+        return decodeUrlSafe(src.getBytes(DEFAULT_CHARSET));
+    }
+
+    static {
+        DEFAULT_CHARSET = StandardCharsets.UTF_8;
+    }
+}

+ 112 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java

@@ -0,0 +1,112 @@
+/*
+ *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<String, Object> CACHE_MAP = new ConcurrentHashMap<String, Object>();
+
+    /**
+     * 缓存最大个数
+     */
+    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);*/
+            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);
+        }
+    }
+
+    private static ScheduledExecutorService scheduledExecutor;
+
+    /**
+     * 缓存刷新,清除过期数据
+     */
+    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();
+    }
+}

+ 121 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java

@@ -0,0 +1,121 @@
+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();
+            int var5 = byteCount;
+            return var5;
+        } 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();
+        }
+    }
+}
+

+ 172 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java

@@ -0,0 +1,172 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+import com.anji.captcha.model.common.CaptchaBaseMapEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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;
+
+
+public class ImageUtils {
+    private static Logger logger = LoggerFactory.getLogger(ImageUtils.class);
+    private static Map<String, String> originalCacheMap = new ConcurrentHashMap();  //滑块底图
+    private static Map<String, String> slidingBlockCacheMap = new ConcurrentHashMap(); //滑块
+    private static Map<String, String> picClickCacheMap = new ConcurrentHashMap(); //点选文字
+    private static Map<String, String[]> fileNameMap = new ConcurrentHashMap<>();
+
+    public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) {
+        //滑动拼图
+        if (StringUtils.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 (StringUtils.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]));
+        logger.info("初始化底图:{}", JsonUtil.toJSONString(fileNameMap));
+    }
+
+    public static void cacheBootImage(Map<String, String> originalMap, Map<String, String> slidingBlockMap, Map<String, String> 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]));
+        logger.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);
+        String s = slidingBlockCacheMap.get(strings[randomInt]);
+        return s;
+    }
+
+    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<String, String> getResourcesImagesFile(String path) {
+        //默认提供六张底图
+        Map<String, String> 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<String, String> getImagesFile(String path) {
+        Map<String, String> 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 (FileNotFoundException e) {
+                e.printStackTrace();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        });
+        return imgMap;
+    }
+
+}

+ 74 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java

@@ -0,0 +1,74 @@
+package com.anji.captcha.util;
+
+import com.anji.captcha.model.vo.PointVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 替换掉fastjson,自定义实现相关方法
+ * note: 该实现不具有通用性,仅用于本项目。
+ *
+ *@author WongBin
+ *@date 2021/1/8
+ */
+public class JsonUtil {
+	private static Logger logger = LoggerFactory.getLogger(JsonUtil.class);
+	public static List<PointVO> parseArray(String text, Class<PointVO> clazz) {
+		if (text == null) {
+			return null;
+		} else {
+			String[] arr = text.replaceFirst("\\[","")
+					.replaceFirst("\\]","").split("\\}");
+			List<PointVO> ret = new ArrayList<>(arr.length);
+			for (String s : arr) {
+				ret.add(parseObject(s,PointVO.class));
+			}
+			return ret;
+		}
+	}
+
+	public static PointVO parseObject(String text, Class<PointVO> 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){
+			logger.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<PointVO> list = (List<PointVO>)object;
+			StringBuilder buf = new StringBuilder("[");
+			list.stream().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());
+	}
+}

+ 39 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java

@@ -0,0 +1,39 @@
+package com.anji.captcha.util;
+
+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("UTF8"));
+            byte[] s = m.digest();
+            StringBuilder result = new StringBuilder();
+            for (int i = 0; i < s.length; i++) {
+                result.append(Integer.toHexString((0x000000FF & s[i]) | 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);
+    }
+
+}

+ 95 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java

@@ -0,0 +1,95 @@
+/*
+ *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) {
+        String ch = hanZi.charAt(new Random().nextInt(hanZi.length())) + "";
+        return ch;
+    }
+
+    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();
+    }
+
+}

+ 139 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StreamUtils.java

@@ -0,0 +1,139 @@
+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];
+            boolean var5 = true;
+
+            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 {
+        }
+    }
+}
+

+ 1742 - 0
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StringUtils.java

@@ -0,0 +1,1742 @@
+/*
+ *Copyright © 2018 anji-plus
+ *安吉加加信息技术有限公司
+ *http://www.anji-plus.com
+ *All rights reserved.
+ */
+package com.anji.captcha.util;
+
+public class StringUtils {
+
+    /**
+     * The empty String <code>""</code>.
+     * @since 2.0
+     */
+    public static final String EMPTY = "";
+
+    /**
+     * Represents a failed index search.
+     * @since 2.1
+     */
+    public static final int INDEX_NOT_FOUND = -1;
+
+    /**
+     * <p>The maximum size to which the padding constant(s) can expand.</p>
+     */
+    private static final int PAD_LIMIT = 8192;
+
+    /**
+     * <p><code>StringUtils</code> instances should NOT be constructed in
+     * standard programming. Instead, the class should be used as
+     * <code>StringUtils.trim(" foo ");</code>.</p>
+     *
+     * <p>This constructor is public to permit tools that require a JavaBean
+     * instance to operate.</p>
+     */
+    public StringUtils() {
+        super();
+    }
+
+    // Empty checks
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Checks if a String is empty ("") or null.</p>
+     *
+     * <pre>
+     * StringUtils.isEmpty(null)      = true
+     * StringUtils.isEmpty("")        = true
+     * StringUtils.isEmpty(" ")       = false
+     * StringUtils.isEmpty("bob")     = false
+     * StringUtils.isEmpty("  bob  ") = false
+     * </pre>
+     *
+     * <p>NOTE: This method changed in Lang version 2.0.
+     * It no longer trims the String.
+     * That functionality is available in isBlank().</p>
+     *
+     * @param str  the String to check, may be null
+     * @return <code>true</code> if the String is empty or null
+     */
+    public static boolean isEmpty(String str) {
+        return str == null || str.length() == 0;
+    }
+
+    /**
+     * <p>Checks if a String is not empty ("") and not null.</p>
+     *
+     * <pre>
+     * StringUtils.isNotEmpty(null)      = false
+     * StringUtils.isNotEmpty("")        = false
+     * StringUtils.isNotEmpty(" ")       = true
+     * StringUtils.isNotEmpty("bob")     = true
+     * StringUtils.isNotEmpty("  bob  ") = true
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @return <code>true</code> if the String is not empty and not null
+     */
+    public static boolean isNotEmpty(String str) {
+        return !StringUtils.isEmpty(str);
+    }
+
+    /**
+     * <p>Checks if a String is whitespace, empty ("") or null.</p>
+     *
+     * <pre>
+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("null")      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @return <code>true</code> if the String is null, empty or whitespace
+     * @since 2.0
+     */
+    public static boolean isBlank(String str) {
+        int strLen;
+        if (str == null || (strLen = str.length()) == 0) {
+            return true;
+        }
+        if(equals("null", str.trim().toLowerCase())){
+            return true;
+        }
+        for (int i = 0; i < strLen; i++) {
+            if ((Character.isWhitespace(str.charAt(i)) == false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * <p>Checks if a String is not empty (""), not null and not whitespace only.</p>
+     *
+     * <pre>
+     * StringUtils.isNotBlank(null)      = false
+     * StringUtils.isNotBlank("null")    = false
+     * StringUtils.isNotBlank("")        = false
+     * StringUtils.isNotBlank(" ")       = false
+     * StringUtils.isNotBlank("bob")     = true
+     * StringUtils.isNotBlank("  bob  ") = true
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @return <code>true</code> if the String is
+     *  not empty and not null and not whitespace
+     * @since 2.0
+     */
+    public static boolean isNotBlank(String str) {
+        return !StringUtils.isBlank(str);
+    }
+
+    // Trim
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Removes control characters (char &lt;= 32) from both
+     * ends of this String, handling <code>null</code> by returning
+     * an empty String ("").</p>
+     *
+     * <pre>
+     * StringUtils.clean(null)          = ""
+     * StringUtils.clean("")            = ""
+     * StringUtils.clean("abc")         = "abc"
+     * StringUtils.clean("    abc    ") = "abc"
+     * StringUtils.clean("     ")       = ""
+     * </pre>
+     *
+     * @see String#trim()
+     * @param str  the String to clean, may be null
+     * @return the trimmed text, never <code>null</code>
+     * @deprecated Use the clearer named {@link #trimToEmpty(String)}.
+     *             Method will be removed in Commons Lang 3.0.
+     */
+    public static String clean(String str) {
+        return str == null ? EMPTY : str.trim();
+    }
+
+    /**
+     * <p>Removes control characters (char &lt;= 32) from both
+     * ends of this String, handling <code>null</code> by returning
+     * <code>null</code>.</p>
+     *
+     * <p>The String is trimmed using {@link String#trim()}.
+     * Trim removes start and end characters &lt;= 32.
+     * To strip whitespace use {@link #strip(String)}.</p>
+     *
+     * <p>To trim your choice of characters, use the
+     * {@link #strip(String, String)} methods.</p>
+     *
+     * <pre>
+     * StringUtils.trim(null)          = null
+     * StringUtils.trim("")            = ""
+     * StringUtils.trim("     ")       = ""
+     * StringUtils.trim("abc")         = "abc"
+     * StringUtils.trim("    abc    ") = "abc"
+     * </pre>
+     *
+     * @param str  the String to be trimmed, may be null
+     * @return the trimmed string, <code>null</code> if null String input
+     */
+    public static String trim(String str) {
+        return str == null ? null : str.trim();
+    }
+
+    /**
+     * <p>Removes control characters (char &lt;= 32) from both
+     * ends of this String returning <code>null</code> if the String is
+     * empty ("") after the trim or if it is <code>null</code>.
+     *
+     * <p>The String is trimmed using {@link String#trim()}.
+     * Trim removes start and end characters &lt;= 32.
+     * To strip whitespace use {@link #stripToNull(String)}.</p>
+     *
+     * <pre>
+     * StringUtils.trimToNull(null)          = null
+     * StringUtils.trimToNull("")            = null
+     * StringUtils.trimToNull("     ")       = null
+     * StringUtils.trimToNull("abc")         = "abc"
+     * StringUtils.trimToNull("    abc    ") = "abc"
+     * </pre>
+     *
+     * @param str  the String to be trimmed, may be null
+     * @return the trimmed String,
+     *  <code>null</code> if only chars &lt;= 32, empty or null String input
+     * @since 2.0
+     */
+    public static String trimToNull(String str) {
+        String ts = trim(str);
+        return isEmpty(ts) ? null : ts;
+    }
+
+    /**
+     * <p>Removes control characters (char &lt;= 32) from both
+     * ends of this String returning an empty String ("") if the String
+     * is empty ("") after the trim or if it is <code>null</code>.
+     *
+     * <p>The String is trimmed using {@link String#trim()}.
+     * Trim removes start and end characters &lt;= 32.
+     * To strip whitespace use {@link #stripToEmpty(String)}.</p>
+     *
+     * <pre>
+     * StringUtils.trimToEmpty(null)          = ""
+     * StringUtils.trimToEmpty("")            = ""
+     * StringUtils.trimToEmpty("     ")       = ""
+     * StringUtils.trimToEmpty("abc")         = "abc"
+     * StringUtils.trimToEmpty("    abc    ") = "abc"
+     * </pre>
+     *
+     * @param str  the String to be trimmed, may be null
+     * @return the trimmed String, or an empty String if <code>null</code> input
+     * @since 2.0
+     */
+    public static String trimToEmpty(String str) {
+        return str == null ? EMPTY : str.trim();
+    }
+
+    // Stripping
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Strips whitespace from the start and end of a String.</p>
+     *
+     * <p>This is similar to {@link #trim(String)} but removes whitespace.
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.</p>
+     *
+     * <pre>
+     * StringUtils.strip(null)     = null
+     * StringUtils.strip("")       = ""
+     * StringUtils.strip("   ")    = ""
+     * StringUtils.strip("abc")    = "abc"
+     * StringUtils.strip("  abc")  = "abc"
+     * StringUtils.strip("abc  ")  = "abc"
+     * StringUtils.strip(" abc ")  = "abc"
+     * StringUtils.strip(" ab c ") = "ab c"
+     * </pre>
+     *
+     * @param str  the String to remove whitespace from, may be null
+     * @return the stripped String, <code>null</code> if null String input
+     */
+    public static String strip(String str) {
+        return strip(str, null);
+    }
+
+    /**
+     * <p>Strips whitespace from the start and end of a String  returning
+     * <code>null</code> if the String is empty ("") after the strip.</p>
+     *
+     * <p>This is similar to {@link #trimToNull(String)} but removes whitespace.
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.stripToNull(null)     = null
+     * StringUtils.stripToNull("")       = null
+     * StringUtils.stripToNull("   ")    = null
+     * StringUtils.stripToNull("abc")    = "abc"
+     * StringUtils.stripToNull("  abc")  = "abc"
+     * StringUtils.stripToNull("abc  ")  = "abc"
+     * StringUtils.stripToNull(" abc ")  = "abc"
+     * StringUtils.stripToNull(" ab c ") = "ab c"
+     * </pre>
+     *
+     * @param str  the String to be stripped, may be null
+     * @return the stripped String,
+     *  <code>null</code> if whitespace, empty or null String input
+     * @since 2.0
+     */
+    public static String stripToNull(String str) {
+        if (str == null) {
+            return null;
+        }
+        str = strip(str, null);
+        return str.length() == 0 ? null : str;
+    }
+
+    /**
+     * <p>Strips whitespace from the start and end of a String  returning
+     * an empty String if <code>null</code> input.</p>
+     *
+     * <p>This is similar to {@link #trimToEmpty(String)} but removes whitespace.
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.stripToEmpty(null)     = ""
+     * StringUtils.stripToEmpty("")       = ""
+     * StringUtils.stripToEmpty("   ")    = ""
+     * StringUtils.stripToEmpty("abc")    = "abc"
+     * StringUtils.stripToEmpty("  abc")  = "abc"
+     * StringUtils.stripToEmpty("abc  ")  = "abc"
+     * StringUtils.stripToEmpty(" abc ")  = "abc"
+     * StringUtils.stripToEmpty(" ab c ") = "ab c"
+     * </pre>
+     *
+     * @param str  the String to be stripped, may be null
+     * @return the trimmed String, or an empty String if <code>null</code> input
+     * @since 2.0
+     */
+    public static String stripToEmpty(String str) {
+        return str == null ? EMPTY : strip(str, null);
+    }
+
+    /**
+     * <p>Strips any of a set of characters from the start and end of a String.
+     * This is similar to {@link String#trim()} but allows the characters
+     * to be stripped to be controlled.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * An empty string ("") input returns the empty string.</p>
+     *
+     * <p>If the stripChars String is <code>null</code>, whitespace is
+     * stripped as defined by {@link Character#isWhitespace(char)}.
+     * Alternatively use {@link #strip(String)}.</p>
+     *
+     * <pre>
+     * StringUtils.strip(null, *)          = null
+     * StringUtils.strip("", *)            = ""
+     * StringUtils.strip("abc", null)      = "abc"
+     * StringUtils.strip("  abc", null)    = "abc"
+     * StringUtils.strip("abc  ", null)    = "abc"
+     * StringUtils.strip(" abc ", null)    = "abc"
+     * StringUtils.strip("  abcyx", "xyz") = "  abc"
+     * </pre>
+     *
+     * @param str  the String to remove characters from, may be null
+     * @param stripChars  the characters to remove, null treated as whitespace
+     * @return the stripped String, <code>null</code> if null String input
+     */
+    public static String strip(String str, String stripChars) {
+        if (isEmpty(str)) {
+            return str;
+        }
+        str = stripStart(str, stripChars);
+        return stripEnd(str, stripChars);
+    }
+
+    /**
+     * <p>Strips any of a set of characters from the start of a String.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * An empty string ("") input returns the empty string.</p>
+     *
+     * <p>If the stripChars String is <code>null</code>, whitespace is
+     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.stripStart(null, *)          = null
+     * StringUtils.stripStart("", *)            = ""
+     * StringUtils.stripStart("abc", "")        = "abc"
+     * StringUtils.stripStart("abc", null)      = "abc"
+     * StringUtils.stripStart("  abc", null)    = "abc"
+     * StringUtils.stripStart("abc  ", null)    = "abc  "
+     * StringUtils.stripStart(" abc ", null)    = "abc "
+     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
+     * </pre>
+     *
+     * @param str  the String to remove characters from, may be null
+     * @param stripChars  the characters to remove, null treated as whitespace
+     * @return the stripped String, <code>null</code> if null String input
+     */
+    public static String stripStart(String str, String stripChars) {
+        int strLen;
+        if (str == null || (strLen = str.length()) == 0) {
+            return str;
+        }
+        int start = 0;
+        if (stripChars == null) {
+            while ((start != strLen) && Character.isWhitespace(str.charAt(start))) {
+                start++;
+            }
+        } else if (stripChars.length() == 0) {
+            return str;
+        } else {
+            while ((start != strLen) && (stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND)) {
+                start++;
+            }
+        }
+        return str.substring(start);
+    }
+
+    /**
+     * <p>Strips any of a set of characters from the end of a String.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * An empty string ("") input returns the empty string.</p>
+     *
+     * <p>If the stripChars String is <code>null</code>, whitespace is
+     * stripped as defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * </pre>
+     *
+     * @param str  the String to remove characters from, may be null
+     * @param stripChars  the set of characters to remove, null treated as whitespace
+     * @return the stripped String, <code>null</code> if null String input
+     */
+    public static String stripEnd(String str, String stripChars) {
+        int end;
+        if (str == null || (end = str.length()) == 0) {
+            return str;
+        }
+
+        if (stripChars == null) {
+            while ((end != 0) && Character.isWhitespace(str.charAt(end - 1))) {
+                end--;
+            }
+        } else if (stripChars.length() == 0) {
+            return str;
+        } else {
+            while ((end != 0) && (stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND)) {
+                end--;
+            }
+        }
+        return str.substring(0, end);
+    }
+
+    // StripAll
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Strips whitespace from the start and end of every String in an array.
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <p>A new array is returned each time, except for length zero.
+     * A <code>null</code> array will return <code>null</code>.
+     * An empty array will return itself.
+     * A <code>null</code> array entry will be ignored.</p>
+     *
+     * <pre>
+     * StringUtils.stripAll(null)             = null
+     * StringUtils.stripAll([])               = []
+     * StringUtils.stripAll(["abc", "  abc"]) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null])  = ["abc", null]
+     * </pre>
+     *
+     * @param strs  the array to remove whitespace from, may be null
+     * @return the stripped Strings, <code>null</code> if null array input
+     */
+    public static String[] stripAll(String[] strs) {
+        return stripAll(strs, null);
+    }
+
+    /**
+     * <p>Strips any of a set of characters from the start and end of every
+     * String in an array.</p>
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.</p>
+     *
+     * <p>A new array is returned each time, except for length zero.
+     * A <code>null</code> array will return <code>null</code>.
+     * An empty array will return itself.
+     * A <code>null</code> array entry will be ignored.
+     * A <code>null</code> stripChars will strip whitespace as defined by
+     * {@link Character#isWhitespace(char)}.</p>
+     *
+     * <pre>
+     * StringUtils.stripAll(null, *)                = null
+     * StringUtils.stripAll([], *)                  = []
+     * StringUtils.stripAll(["abc", "  abc"], null) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null], null)  = ["abc", null]
+     * StringUtils.stripAll(["abc  ", null], "yz")  = ["abc  ", null]
+     * StringUtils.stripAll(["yabcz", null], "yz")  = ["abc", null]
+     * </pre>
+     *
+     * @param strs  the array to remove characters from, may be null
+     * @param stripChars  the characters to remove, null treated as whitespace
+     * @return the stripped Strings, <code>null</code> if null array input
+     */
+    public static String[] stripAll(String[] strs, String stripChars) {
+        int strsLen;
+        if (strs == null || (strsLen = strs.length) == 0) {
+            return strs;
+        }
+        String[] newArr = new String[strsLen];
+        for (int i = 0; i < strsLen; i++) {
+            newArr[i] = strip(strs[i], stripChars);
+        }
+        return newArr;
+    }
+
+    // Equals
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Compares two Strings, returning <code>true</code> if they are equal.</p>
+     *
+     * <p><code>null</code>s are handled without exceptions. Two <code>null</code>
+     * references are considered to be equal. The comparison is case sensitive.</p>
+     *
+     * <pre>
+     * StringUtils.equals(null, null)   = true
+     * StringUtils.equals(null, "abc")  = false
+     * StringUtils.equals("abc", null)  = false
+     * StringUtils.equals("abc", "abc") = true
+     * StringUtils.equals("abc", "ABC") = false
+     * </pre>
+     *
+     * @see String#equals(Object)
+     * @param str1  the first String, may be null
+     * @param str2  the second String, may be null
+     * @return <code>true</code> if the Strings are equal, case sensitive, or
+     *  both <code>null</code>
+     */
+    public static boolean equals(String str1, String str2) {
+        return str1 == null ? str2 == null : str1.equals(str2);
+    }
+
+    /**
+     * <p>Compares two Strings, returning <code>true</code> if they are equal ignoring
+     * the case.</p>
+     *
+     * <p><code>null</code>s are handled without exceptions. Two <code>null</code>
+     * references are considered equal. Comparison is case insensitive.</p>
+     *
+     * <pre>
+     * StringUtils.equalsIgnoreCase(null, null)   = true
+     * StringUtils.equalsIgnoreCase(null, "abc")  = false
+     * StringUtils.equalsIgnoreCase("abc", null)  = false
+     * StringUtils.equalsIgnoreCase("abc", "abc") = true
+     * StringUtils.equalsIgnoreCase("abc", "ABC") = true
+     * </pre>
+     *
+     * @see String#equalsIgnoreCase(String)
+     * @param str1  the first String, may be null
+     * @param str2  the second String, may be null
+     * @return <code>true</code> if the Strings are equal, case insensitive, or
+     *  both <code>null</code>
+     */
+    public static boolean equalsIgnoreCase(String str1, String str2) {
+        return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2);
+    }
+
+    // IndexOf
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Finds the first index within a String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(int)}.</p>
+     *
+     * <p>A <code>null</code> or empty ("") String will return <code>INDEX_NOT_FOUND (-1)</code>.</p>
+     *
+     * <pre>
+     * StringUtils.indexOf(null, *)         = -1
+     * StringUtils.indexOf("", *)           = -1
+     * StringUtils.indexOf("aabaabaa", 'a') = 0
+     * StringUtils.indexOf("aabaabaa", 'b') = 2
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchChar  the character to find
+     * @return the first index of the search character,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int indexOf(String str, char searchChar) {
+        if (isEmpty(str)) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.indexOf(searchChar);
+    }
+
+    /**
+     * <p>Finds the first index within a String from a start position,
+     * handling <code>null</code>.
+     * This method uses {@link String#indexOf(int, int)}.</p>
+     *
+     * <p>A <code>null</code> or empty ("") String will return <code>(INDEX_NOT_FOUND) -1</code>.
+     * A negative start position is treated as zero.
+     * A start position greater than the string length returns <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf("", *, *)            = -1
+     * StringUtils.indexOf("aabaabaa", 'b', 0)  = 2
+     * StringUtils.indexOf("aabaabaa", 'b', 3)  = 5
+     * StringUtils.indexOf("aabaabaa", 'b', 9)  = -1
+     * StringUtils.indexOf("aabaabaa", 'b', -1) = 2
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchChar  the character to find
+     * @param startPos  the start position, negative treated as zero
+     * @return the first index of the search character,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int indexOf(String str, char searchChar, int startPos) {
+        if (isEmpty(str)) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.indexOf(searchChar, startPos);
+    }
+
+    /**
+     * <p>Finds the first index within a String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.indexOf(null, *)          = -1
+     * StringUtils.indexOf(*, null)          = -1
+     * StringUtils.indexOf("", "")           = 0
+     * StringUtils.indexOf("", *)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a")  = 0
+     * StringUtils.indexOf("aabaabaa", "b")  = 2
+     * StringUtils.indexOf("aabaabaa", "ab") = 1
+     * StringUtils.indexOf("aabaabaa", "")   = 0
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int indexOf(String str, String searchStr) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.indexOf(searchStr);
+    }
+
+    /**
+     * <p>Finds the n-th index within a String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.ordinalIndexOf(null, *, *)          = -1
+     * StringUtils.ordinalIndexOf(*, null, *)          = -1
+     * StringUtils.ordinalIndexOf("", "", *)           = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 1)   = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 2)   = 0
+     * </pre>
+     *
+     * <p>Note that 'head(String str, int n)' may be implemented as: </p>
+     *
+     * <pre>
+     *   str.substring(0, lastOrdinalIndexOf(str, "\n", n))
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param ordinal  the n-th <code>searchStr</code> to find
+     * @return the n-th index of the search String,
+     *  <code>-1</code> (<code>INDEX_NOT_FOUND</code>) if no match or <code>null</code> string input
+     * @since 2.1
+     */
+    public static int ordinalIndexOf(String str, String searchStr, int ordinal) {
+        return ordinalIndexOf(str, searchStr, ordinal, false);
+    }
+
+    /**
+     * <p>Finds the n-th index within a String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.</p>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param ordinal  the n-th <code>searchStr</code> to find
+     * @param lastIndex true if lastOrdinalIndexOf() otherwise false if ordinalIndexOf()
+     * @return the n-th index of the search String,
+     *  <code>-1</code> (<code>INDEX_NOT_FOUND</code>) if no match or <code>null</code> string input
+     */
+    // Shared code between ordinalIndexOf(String,String,int) and lastOrdinalIndexOf(String,String,int)
+    private static int ordinalIndexOf(String str, String searchStr, int ordinal, boolean lastIndex) {
+        if (str == null || searchStr == null || ordinal <= 0) {
+            return INDEX_NOT_FOUND;
+        }
+        if (searchStr.length() == 0) {
+            return lastIndex ? str.length() : 0;
+        }
+        int found = 0;
+        int index = lastIndex ? str.length() : INDEX_NOT_FOUND;
+        do {
+            if(lastIndex) {
+                index = str.lastIndexOf(searchStr, index - 1);
+            } else {
+                index = str.indexOf(searchStr, index + 1);
+            }
+            if (index < 0) {
+                return index;
+            }
+            found++;
+        } while (found < ordinal);
+        return index;
+    }
+
+    /**
+     * <p>Finds the first index within a String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(String, int)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position is treated as zero.
+     * An empty ("") search String always matches.
+     * A start position greater than the string length only matches
+     * an empty search String.</p>
+     *
+     * <pre>
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf(*, null, *)          = -1
+     * StringUtils.indexOf("", "", 0)           = 0
+     * StringUtils.indexOf("", *, 0)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.indexOf("aabaabaa", "b", 0)  = 2
+     * StringUtils.indexOf("aabaabaa", "ab", 0) = 1
+     * StringUtils.indexOf("aabaabaa", "b", 3)  = 5
+     * StringUtils.indexOf("aabaabaa", "b", 9)  = -1
+     * StringUtils.indexOf("aabaabaa", "b", -1) = 2
+     * StringUtils.indexOf("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOf("abc", "", 9)        = 3
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param startPos  the start position, negative treated as zero
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int indexOf(String str, String searchStr, int startPos) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        // JDK1.2/JDK1.3 have a bug, when startPos > str.length for "", hence
+        if (searchStr.length() == 0 && startPos >= str.length()) {
+            return str.length();
+        }
+        return str.indexOf(searchStr, startPos);
+    }
+
+    /**
+     * <p>Case in-sensitive find of the first index within a String.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position is treated as zero.
+     * An empty ("") search String always matches.
+     * A start position greater than the string length only matches
+     * an empty search String.</p>
+     *
+     * <pre>
+     * StringUtils.indexOfIgnoreCase(null, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null)          = -1
+     * StringUtils.indexOfIgnoreCase("", "")           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "a")  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "b")  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "ab") = 1
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.5
+     */
+    public static int indexOfIgnoreCase(String str, String searchStr) {
+        return indexOfIgnoreCase(str, searchStr, 0);
+    }
+
+    /**
+     * <p>Case in-sensitive find of the first index within a String
+     * from the specified position.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position is treated as zero.
+     * An empty ("") search String always matches.
+     * A start position greater than the string length only matches
+     * an empty search String.</p>
+     *
+     * <pre>
+     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = 3
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param startPos  the start position, negative treated as zero
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.5
+     */
+    public static int indexOfIgnoreCase(String str, String searchStr, int startPos) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        if (startPos < 0) {
+            startPos = 0;
+        }
+        int endLimit = (str.length() - searchStr.length()) + 1;
+        if (startPos > endLimit) {
+            return INDEX_NOT_FOUND;
+        }
+        if (searchStr.length() == 0) {
+            return startPos;
+        }
+        for (int i = startPos; i < endLimit; i++) {
+            if (str.regionMatches(true, i, searchStr, 0, searchStr.length())) {
+                return i;
+            }
+        }
+        return INDEX_NOT_FOUND;
+    }
+
+    // LastIndexOf
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Finds the last index within a String, handling <code>null</code>.
+     * This method uses {@link String#lastIndexOf(int)}.</p>
+     *
+     * <p>A <code>null</code> or empty ("") String will return <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOf(null, *)         = -1
+     * StringUtils.lastIndexOf("", *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a') = 7
+     * StringUtils.lastIndexOf("aabaabaa", 'b') = 5
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchChar  the character to find
+     * @return the last index of the search character,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int lastIndexOf(String str, char searchChar) {
+        if (isEmpty(str)) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.lastIndexOf(searchChar);
+    }
+
+    /**
+     * <p>Finds the last index within a String from a start position,
+     * handling <code>null</code>.
+     * This method uses {@link String#lastIndexOf(int, int)}.</p>
+     *
+     * <p>A <code>null</code> or empty ("") String will return <code>-1</code>.
+     * A negative start position returns <code>-1</code>.
+     * A start position greater than the string length searches the whole string.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf("", *,  *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 4)  = 2
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 0)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a', 0)  = 0
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchChar  the character to find
+     * @param startPos  the start position
+     * @return the last index of the search character,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int lastIndexOf(String str, char searchChar, int startPos) {
+        if (isEmpty(str)) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.lastIndexOf(searchChar, startPos);
+    }
+
+    /**
+     * <p>Finds the last index within a String, handling <code>null</code>.
+     * This method uses {@link String#lastIndexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOf(null, *)          = -1
+     * StringUtils.lastIndexOf(*, null)          = -1
+     * StringUtils.lastIndexOf("", "")           = 0
+     * StringUtils.lastIndexOf("aabaabaa", "a")  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b")  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab") = 4
+     * StringUtils.lastIndexOf("aabaabaa", "")   = 8
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return the last index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int lastIndexOf(String str, String searchStr) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.lastIndexOf(searchStr);
+    }
+
+    /**
+     * <p>Finds the n-th last index within a String, handling <code>null</code>.
+     * This method uses {@link String#lastIndexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.</p>
+     *
+     * <pre>
+     * StringUtils.lastOrdinalIndexOf(null, *, *)          = -1
+     * StringUtils.lastOrdinalIndexOf(*, null, *)          = -1
+     * StringUtils.lastOrdinalIndexOf("", "", *)           = 0
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 1)  = 7
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 2)  = 6
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 1)  = 5
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 2)  = 2
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 1) = 4
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 2) = 1
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 1)   = 8
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 2)   = 8
+     * </pre>
+     *
+     * <p>Note that 'tail(String str, int n)' may be implemented as: </p>
+     *
+     * <pre>
+     *   str.substring(lastOrdinalIndexOf(str, "\n", n) + 1)
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param ordinal  the n-th last <code>searchStr</code> to find
+     * @return the n-th last index of the search String,
+     *  <code>-1</code> (<code>INDEX_NOT_FOUND</code>) if no match or <code>null</code> string input
+     * @since 2.5
+     */
+    public static int lastOrdinalIndexOf(String str, String searchStr, int ordinal) {
+        return ordinalIndexOf(str, searchStr, ordinal, true);
+    }
+
+    /**
+     * <p>Finds the first index within a String, handling <code>null</code>.
+     * This method uses {@link String#lastIndexOf(String, int)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position returns <code>-1</code>.
+     * An empty ("") search String always matches unless the start position is negative.
+     * A start position greater than the string length searches the whole string.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf(*, null, *)          = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 8)  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b", 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab", 8) = 4
+     * StringUtils.lastIndexOf("aabaabaa", "b", 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "b", -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.lastIndexOf("aabaabaa", "b", 0)  = -1
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param startPos  the start position, negative treated as zero
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.0
+     */
+    public static int lastIndexOf(String str, String searchStr, int startPos) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        return str.lastIndexOf(searchStr, startPos);
+    }
+
+    /**
+     * <p>Case in-sensitive find of the last index within a String.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position returns <code>-1</code>.
+     * An empty ("") search String always matches unless the start position is negative.
+     * A start position greater than the string length searches the whole string.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOfIgnoreCase(null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A")  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B")  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB") = 4
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.5
+     */
+    public static int lastIndexOfIgnoreCase(String str, String searchStr) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        return lastIndexOfIgnoreCase(str, searchStr, str.length());
+    }
+
+    /**
+     * <p>Case in-sensitive find of the last index within a String
+     * from the specified position.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A negative start position returns <code>-1</code>.
+     * An empty ("") search String always matches unless the start position is negative.
+     * A start position greater than the string length searches the whole string.</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8)  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", -1) = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 0)  = -1
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @param startPos  the start position
+     * @return the first index of the search String,
+     *  -1 if no match or <code>null</code> string input
+     * @since 2.5
+     */
+    public static int lastIndexOfIgnoreCase(String str, String searchStr, int startPos) {
+        if (str == null || searchStr == null) {
+            return INDEX_NOT_FOUND;
+        }
+        if (startPos > (str.length() - searchStr.length())) {
+            startPos = str.length() - searchStr.length();
+        }
+        if (startPos < 0) {
+            return INDEX_NOT_FOUND;
+        }
+        if (searchStr.length() == 0) {
+            return startPos;
+        }
+
+        for (int i = startPos; i >= 0; i--) {
+            if (str.regionMatches(true, i, searchStr, 0, searchStr.length())) {
+                return i;
+            }
+        }
+        return INDEX_NOT_FOUND;
+    }
+
+    // Contains
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Checks if String contains a search character, handling <code>null</code>.
+     * This method uses {@link String#indexOf(int)}.</p>
+     *
+     * <p>A <code>null</code> or empty ("") String will return <code>false</code>.</p>
+     *
+     * <pre>
+     * StringUtils.contains(null, *)    = false
+     * StringUtils.contains("", *)      = false
+     * StringUtils.contains("abc", 'a') = true
+     * StringUtils.contains("abc", 'z') = false
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchChar  the character to find
+     * @return true if the String contains the search character,
+     *  false if not or <code>null</code> string input
+     * @since 2.0
+     */
+    public static boolean contains(String str, char searchChar) {
+        if (isEmpty(str)) {
+            return false;
+        }
+        return str.indexOf(searchChar) >= 0;
+    }
+
+    /**
+     * <p>Checks if String contains a search String, handling <code>null</code>.
+     * This method uses {@link String#indexOf(String)}.</p>
+     *
+     * <p>A <code>null</code> String will return <code>false</code>.</p>
+     *
+     * <pre>
+     * StringUtils.contains(null, *)     = false
+     * StringUtils.contains(*, null)     = false
+     * StringUtils.contains("", "")      = true
+     * StringUtils.contains("abc", "")   = true
+     * StringUtils.contains("abc", "a")  = true
+     * StringUtils.contains("abc", "z")  = false
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return true if the String contains the search String,
+     *  false if not or <code>null</code> string input
+     * @since 2.0
+     */
+    public static boolean contains(String str, String searchStr) {
+        if (str == null || searchStr == null) {
+            return false;
+        }
+        return str.indexOf(searchStr) >= 0;
+    }
+
+    /**
+     * <p>Checks if String contains a search String irrespective of case,
+     * handling <code>null</code>. Case-insensitivity is defined as by
+     * {@link String#equalsIgnoreCase(String)}.
+     *
+     * <p>A <code>null</code> String will return <code>false</code>.</p>
+     *
+     * <pre>
+     * StringUtils.contains(null, *) = false
+     * StringUtils.contains(*, null) = false
+     * StringUtils.contains("", "") = true
+     * StringUtils.contains("abc", "") = true
+     * StringUtils.contains("abc", "a") = true
+     * StringUtils.contains("abc", "z") = false
+     * StringUtils.contains("abc", "A") = true
+     * StringUtils.contains("abc", "Z") = false
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStr  the String to find, may be null
+     * @return true if the String contains the search String irrespective of
+     * case or false if not or <code>null</code> string input
+     */
+    public static boolean containsIgnoreCase(String str, String searchStr) {
+        if (str == null || searchStr == null) {
+            return false;
+        }
+        int len = searchStr.length();
+        int max = str.length() - len;
+        for (int i = 0; i <= max; i++) {
+            if (str.regionMatches(true, i, searchStr, 0, len)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    // IndexOfAny strings
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Find the first index of any of a set of potential substrings.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A <code>null</code> or zero length search array will return <code>-1</code>.
+     * A <code>null</code> search array entry will be ignored, but a search
+     * array containing "" will return <code>0</code> if <code>str</code> is not
+     * null. This method uses {@link String#indexOf(String)}.</p>
+     *
+     * <pre>
+     * StringUtils.indexOfAny(null, *)                     = -1
+     * StringUtils.indexOfAny(*, null)                     = -1
+     * StringUtils.indexOfAny(*, [])                       = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["ab","cd"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["cd","ab"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["mn","op"])   = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["zab","aby"]) = 1
+     * StringUtils.indexOfAny("zzabyycdxx", [""])          = 0
+     * StringUtils.indexOfAny("", [""])                    = 0
+     * StringUtils.indexOfAny("", ["a"])                   = -1
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStrs  the Strings to search for, may be null
+     * @return the first index of any of the searchStrs in str, -1 if no match
+     */
+    public static int indexOfAny(String str, String[] searchStrs) {
+        if ((str == null) || (searchStrs == null)) {
+            return INDEX_NOT_FOUND;
+        }
+        int sz = searchStrs.length;
+
+        // String's can't have a MAX_VALUEth index.
+        int ret = Integer.MAX_VALUE;
+
+        int tmp = 0;
+        for (int i = 0; i < sz; i++) {
+            String search = searchStrs[i];
+            if (search == null) {
+                continue;
+            }
+            tmp = str.indexOf(search);
+            if (tmp == INDEX_NOT_FOUND) {
+                continue;
+            }
+
+            if (tmp < ret) {
+                ret = tmp;
+            }
+        }
+
+        return (ret == Integer.MAX_VALUE) ? INDEX_NOT_FOUND : ret;
+    }
+
+    /**
+     * <p>Find the latest index of any of a set of potential substrings.</p>
+     *
+     * <p>A <code>null</code> String will return <code>-1</code>.
+     * A <code>null</code> search array will return <code>-1</code>.
+     * A <code>null</code> or zero length search array entry will be ignored,
+     * but a search array containing "" will return the length of <code>str</code>
+     * if <code>str</code> is not null. This method uses {@link String#indexOf(String)}</p>
+     *
+     * <pre>
+     * StringUtils.lastIndexOfAny(null, *)                   = -1
+     * StringUtils.lastIndexOfAny(*, null)                   = -1
+     * StringUtils.lastIndexOfAny(*, [])                     = -1
+     * StringUtils.lastIndexOfAny(*, [null])                 = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["ab","cd"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["cd","ab"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn",""])   = 10
+     * </pre>
+     *
+     * @param str  the String to check, may be null
+     * @param searchStrs  the Strings to search for, may be null
+     * @return the last index of any of the Strings, -1 if no match
+     */
+    public static int lastIndexOfAny(String str, String[] searchStrs) {
+        if ((str == null) || (searchStrs == null)) {
+            return INDEX_NOT_FOUND;
+        }
+        int sz = searchStrs.length;
+        int ret = INDEX_NOT_FOUND;
+        int tmp = 0;
+        for (int i = 0; i < sz; i++) {
+            String search = searchStrs[i];
+            if (search == null) {
+                continue;
+            }
+            tmp = str.lastIndexOf(search);
+            if (tmp > ret) {
+                ret = tmp;
+            }
+        }
+        return ret;
+    }
+
+    // Substring
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Gets a substring from the specified String avoiding exceptions.</p>
+     *
+     * <p>A negative start position can be used to start <code>n</code>
+     * characters from the end of the String.</p>
+     *
+     * <p>A <code>null</code> String will return <code>null</code>.
+     * An empty ("") String will return "".</p>
+     *
+     * <pre>
+     * StringUtils.substring(null, *)   = null
+     * StringUtils.substring("", *)     = ""
+     * StringUtils.substring("abc", 0)  = "abc"
+     * StringUtils.substring("abc", 2)  = "c"
+     * StringUtils.substring("abc", 4)  = ""
+     * StringUtils.substring("abc", -2) = "bc"
+     * StringUtils.substring("abc", -4) = "abc"
+     * </pre>
+     *
+     * @param str  the String to get the substring from, may be null
+     * @param start  the position to start from, negative means
+     *  count back from the end of the String by this many characters
+     * @return substring from start position, <code>null</code> if null String input
+     */
+    public static String substring(String str, int start) {
+        if (str == null) {
+            return null;
+        }
+
+        // handle negatives, which means last n characters
+        if (start < 0) {
+            start = str.length() + start; // remember start is negative
+        }
+
+        if (start < 0) {
+            start = 0;
+        }
+        if (start > str.length()) {
+            return EMPTY;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * <p>Gets a substring from the specified String avoiding exceptions.</p>
+     *
+     * <p>A negative start position can be used to start/end <code>n</code>
+     * characters from the end of the String.</p>
+     *
+     * <p>The returned substring starts with the character in the <code>start</code>
+     * position and ends before the <code>end</code> position. All position counting is
+     * zero-based -- i.e., to start at the beginning of the string use
+     * <code>start = 0</code>. Negative start and end positions can be used to
+     * specify offsets relative to the end of the String.</p>
+     *
+     * <p>If <code>start</code> is not strictly to the left of <code>end</code>, ""
+     * is returned.</p>
+     *
+     * <pre>
+     * StringUtils.substring(null, *, *)    = null
+     * StringUtils.substring("", * ,  *)    = "";
+     * StringUtils.substring("abc", 0, 2)   = "ab"
+     * StringUtils.substring("abc", 2, 0)   = ""
+     * StringUtils.substring("abc", 2, 4)   = "c"
+     * StringUtils.substring("abc", 4, 6)   = ""
+     * StringUtils.substring("abc", 2, 2)   = ""
+     * StringUtils.substring("abc", -2, -1) = "b"
+     * StringUtils.substring("abc", -4, 2)  = "ab"
+     * </pre>
+     *
+     * @param str  the String to get the substring from, may be null
+     * @param start  the position to start from, negative means
+     *  count back from the end of the String by this many characters
+     * @param end  the position to end at (exclusive), negative means
+     *  count back from the end of the String by this many characters
+     * @return substring from start position to end positon,
+     *  <code>null</code> if null String input
+     */
+    public static String substring(String str, int start, int end) {
+        if (str == null) {
+            return null;
+        }
+
+        // handle negatives
+        if (end < 0) {
+            end = str.length() + end; // remember end is negative
+        }
+        if (start < 0) {
+            start = str.length() + start; // remember start is negative
+        }
+
+        // check length next
+        if (end > str.length()) {
+            end = str.length();
+        }
+
+        // if start is greater than end, return ""
+        if (start > end) {
+            return EMPTY;
+        }
+
+        if (start < 0) {
+            start = 0;
+        }
+        if (end < 0) {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+    // Left/Right/Mid
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Gets the leftmost <code>len</code> characters of a String.</p>
+     *
+     * <p>If <code>len</code> characters are not available, or the
+     * String is <code>null</code>, the String will be returned without
+     * an exception. An empty String is returned if len is negative.</p>
+     *
+     * <pre>
+     * StringUtils.left(null, *)    = null
+     * StringUtils.left(*, -ve)     = ""
+     * StringUtils.left("", *)      = ""
+     * StringUtils.left("abc", 0)   = ""
+     * StringUtils.left("abc", 2)   = "ab"
+     * StringUtils.left("abc", 4)   = "abc"
+     * </pre>
+     *
+     * @param str  the String to get the leftmost characters from, may be null
+     * @param len  the length of the required String
+     * @return the leftmost characters, <code>null</code> if null String input
+     */
+    public static String left(String str, int len) {
+        if (str == null) {
+            return null;
+        }
+        if (len < 0) {
+            return EMPTY;
+        }
+        if (str.length() <= len) {
+            return str;
+        }
+        return str.substring(0, len);
+    }
+
+    /**
+     * <p>Gets the rightmost <code>len</code> characters of a String.</p>
+     *
+     * <p>If <code>len</code> characters are not available, or the String
+     * is <code>null</code>, the String will be returned without an
+     * an exception. An empty String is returned if len is negative.</p>
+     *
+     * <pre>
+     * StringUtils.right(null, *)    = null
+     * StringUtils.right(*, -ve)     = ""
+     * StringUtils.right("", *)      = ""
+     * StringUtils.right("abc", 0)   = ""
+     * StringUtils.right("abc", 2)   = "bc"
+     * StringUtils.right("abc", 4)   = "abc"
+     * </pre>
+     *
+     * @param str  the String to get the rightmost characters from, may be null
+     * @param len  the length of the required String
+     * @return the rightmost characters, <code>null</code> if null String input
+     */
+    public static String right(String str, int len) {
+        if (str == null) {
+            return null;
+        }
+        if (len < 0) {
+            return EMPTY;
+        }
+        if (str.length() <= len) {
+            return str;
+        }
+        return str.substring(str.length() - len);
+    }
+
+    /**
+     * <p>Gets <code>len</code> characters from the middle of a String.</p>
+     *
+     * <p>If <code>len</code> characters are not available, the remainder
+     * of the String will be returned without an exception. If the
+     * String is <code>null</code>, <code>null</code> will be returned.
+     * An empty String is returned if len is negative or exceeds the
+     * length of <code>str</code>.</p>
+     *
+     * <pre>
+     * StringUtils.mid(null, *, *)    = null
+     * StringUtils.mid(*, *, -ve)     = ""
+     * StringUtils.mid("", 0, *)      = ""
+     * StringUtils.mid("abc", 0, 2)   = "ab"
+     * StringUtils.mid("abc", 0, 4)   = "abc"
+     * StringUtils.mid("abc", 2, 4)   = "c"
+     * StringUtils.mid("abc", 4, 2)   = ""
+     * StringUtils.mid("abc", -2, 2)  = "ab"
+     * </pre>
+     *
+     * @param str  the String to get the characters from, may be null
+     * @param pos  the position to start from, negative treated as zero
+     * @param len  the length of the required String
+     * @return the middle characters, <code>null</code> if null String input
+     */
+    public static String mid(String str, int pos, int len) {
+        if (str == null) {
+            return null;
+        }
+        if (len < 0 || pos > str.length()) {
+            return EMPTY;
+        }
+        if (pos < 0) {
+            pos = 0;
+        }
+        if (str.length() <= (pos + len)) {
+            return str.substring(pos);
+        }
+        return str.substring(pos, pos + len);
+    }
+
+    // SubStringAfter/SubStringBefore
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Gets the substring before the first occurrence of a separator.
+     * The separator is not returned.</p>
+     *
+     * <p>A <code>null</code> string input will return <code>null</code>.
+     * An empty ("") string input will return the empty string.
+     * A <code>null</code> separator will return the input string.</p>
+     *
+     * <p>If nothing is found, the string input is returned.</p>
+     *
+     * <pre>
+     * StringUtils.substringBefore(null, *)      = null
+     * StringUtils.substringBefore("", *)        = ""
+     * StringUtils.substringBefore("abc", "a")   = ""
+     * StringUtils.substringBefore("abcba", "b") = "a"
+     * StringUtils.substringBefore("abc", "c")   = "ab"
+     * StringUtils.substringBefore("abc", "d")   = "abc"
+     * StringUtils.substringBefore("abc", "")    = ""
+     * StringUtils.substringBefore("abc", null)  = "abc"
+     * </pre>
+     *
+     * @param str  the String to get a substring from, may be null
+     * @param separator  the String to search for, may be null
+     * @return the substring before the first occurrence of the separator,
+     *  <code>null</code> if null String input
+     * @since 2.0
+     */
+    public static String substringBefore(String str, String separator) {
+        if (isEmpty(str) || separator == null) {
+            return str;
+        }
+        if (separator.length() == 0) {
+            return EMPTY;
+        }
+        int pos = str.indexOf(separator);
+        if (pos == INDEX_NOT_FOUND) {
+            return str;
+        }
+        return str.substring(0, pos);
+    }
+
+    /**
+     * <p>Gets the substring after the first occurrence of a separator.
+     * The separator is not returned.</p>
+     *
+     * <p>A <code>null</code> string input will return <code>null</code>.
+     * An empty ("") string input will return the empty string.
+     * A <code>null</code> separator will return the empty string if the
+     * input string is not <code>null</code>.</p>
+     *
+     * <p>If nothing is found, the empty string is returned.</p>
+     *
+     * <pre>
+     * StringUtils.substringAfter(null, *)      = null
+     * StringUtils.substringAfter("", *)        = ""
+     * StringUtils.substringAfter(*, null)      = ""
+     * StringUtils.substringAfter("abc", "a")   = "bc"
+     * StringUtils.substringAfter("abcba", "b") = "cba"
+     * StringUtils.substringAfter("abc", "c")   = ""
+     * StringUtils.substringAfter("abc", "d")   = ""
+     * StringUtils.substringAfter("abc", "")    = "abc"
+     * </pre>
+     *
+     * @param str  the String to get a substring from, may be null
+     * @param separator  the String to search for, may be null
+     * @return the substring after the first occurrence of the separator,
+     *  <code>null</code> if null String input
+     * @since 2.0
+     */
+    public static String substringAfter(String str, String separator) {
+        if (isEmpty(str)) {
+            return str;
+        }
+        if (separator == null) {
+            return EMPTY;
+        }
+        int pos = str.indexOf(separator);
+        if (pos == INDEX_NOT_FOUND) {
+            return EMPTY;
+        }
+        return str.substring(pos + separator.length());
+    }
+
+    /**
+     * <p>Gets the substring before the last occurrence of a separator.
+     * The separator is not returned.</p>
+     *
+     * <p>A <code>null</code> string input will return <code>null</code>.
+     * An empty ("") string input will return the empty string.
+     * An empty or <code>null</code> separator will return the input string.</p>
+     *
+     * <p>If nothing is found, the string input is returned.</p>
+     *
+     * <pre>
+     * StringUtils.substringBeforeLast(null, *)      = null
+     * StringUtils.substringBeforeLast("", *)        = ""
+     * StringUtils.substringBeforeLast("abcba", "b") = "abc"
+     * StringUtils.substringBeforeLast("abc", "c")   = "ab"
+     * StringUtils.substringBeforeLast("a", "a")     = ""
+     * StringUtils.substringBeforeLast("a", "z")     = "a"
+     * StringUtils.substringBeforeLast("a", null)    = "a"
+     * StringUtils.substringBeforeLast("a", "")      = "a"
+     * </pre>
+     *
+     * @param str  the String to get a substring from, may be null
+     * @param separator  the String to search for, may be null
+     * @return the substring before the last occurrence of the separator,
+     *  <code>null</code> if null String input
+     * @since 2.0
+     */
+    public static String substringBeforeLast(String str, String separator) {
+        if (isEmpty(str) || isEmpty(separator)) {
+            return str;
+        }
+        int pos = str.lastIndexOf(separator);
+        if (pos == INDEX_NOT_FOUND) {
+            return str;
+        }
+        return str.substring(0, pos);
+    }
+
+    /**
+     * <p>Gets the substring after the last occurrence of a separator.
+     * The separator is not returned.</p>
+     *
+     * <p>A <code>null</code> string input will return <code>null</code>.
+     * An empty ("") string input will return the empty string.
+     * An empty or <code>null</code> separator will return the empty string if
+     * the input string is not <code>null</code>.</p>
+     *
+     * <p>If nothing is found, the empty string is returned.</p>
+     *
+     * <pre>
+     * StringUtils.substringAfterLast(null, *)      = null
+     * StringUtils.substringAfterLast("", *)        = ""
+     * StringUtils.substringAfterLast(*, "")        = ""
+     * StringUtils.substringAfterLast(*, null)      = ""
+     * StringUtils.substringAfterLast("abc", "a")   = "bc"
+     * StringUtils.substringAfterLast("abcba", "b") = "a"
+     * StringUtils.substringAfterLast("abc", "c")   = ""
+     * StringUtils.substringAfterLast("a", "a")     = ""
+     * StringUtils.substringAfterLast("a", "z")     = ""
+     * </pre>
+     *
+     * @param str  the String to get a substring from, may be null
+     * @param separator  the String to search for, may be null
+     * @return the substring after the last occurrence of the separator,
+     *  <code>null</code> if null String input
+     * @since 2.0
+     */
+    public static String substringAfterLast(String str, String separator) {
+        if (isEmpty(str)) {
+            return str;
+        }
+        if (isEmpty(separator)) {
+            return EMPTY;
+        }
+        int pos = str.lastIndexOf(separator);
+        if (pos == INDEX_NOT_FOUND || pos == (str.length() - separator.length())) {
+            return EMPTY;
+        }
+        return str.substring(pos + separator.length());
+    }
+
+    // Substring between
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Gets the String that is nested in between two instances of the
+     * same String.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * A <code>null</code> tag returns <code>null</code>.</p>
+     *
+     * <pre>
+     * StringUtils.substringBetween(null, *)            = null
+     * StringUtils.substringBetween("", "")             = ""
+     * StringUtils.substringBetween("", "tag")          = null
+     * StringUtils.substringBetween("tagabctag", null)  = null
+     * StringUtils.substringBetween("tagabctag", "")    = ""
+     * StringUtils.substringBetween("tagabctag", "tag") = "abc"
+     * </pre>
+     *
+     * @param str  the String containing the substring, may be null
+     * @param tag  the String before and after the substring, may be null
+     * @return the substring, <code>null</code> if no match
+     * @since 2.0
+     */
+    public static String substringBetween(String str, String tag) {
+        return substringBetween(str, tag, tag);
+    }
+
+    /**
+     * <p>Gets the String that is nested in between two Strings.
+     * Only the first match is returned.</p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * A <code>null</code> open/close returns <code>null</code> (no match).
+     * An empty ("") open and close returns an empty string.</p>
+     *
+     * <pre>
+     * StringUtils.substringBetween("wx[b]yz", "[", "]") = "b"
+     * StringUtils.substringBetween(null, *, *)          = null
+     * StringUtils.substringBetween(*, null, *)          = null
+     * StringUtils.substringBetween(*, *, null)          = null
+     * StringUtils.substringBetween("", "", "")          = ""
+     * StringUtils.substringBetween("", "", "]")         = null
+     * StringUtils.substringBetween("", "[", "]")        = null
+     * StringUtils.substringBetween("yabcz", "", "")     = ""
+     * StringUtils.substringBetween("yabcz", "y", "z")   = "abc"
+     * StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"
+     * </pre>
+     *
+     * @param str  the String containing the substring, may be null
+     * @param open  the String before the substring, may be null
+     * @param close  the String after the substring, may be null
+     * @return the substring, <code>null</code> if no match
+     * @since 2.0
+     */
+    public static String substringBetween(String str, String open, String close) {
+        if (str == null || open == null || close == null) {
+            return null;
+        }
+        int start = str.indexOf(open);
+        if (start != INDEX_NOT_FOUND) {
+            int end = str.indexOf(close, start + open.length());
+            if (end != INDEX_NOT_FOUND) {
+                return str.substring(start + open.length(), end);
+            }
+        }
+        return null;
+    }
+
+}

+ 3 - 0
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

+ 1 - 0
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

BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png


BIN
yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf


+ 55 - 0
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和嵌入式操作系统中使用

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff