feat: 将aj-captcha换为源码导入
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 { | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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"; | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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 + | ||||
|                 '}'; | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| 	}; | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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"; | ||||
|     } | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,318 @@ | ||||
| /* | ||||
|  *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.Const; | ||||
| import com.anji.captcha.model.common.RepCodeEnum; | ||||
| import com.anji.captcha.model.common.ResponseModel; | ||||
| import com.anji.captcha.model.vo.CaptchaVO; | ||||
| import com.anji.captcha.model.vo.PointVO; | ||||
| import com.anji.captcha.util.*; | ||||
|  | ||||
| import java.awt.*; | ||||
| import java.awt.geom.AffineTransform; | ||||
| import java.awt.image.BufferedImage; | ||||
| import java.util.List; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * 点选文字验证码 | ||||
|  * <p> | ||||
|  * Created by raodeming on 2019/12/25. | ||||
|  */ | ||||
| public class ClickWordCaptchaServiceImpl extends AbstractCaptchaService { | ||||
|  | ||||
|     public static String HAN_ZI = "\u7684\u4e00\u4e86\u662f\u6211\u4e0d\u5728\u4eba\u4eec\u6709\u6765\u4ed6\u8fd9\u4e0a\u7740\u4e2a\u5730\u5230\u5927\u91cc\u8bf4\u5c31\u53bb\u5b50\u5f97\u4e5f\u548c\u90a3\u8981\u4e0b\u770b\u5929\u65f6\u8fc7\u51fa\u5c0f\u4e48\u8d77\u4f60\u90fd\u628a\u597d\u8fd8\u591a\u6ca1\u4e3a\u53c8\u53ef\u5bb6\u5b66\u53ea\u4ee5\u4e3b\u4f1a\u6837\u5e74\u60f3\u751f\u540c\u8001\u4e2d\u5341\u4ece\u81ea\u9762\u524d\u5934\u9053\u5b83\u540e\u7136\u8d70\u5f88\u50cf\u89c1\u4e24\u7528\u5979\u56fd\u52a8\u8fdb\u6210\u56de\u4ec0\u8fb9\u4f5c\u5bf9\u5f00\u800c\u5df1\u4e9b\u73b0\u5c71\u6c11\u5019\u7ecf\u53d1\u5de5\u5411\u4e8b\u547d\u7ed9\u957f\u6c34\u51e0\u4e49\u4e09\u58f0\u4e8e\u9ad8\u624b\u77e5\u7406\u773c\u5fd7\u70b9\u5fc3\u6218\u4e8c\u95ee\u4f46\u8eab\u65b9\u5b9e\u5403\u505a\u53eb\u5f53\u4f4f\u542c\u9769\u6253\u5462\u771f\u5168\u624d\u56db\u5df2\u6240\u654c\u4e4b\u6700\u5149\u4ea7\u60c5\u8def\u5206\u603b\u6761\u767d\u8bdd\u4e1c\u5e2d\u6b21\u4eb2\u5982\u88ab\u82b1\u53e3\u653e\u513f\u5e38\u6c14\u4e94\u7b2c\u4f7f\u5199\u519b\u5427\u6587\u8fd0\u518d\u679c\u600e\u5b9a\u8bb8\u5feb\u660e\u884c\u56e0\u522b\u98de\u5916\u6811\u7269\u6d3b\u90e8\u95e8\u65e0\u5f80\u8239\u671b\u65b0\u5e26\u961f\u5148\u529b\u5b8c\u5374\u7ad9\u4ee3\u5458\u673a\u66f4\u4e5d\u60a8\u6bcf\u98ce\u7ea7\u8ddf\u7b11\u554a\u5b69\u4e07\u5c11\u76f4\u610f\u591c\u6bd4\u9636\u8fde\u8f66\u91cd\u4fbf\u6597\u9a6c\u54ea\u5316\u592a\u6307\u53d8\u793e\u4f3c\u58eb\u8005\u5e72\u77f3\u6ee1\u65e5\u51b3\u767e\u539f\u62ff\u7fa4\u7a76\u5404\u516d\u672c\u601d\u89e3\u7acb\u6cb3\u6751\u516b\u96be\u65e9\u8bba\u5417\u6839\u5171\u8ba9\u76f8\u7814\u4eca\u5176\u4e66\u5750\u63a5\u5e94\u5173\u4fe1\u89c9\u6b65\u53cd\u5904\u8bb0\u5c06\u5343\u627e\u4e89\u9886\u6216\u5e08\u7ed3\u5757\u8dd1\u8c01\u8349\u8d8a\u5b57\u52a0\u811a\u7d27\u7231\u7b49\u4e60\u9635\u6015\u6708\u9752\u534a\u706b\u6cd5\u9898\u5efa\u8d76\u4f4d\u5531\u6d77\u4e03\u5973\u4efb\u4ef6\u611f\u51c6\u5f20\u56e2\u5c4b\u79bb\u8272\u8138\u7247\u79d1\u5012\u775b\u5229\u4e16\u521a\u4e14\u7531\u9001\u5207\u661f\u5bfc\u665a\u8868\u591f\u6574\u8ba4\u54cd\u96ea\u6d41\u672a\u573a\u8be5\u5e76\u5e95\u6df1\u523b\u5e73\u4f1f\u5fd9\u63d0\u786e\u8fd1\u4eae\u8f7b\u8bb2\u519c\u53e4\u9ed1\u544a\u754c\u62c9\u540d\u5440\u571f\u6e05\u9633\u7167\u529e\u53f2\u6539\u5386\u8f6c\u753b\u9020\u5634\u6b64\u6cbb\u5317\u5fc5\u670d\u96e8\u7a7f\u5185\u8bc6\u9a8c\u4f20\u4e1a\u83dc\u722c\u7761\u5174\u5f62\u91cf\u54b1\u89c2\u82e6\u4f53\u4f17\u901a\u51b2\u5408\u7834\u53cb\u5ea6\u672f\u996d\u516c\u65c1\u623f\u6781\u5357\u67aa\u8bfb\u6c99\u5c81\u7ebf\u91ce\u575a\u7a7a\u6536\u7b97\u81f3\u653f\u57ce\u52b3\u843d\u94b1\u7279\u56f4\u5f1f\u80dc\u6559\u70ed\u5c55\u5305\u6b4c\u7c7b\u6e10\u5f3a\u6570\u4e61\u547c\u6027\u97f3\u7b54\u54e5\u9645\u65e7\u795e\u5ea7\u7ae0\u5e2e\u5566\u53d7\u7cfb\u4ee4\u8df3\u975e\u4f55\u725b\u53d6\u5165\u5cb8\u6562\u6389\u5ffd\u79cd\u88c5\u9876\u6025\u6797\u505c\u606f\u53e5\u533a\u8863\u822c\u62a5\u53f6\u538b\u6162\u53d4\u80cc\u7ec6"; | ||||
|  | ||||
|     protected static String clickWordFontStr = "NotoSerif-Light.ttf"; | ||||
|  | ||||
|     protected Font clickWordFont;//点选文字字体 | ||||
|  | ||||
|     @Override | ||||
|     public String captchaType() { | ||||
|         return CaptchaTypeEnum.CLICKWORD.getCodeValue(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void init(Properties config) { | ||||
|         super.init(config); | ||||
|         clickWordFontStr = config.getProperty(Const.CAPTCHA_FONT_TYPE, "SourceHanSansCN-Normal.otf"); | ||||
|         try { | ||||
|             int size = Integer.valueOf(config.getProperty(Const.CAPTCHA_FONT_SIZE,HAN_ZI_SIZE+"")); | ||||
|  | ||||
|             if (clickWordFontStr.toLowerCase().endsWith(".ttf") | ||||
|                     || clickWordFontStr.toLowerCase().endsWith(".ttc") | ||||
|                     || clickWordFontStr.toLowerCase().endsWith(".otf")) { | ||||
|                 this.clickWordFont = Font.createFont(Font.TRUETYPE_FONT, | ||||
|                         getClass().getResourceAsStream("/fonts/" + clickWordFontStr)) | ||||
|                         .deriveFont(Font.BOLD, size); | ||||
|             } else { | ||||
|                 int style = Integer.valueOf(config.getProperty(Const.CAPTCHA_FONT_STYLE,Font.BOLD+"")); | ||||
|                 this.clickWordFont = new Font(clickWordFontStr, style, size); | ||||
|             } | ||||
|         } catch (Exception ex) { | ||||
|             logger.error("load font error:{}", ex); | ||||
|         } | ||||
|         this.wordTotalCount = Integer.valueOf(config.getProperty(Const.CAPTCHA_WORD_COUNT,"4")); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void destroy(Properties config) { | ||||
|         logger.info("start-clear-history-data-", captchaType()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ResponseModel get(CaptchaVO captchaVO) { | ||||
|         ResponseModel r = super.get(captchaVO); | ||||
|         if (!validatedReq(r)) { | ||||
|             return r; | ||||
|         } | ||||
|         BufferedImage bufferedImage = ImageUtils.getPicClick(); | ||||
|         if (null == bufferedImage) { | ||||
|             logger.error("滑动底图未初始化成功,请检查路径"); | ||||
|             return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL); | ||||
|         } | ||||
|         CaptchaVO imageData = getImageData(bufferedImage); | ||||
|         if (imageData == null | ||||
|                 || StringUtils.isBlank(imageData.getOriginalImageBase64())) { | ||||
|             return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR); | ||||
|         } | ||||
|         return ResponseModel.successData(imageData); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public ResponseModel check(CaptchaVO captchaVO) { | ||||
|         ResponseModel r = super.check(captchaVO); | ||||
|         if (!validatedReq(r)) { | ||||
|             return r; | ||||
|         } | ||||
|         //取坐标信息 | ||||
|         String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken()); | ||||
|         if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { | ||||
|             return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); | ||||
|         } | ||||
|         String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey); | ||||
|         //验证码只用一次,即刻失效 | ||||
|         CaptchaServiceFactory.getCache(cacheType).delete(codeKey); | ||||
|         List<PointVO> point = null; | ||||
|         List<PointVO> point1 = null; | ||||
|         String pointJson = null; | ||||
|         /** | ||||
|          * [ | ||||
|          *             { | ||||
|          *                 "x": 85.0, | ||||
|          *                 "y": 34.0 | ||||
|          *             }, | ||||
|          *             { | ||||
|          *                 "x": 129.0, | ||||
|          *                 "y": 56.0 | ||||
|          *             }, | ||||
|          *             { | ||||
|          *                 "x": 233.0, | ||||
|          *                 "y": 27.0 | ||||
|          *             } | ||||
|          * ] | ||||
|          */ | ||||
|         try { | ||||
|             point = JsonUtil.parseArray(s, PointVO.class); | ||||
|             //aes解密 | ||||
|             pointJson = decrypt(captchaVO.getPointJson(), point.get(0).getSecretKey()); | ||||
|             point1 = JsonUtil.parseArray(pointJson, PointVO.class); | ||||
|         } catch (Exception e) { | ||||
|             logger.error("验证码坐标解析失败", e); | ||||
|             afterValidateFail(captchaVO); | ||||
|             return ResponseModel.errorMsg(e.getMessage()); | ||||
|         } | ||||
|         for (int i = 0; i < point.size(); i++) { | ||||
|             if (point.get(i).x - HAN_ZI_SIZE > point1.get(i).x | ||||
|                     || point1.get(i).x > point.get(i).x + HAN_ZI_SIZE | ||||
|                     || point.get(i).y - HAN_ZI_SIZE > point1.get(i).y | ||||
|                     || point1.get(i).y > point.get(i).y + HAN_ZI_SIZE) { | ||||
|                 afterValidateFail(captchaVO); | ||||
|                 return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR); | ||||
|             } | ||||
|         } | ||||
|         //校验成功,将信息存入缓存 | ||||
|         String secretKey = point.get(0).getSecretKey(); | ||||
|         String value = null; | ||||
|         try { | ||||
|             value = AESUtil.aesEncrypt(captchaVO.getToken().concat("---").concat(pointJson), secretKey); | ||||
|         } catch (Exception e) { | ||||
|             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) { | ||||
|         /*if (captchaVO == null) { | ||||
|             return RepCodeEnum.NULL_ERROR.parseError("captchaVO"); | ||||
|         } | ||||
|         if (StringUtils.isEmpty(captchaVO.getCaptchaVerification())) { | ||||
|             return RepCodeEnum.NULL_ERROR.parseError("captchaVerification"); | ||||
|         }*/ | ||||
|         ResponseModel r = super.verification(captchaVO); | ||||
|         if (!validatedReq(r)) { | ||||
|             return r; | ||||
|         } | ||||
|         try { | ||||
|             String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification()); | ||||
|             if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { | ||||
|                 return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); | ||||
|             } | ||||
|             //二次校验取值后,即刻失效 | ||||
|             CaptchaServiceFactory.getCache(cacheType).delete(codeKey); | ||||
|         } catch (Exception e) { | ||||
|             logger.error("验证码坐标解析失败", e); | ||||
|             return ResponseModel.errorMsg(e.getMessage()); | ||||
|         } | ||||
|         return ResponseModel.success(); | ||||
|     } | ||||
|  | ||||
|     public int getWordTotalCount() { | ||||
|         return wordTotalCount; | ||||
|     } | ||||
|  | ||||
|     public void setWordTotalCount(int wordTotalCount) { | ||||
|         this.wordTotalCount = wordTotalCount; | ||||
|     } | ||||
|  | ||||
|     public boolean isFontColorRandom() { | ||||
|         return fontColorRandom; | ||||
|     } | ||||
|  | ||||
|     public void setFontColorRandom(boolean fontColorRandom) { | ||||
|         this.fontColorRandom = fontColorRandom; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 点选文字 字体总个数 | ||||
|      */ | ||||
|     private int wordTotalCount = 4; | ||||
|     /** | ||||
|      * 点选文字 字体颜色是否随机 | ||||
|      */ | ||||
|     private boolean fontColorRandom = Boolean.TRUE; | ||||
|  | ||||
|     private CaptchaVO getImageData(BufferedImage backgroundImage) { | ||||
|         CaptchaVO dataVO = new CaptchaVO(); | ||||
|         List<String> wordList = new ArrayList<String>(); | ||||
|         List<PointVO> pointList = new ArrayList(); | ||||
|  | ||||
|         Graphics backgroundGraphics = backgroundImage.getGraphics(); | ||||
|         int width = backgroundImage.getWidth(); | ||||
|         int height = backgroundImage.getHeight(); | ||||
|  | ||||
|         int wordCount = getWordTotalCount(); | ||||
|         //定义随机1到arr.length某一个字不参与校验 | ||||
|         int num = RandomUtils.getRandomInt(1, wordCount); | ||||
|         Set<String> currentWords = getRandomWords(wordCount); | ||||
|         String secretKey = null; | ||||
|         if (captchaAesStatus) { | ||||
|             secretKey = AESUtil.getKey(); | ||||
|         } | ||||
|         /*for (int i = 0; i < wordCount; i++) { | ||||
|             String word; | ||||
|             do { | ||||
|                 word = RandomUtils.getRandomHan(HAN_ZI); | ||||
|                 currentWords.add(word); | ||||
|             } while (!currentWords.contains(word));*/ | ||||
|         int i = 0; | ||||
|         for (String word : currentWords) { | ||||
|             //随机字体坐标 | ||||
|             PointVO point = randomWordPoint(width, height, i, wordCount); | ||||
|             point.setSecretKey(secretKey); | ||||
|             //随机字体颜色 | ||||
|             if (isFontColorRandom()) { | ||||
|                 backgroundGraphics.setColor(new Color(RandomUtils.getRandomInt(1, 255), | ||||
|                         RandomUtils.getRandomInt(1, 255), RandomUtils.getRandomInt(1, 255))); | ||||
|             } else { | ||||
|                 backgroundGraphics.setColor(Color.BLACK); | ||||
|             } | ||||
|             //设置角度 | ||||
|             AffineTransform affineTransform = new AffineTransform(); | ||||
|             affineTransform.rotate(Math.toRadians(RandomUtils.getRandomInt(-45, 45)), 0, 0); | ||||
|             Font rotatedFont = clickWordFont.deriveFont(affineTransform); | ||||
|             backgroundGraphics.setFont(rotatedFont); | ||||
|             backgroundGraphics.drawString(word, point.getX(), point.getY()); | ||||
|  | ||||
|             if ((num - 1) != i) { | ||||
|                 wordList.add(word); | ||||
|                 pointList.add(point); | ||||
|             } | ||||
|             i++; | ||||
|         } | ||||
|  | ||||
|         backgroundGraphics.setFont(waterMarkFont); | ||||
|         backgroundGraphics.setColor(Color.white); | ||||
|         backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7); | ||||
|  | ||||
|         //创建合并图片 | ||||
|         BufferedImage combinedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); | ||||
|         Graphics combinedGraphics = combinedImage.getGraphics(); | ||||
|         combinedGraphics.drawImage(backgroundImage, 0, 0, null); | ||||
|  | ||||
|         dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(backgroundImage).replaceAll("\r|\n", "")); | ||||
|         //pointList信息不传到前端,只做后端check校验 | ||||
|         //dataVO.setPointList(pointList); | ||||
|         dataVO.setWordList(wordList); | ||||
|         dataVO.setToken(RandomUtils.getUUID()); | ||||
|         dataVO.setSecretKey(secretKey); | ||||
|         //将坐标信息存入redis中 | ||||
|         String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken()); | ||||
|         CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(pointList), EXPIRESIN_SECONDS); | ||||
| //        base64StrToImage(getImageToBase64Str(backgroundImage), "D:\\点击.png"); | ||||
|         return dataVO; | ||||
|     } | ||||
|  | ||||
|     private Set<String> getRandomWords(int wordCount) { | ||||
|         Set<String> words = new HashSet<>(); | ||||
|         int size = HAN_ZI.length(); | ||||
|         for (; ; ) { | ||||
|             String t = HAN_ZI.charAt(RandomUtils.getRandomInt(size)) + ""; | ||||
|             words.add(t); | ||||
|             if (words.size() >= wordCount) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         return words; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 随机字体循环排序下标 | ||||
|      * | ||||
|      * @param imageWidth    图片宽度 | ||||
|      * @param imageHeight   图片高度 | ||||
|      * @param wordSortIndex 字体循环排序下标(i) | ||||
|      * @param wordCount     字数量 | ||||
|      * @return | ||||
|      */ | ||||
|     private static PointVO randomWordPoint(int imageWidth, int imageHeight, int wordSortIndex, int wordCount) { | ||||
|         int avgWidth = imageWidth / (wordCount + 1); | ||||
|         int x, y; | ||||
|         if (avgWidth < HAN_ZI_SIZE_HALF) { | ||||
|             x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, imageWidth); | ||||
|         } else { | ||||
|             if (wordSortIndex == 0) { | ||||
|                 x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF); | ||||
|             } else { | ||||
|                 x = RandomUtils.getRandomInt(avgWidth * wordSortIndex + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF); | ||||
|             } | ||||
|         } | ||||
|         y = RandomUtils.getRandomInt(HAN_ZI_SIZE, imageHeight - HAN_ZI_SIZE); | ||||
|         return new PointVO(x, y, null); | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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()); | ||||
| 	} | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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 { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,3 @@ | ||||
| com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl | ||||
| com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl | ||||
| com.anji.captcha.service.impl.DefaultCaptchaServiceImpl | ||||
| @@ -1 +1,2 @@ | ||||
| com.anji.captcha.config.AjCaptchaAutoConfiguration | ||||
| cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration | ||||
| After Width: | Height: | Size: 86 KiB | 
| After Width: | Height: | Size: 82 KiB | 
| After Width: | Height: | Size: 77 KiB | 
| After Width: | Height: | Size: 80 KiB | 
| After Width: | Height: | Size: 58 KiB | 
| After Width: | Height: | Size: 84 KiB | 
| After Width: | Height: | Size: 72 KiB | 
| After Width: | Height: | Size: 22 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 21 KiB | 
| After Width: | Height: | Size: 7.7 KiB | 
| After Width: | Height: | Size: 7.8 KiB | 
| After Width: | Height: | Size: 42 KiB | 
| After Width: | Height: | Size: 70 KiB | 
| After Width: | Height: | Size: 41 KiB | 
| After Width: | Height: | Size: 48 KiB | 
| After Width: | Height: | Size: 62 KiB | 
| After Width: | Height: | Size: 60 KiB | 
| After Width: | Height: | Size: 48 KiB | 
| After Width: | Height: | Size: 40 KiB | 
| After Width: | Height: | Size: 66 KiB | 
| After Width: | Height: | Size: 79 KiB | 
| After Width: | Height: | Size: 72 KiB | 
| After Width: | Height: | Size: 59 KiB | 
| After Width: | Height: | Size: 70 KiB | 
| After Width: | Height: | Size: 82 KiB | 
| After Width: | Height: | Size: 41 KiB | 
| After Width: | Height: | Size: 8.1 KiB | 
| After Width: | Height: | Size: 58 KiB | 
| @@ -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和嵌入式操作系统中使用 | ||||
 xingyu
					xingyu