diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index de39f99c27..f0781f5dfc 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -55,7 +55,6 @@ 3.8.0 0.1.55 2.6.0 - 1.3.0 4.1.86.Final 2.6.6 @@ -447,12 +446,6 @@ ${tika-core.version} - - com.anji-plus - spring-boot-starter-captcha - ${aj-captcha.version} - - org.apache.velocity velocity-engine-core diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml b/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml index c2d2374060..9aaad51f5f 100644 --- a/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-captcha/pom.xml @@ -23,17 +23,18 @@ spring-boot-starter + + org.springframework.boot + spring-boot-starter-web + provided + + cn.iocoder.boot yudao-spring-boot-starter-redis - - - com.anji-plus - spring-boot-starter-captcha - diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java new file mode 100644 index 0000000000..e20a2d185e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaAutoConfiguration.java @@ -0,0 +1,14 @@ +package com.anji.captcha.config; + +import com.anji.captcha.properties.AjCaptchaProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableConfigurationProperties(AjCaptchaProperties.class) +@ComponentScan("com.anji.captcha") +@Import({AjCaptchaServiceAutoConfiguration.class, AjCaptchaStorageAutoConfiguration.class}) +public class AjCaptchaAutoConfiguration { +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java new file mode 100644 index 0000000000..57e353e03a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaServiceAutoConfiguration.java @@ -0,0 +1,92 @@ +package com.anji.captcha.config; + +import com.anji.captcha.model.common.Const; +import com.anji.captcha.properties.AjCaptchaProperties; +import com.anji.captcha.service.CaptchaService; +import com.anji.captcha.service.impl.CaptchaServiceFactory; +import com.anji.captcha.util.ImageUtils; +import com.anji.captcha.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.Base64Utils; +import org.springframework.util.FileCopyUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +@Configuration +public class AjCaptchaServiceAutoConfiguration { + + private static Logger logger = LoggerFactory.getLogger(AjCaptchaServiceAutoConfiguration.class); + + @Bean + @ConditionalOnMissingBean + public CaptchaService captchaService(AjCaptchaProperties prop) { + logger.info("自定义配置项:{}", prop.toString()); + Properties config = new Properties(); + config.put(Const.CAPTCHA_CACHETYPE, prop.getCacheType().name()); + config.put(Const.CAPTCHA_WATER_MARK, prop.getWaterMark()); + config.put(Const.CAPTCHA_FONT_TYPE, prop.getFontType()); + config.put(Const.CAPTCHA_TYPE, prop.getType().getCodeValue()); + config.put(Const.CAPTCHA_INTERFERENCE_OPTIONS, prop.getInterferenceOptions()); + config.put(Const.ORIGINAL_PATH_JIGSAW, prop.getJigsaw()); + config.put(Const.ORIGINAL_PATH_PIC_CLICK, prop.getPicClick()); + config.put(Const.CAPTCHA_SLIP_OFFSET, prop.getSlipOffset()); + config.put(Const.CAPTCHA_AES_STATUS, String.valueOf(prop.getAesStatus())); + config.put(Const.CAPTCHA_WATER_FONT, prop.getWaterFont()); + config.put(Const.CAPTCHA_CACAHE_MAX_NUMBER, prop.getCacheNumber()); + config.put(Const.CAPTCHA_TIMING_CLEAR_SECOND, prop.getTimingClear()); + + config.put(Const.HISTORY_DATA_CLEAR_ENABLE, prop.isHistoryDataClearEnable() ? "1" : "0"); + + config.put(Const.REQ_FREQUENCY_LIMIT_ENABLE, prop.getReqFrequencyLimitEnable() ? "1" : "0"); + config.put(Const.REQ_GET_LOCK_LIMIT, prop.getReqGetLockLimit() + ""); + config.put(Const.REQ_GET_LOCK_SECONDS, prop.getReqGetLockSeconds() + ""); + config.put(Const.REQ_GET_MINUTE_LIMIT, prop.getReqGetMinuteLimit() + ""); + config.put(Const.REQ_CHECK_MINUTE_LIMIT, prop.getReqCheckMinuteLimit() + ""); + config.put(Const.REQ_VALIDATE_MINUTE_LIMIT, prop.getReqVerifyMinuteLimit() + ""); + + config.put(Const.CAPTCHA_FONT_SIZE, prop.getFontSize() + ""); + config.put(Const.CAPTCHA_FONT_STYLE, prop.getFontStyle() + ""); + config.put(Const.CAPTCHA_WORD_COUNT, prop.getClickWordCount() + ""); + + if ((StringUtils.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:")) + || (StringUtils.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:"))) { + //自定义resources目录下初始化底图 + config.put(Const.CAPTCHA_INIT_ORIGINAL, "true"); + initializeBaseMap(prop.getJigsaw(), prop.getPicClick()); + } + CaptchaService s = CaptchaServiceFactory.getInstance(config); + return s; + } + + private static void initializeBaseMap(String jigsaw, String picClick) { + ImageUtils.cacheBootImage(getResourcesImagesFile(jigsaw + "/original/*.png"), + getResourcesImagesFile(jigsaw + "/slidingBlock/*.png"), + getResourcesImagesFile(picClick + "/*.png")); + } + + public static Map getResourcesImagesFile(String path) { + Map 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; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java new file mode 100644 index 0000000000..f84f6cb085 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/config/AjCaptchaStorageAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.anji.captcha.config; + +import com.anji.captcha.properties.AjCaptchaProperties; +import com.anji.captcha.service.CaptchaCacheService; +import com.anji.captcha.service.impl.CaptchaServiceFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 存储策略自动配置. + * + */ +@Configuration +public class AjCaptchaStorageAutoConfiguration { + + @Bean(name = "AjCaptchaCacheService") + public CaptchaCacheService captchaCacheService(AjCaptchaProperties ajCaptchaProperties){ + //缓存类型redis/local/.... + return CaptchaServiceFactory.getCache(ajCaptchaProperties.getCacheType().name()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/controller/CaptchaController.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/controller/CaptchaController.java new file mode 100644 index 0000000000..468833ecd3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/controller/CaptchaController.java @@ -0,0 +1,65 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.controller; + +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.service.CaptchaService; +import com.anji.captcha.util.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + + +@RestController +@RequestMapping("/captcha") +public class CaptchaController { + + @Autowired + private CaptchaService captchaService; + + @PostMapping("/get") + public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) { + assert request.getRemoteHost()!=null; + data.setBrowserInfo(getRemoteId(request)); + return captchaService.get(data); + } + + @PostMapping("/check") + public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) { + data.setBrowserInfo(getRemoteId(request)); + return captchaService.check(data); + } + + //@PostMapping("/verify") + public ResponseModel verify(@RequestBody CaptchaVO data, HttpServletRequest request) { + return captchaService.verification(data); + } + + public static final String getRemoteId(HttpServletRequest request) { + String xfwd = request.getHeader("X-Forwarded-For"); + String ip = getRemoteIpFromXfwd(xfwd); + String ua = request.getHeader("user-agent"); + if (StringUtils.isNotBlank(ip)) { + return ip + ua; + } + return request.getRemoteAddr() + ua; + } + + private static String getRemoteIpFromXfwd(String xfwd) { + if (StringUtils.isNotBlank(xfwd)) { + String[] ipList = xfwd.split(","); + return StringUtils.trim(ipList[0]); + } + return null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java new file mode 100644 index 0000000000..7249ee9db5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaBaseMapEnum.java @@ -0,0 +1,50 @@ +package com.anji.captcha.model.common; + +/** + * 底图类型枚举 + */ +public enum CaptchaBaseMapEnum { + ORIGINAL("ORIGINAL","滑动拼图底图"), + SLIDING_BLOCK("SLIDING_BLOCK","滑动拼图滑块底图"), + PIC_CLICK("PIC_CLICK","文字点选底图"); + + private String codeValue; + private String codeDesc; + + private CaptchaBaseMapEnum(String codeValue, String codeDesc) { + this.codeValue = codeValue; + this.codeDesc = codeDesc; + } + + public String getCodeValue(){ return this.codeValue;} + + public String getCodeDesc(){ return this.codeDesc;} + + //根据codeValue获取枚举 + public static CaptchaBaseMapEnum parseFromCodeValue(String codeValue){ + for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()){ + if(e.codeValue.equals(codeValue)){ return e;} + } + return null; + } + + //根据codeValue获取描述 + public static String getCodeDescByCodeBalue(String codeValue){ + CaptchaBaseMapEnum enumItem = parseFromCodeValue(codeValue); + return enumItem == null ? "" : enumItem.getCodeDesc(); + } + + //验证codeValue是否有效 + public static boolean validateCodeValue(String codeValue){ return parseFromCodeValue(codeValue)!=null;} + + //列出所有值字符串 + public static String getString(){ + StringBuffer buffer = new StringBuffer(); + for (CaptchaBaseMapEnum e : CaptchaBaseMapEnum.values()){ + buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", "); + } + buffer.deleteCharAt(buffer.lastIndexOf(",")); + return buffer.toString().trim(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java new file mode 100644 index 0000000000..7e1329a9f9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/CaptchaTypeEnum.java @@ -0,0 +1,56 @@ +package com.anji.captcha.model.common; + +public enum CaptchaTypeEnum { + /** + * 滑块拼图. + */ + BLOCKPUZZLE("blockPuzzle","滑块拼图"), + /** + * 文字点选. + */ + CLICKWORD("clickWord","文字点选"), + /** + * 默认. + */ + DEFAULT("default","默认"); + + private String codeValue; + private String codeDesc; + + private CaptchaTypeEnum(String codeValue, String codeDesc) { + this.codeValue = codeValue; + this.codeDesc = codeDesc; + } + + public String getCodeValue(){ return this.codeValue;} + + public String getCodeDesc(){ return this.codeDesc;} + + //根据codeValue获取枚举 + public static CaptchaTypeEnum parseFromCodeValue(String codeValue){ + for (CaptchaTypeEnum e : CaptchaTypeEnum.values()){ + if(e.codeValue.equals(codeValue)){ return e;} + } + return null; + } + + //根据codeValue获取描述 + public static String getCodeDescByCodeBalue(String codeValue){ + CaptchaTypeEnum enumItem = parseFromCodeValue(codeValue); + return enumItem == null ? "" : enumItem.getCodeDesc(); + } + + //验证codeValue是否有效 + public static boolean validateCodeValue(String codeValue){ return parseFromCodeValue(codeValue)!=null;} + + //列出所有值字符串 + public static String getString(){ + StringBuffer buffer = new StringBuffer(); + for (CaptchaTypeEnum e : CaptchaTypeEnum.values()){ + buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", "); + } + buffer.deleteCharAt(buffer.lastIndexOf(",")); + return buffer.toString().trim(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java new file mode 100644 index 0000000000..c029d5d1de --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/Const.java @@ -0,0 +1,112 @@ +package com.anji.captcha.model.common; + +/*** + * @author wongbin + */ +public interface Const { + + /** + *滑块底图路径 + */ + String ORIGINAL_PATH_JIGSAW = "captcha.captchaOriginalPath.jigsaw"; + + /*** + *点选底图路径 + */ + String ORIGINAL_PATH_PIC_CLICK = "captcha.captchaOriginalPath.pic-click"; + + /** + * 缓存local/redis... + */ + String CAPTCHA_CACHETYPE = "captcha.cacheType"; + + /** + * 右下角水印文字(我的水印) + */ + String CAPTCHA_WATER_MARK = "captcha.water.mark"; + + /** + * 点选文字验证码的文字字体(宋体) + */ + String CAPTCHA_FONT_TYPE = "captcha.font.type"; + String CAPTCHA_FONT_STYLE = "captcha.font.style"; + String CAPTCHA_FONT_SIZE = "captcha.font.size"; + + /** + * 验证码类型default两种都实例化。 + */ + String CAPTCHA_TYPE = "captcha.type"; + + /** + * 滑动干扰项(0/1/2) + */ + String CAPTCHA_INTERFERENCE_OPTIONS = "captcha.interference.options"; + + /** + * 底图自定义初始化 + */ + String CAPTCHA_INIT_ORIGINAL = "captcha.init.original"; + + /** + * 滑动误差偏移量 + */ + String CAPTCHA_SLIP_OFFSET = "captcha.slip.offset"; + + /** + * aes加密开关 + */ + String CAPTCHA_AES_STATUS = "captcha.aes.status"; + + /** + * 右下角水印字体(宋体) + */ + String CAPTCHA_WATER_FONT = "captcha.water.font"; + + /** + * local缓存的阈值 + */ + String CAPTCHA_CACAHE_MAX_NUMBER = "captcha.cache.number"; + + /** + * 定时清理过期local缓存,秒 + */ + String CAPTCHA_TIMING_CLEAR_SECOND = "captcha.timing.clear"; + + /** + * 历史资源清除开关 0禁用,1 开启 + */ + String HISTORY_DATA_CLEAR_ENABLE = "captcha.history.data.clear.enable"; + + /** + * 接口限流开关 0禁用 1启用 + */ + String REQ_FREQUENCY_LIMIT_ENABLE = "captcha.req.frequency.limit.enable"; + + /** + * get 接口 一分钟请求次数限制 + */ + String REQ_GET_MINUTE_LIMIT = "captcha.req.get.minute.limit"; + + /** + * 验证失败后,get接口锁定时间 + */ + String REQ_GET_LOCK_LIMIT = "captcha.req.get.lock.limit"; + /** + * 验证失败后,get接口锁定时间 + */ + String REQ_GET_LOCK_SECONDS = "captcha.req.get.lock.seconds"; + + /** + * verify 接口 一分钟请求次数限制 + */ + String REQ_VALIDATE_MINUTE_LIMIT = "captcha.req.verify.minute.limit"; + /** + * check接口 一分钟请求次数限制 + */ + String REQ_CHECK_MINUTE_LIMIT = "captcha.req.check.minute.limit"; + + /*** + * 点选文字个数 + */ + String CAPTCHA_WORD_COUNT = "captcha.word.count"; +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java new file mode 100644 index 0000000000..c452cc105e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RepCodeEnum.java @@ -0,0 +1,73 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.model.common; + +import java.text.MessageFormat; + +/** + * 返回应答码 + * @author + * + */ +public enum RepCodeEnum { + + /** 0001 - 0099 网关应答码 */ + SUCCESS("0000", "成功"), + ERROR("0001", "操作失败"), + EXCEPTION("9999", "服务器内部异常"), + + BLANK_ERROR("0011", "{0}不能为空"), + NULL_ERROR("0011", "{0}不能为空"), + NOT_NULL_ERROR("0012", "{0}必须为空"), + NOT_EXIST_ERROR("0013", "{0}数据库中不存在"), + EXIST_ERROR("0014", "{0}数据库中已存在"), + PARAM_TYPE_ERROR("0015", "{0}类型错误"), + PARAM_FORMAT_ERROR("0016", "{0}格式错误"), + + API_CAPTCHA_INVALID("6110", "验证码已失效,请重新获取"), + API_CAPTCHA_COORDINATE_ERROR("6111", "验证失败"), + API_CAPTCHA_ERROR("6112", "获取验证码失败,请联系管理员"), + API_CAPTCHA_BASEMAP_NULL("6113", "底图未初始化成功,请检查路径"), + + API_REQ_LIMIT_GET_ERROR("6201", "get接口请求次数超限,请稍后再试!"), + API_REQ_INVALID("6206", "无效请求,请重新获取验证码"), + API_REQ_LOCK_GET_ERROR("6202", "接口验证失败数过多,请稍后再试"), + API_REQ_LIMIT_CHECK_ERROR("6204", "check接口请求次数超限,请稍后再试!"), + API_REQ_LIMIT_VERIFY_ERROR("6205", "verify请求次数超限!"), + ; + private String code; + private String desc; + + RepCodeEnum(String code, String desc) { + this.code = code; + this.desc = desc; + } + public String getCode() { + return code; + } + public String getDesc() { + return desc; + } + public String getName(){ + return this.name(); + } + + /** 将入参fieldNames与this.desc组合成错误信息 + * {fieldName}不能为空 + * @param fieldNames + * @return + */ + public ResponseModel parseError(Object... fieldNames) { + ResponseModel errorMessage=new ResponseModel(); + String newDesc = MessageFormat.format(this.desc, fieldNames); + + errorMessage.setRepCode(this.code); + errorMessage.setRepMsg(newDesc); + return errorMessage; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java new file mode 100644 index 0000000000..06e33c3d94 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/RequestModel.java @@ -0,0 +1,157 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.model.common; + +import com.anji.captcha.util.StringUtils; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; + +public class RequestModel implements Serializable { + + private static final long serialVersionUID = -5800786065305114784L; + + /**当前请求接口路径 /business/accessUser/login */ + private String servletPath; + + /** {"reqData":{"password":"*****","userName":"admin"},"sign":"a304a7f296f565b6d2009797f68180f0","time":"1542456453355","token":""} */ + private String requestString; + + /** {"password":"****","userName":"admin"} */ + private HashMap reqData; + + private String token; + + private Long userId; + + private String userName; + + private List projectList; + + //拥有哪些分组 + private List 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 getProjectList() { + return projectList; + } + + public void setProjectList(List projectList) { + this.projectList = projectList; + } + + public List getGroupIdList() { + return groupIdList; + } + + public void setGroupIdList(List 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; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java new file mode 100644 index 0000000000..dbaf4c746f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/common/ResponseModel.java @@ -0,0 +1,111 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.model.common; + +import com.anji.captcha.util.StringUtils; + +import java.io.Serializable; + +public class ResponseModel implements Serializable { + + private static final long serialVersionUID = 8445617032523881407L; + + private String repCode; + + private String repMsg; + + private Object repData; + + public ResponseModel() { + this.repCode = RepCodeEnum.SUCCESS.getCode(); + } + + public ResponseModel(RepCodeEnum repCodeEnum) { + this.setRepCodeEnum(repCodeEnum); + } + + //成功 + public static ResponseModel success(){ + return ResponseModel.successMsg("成功"); + } + public static ResponseModel successMsg(String message){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepMsg(message); + return responseModel; + } + public static ResponseModel successData(Object data){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCode(RepCodeEnum.SUCCESS.getCode()); + responseModel.setRepData(data); + return responseModel; + } + + //失败 + public static ResponseModel errorMsg(RepCodeEnum message){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCodeEnum(message); + return responseModel; + } + public static ResponseModel errorMsg(String message){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCode(RepCodeEnum.ERROR.getCode()); + responseModel.setRepMsg(message); + return responseModel; + } + public static ResponseModel errorMsg(RepCodeEnum repCodeEnum, String message){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCode(repCodeEnum.getCode()); + responseModel.setRepMsg(message); + return responseModel; + } + public static ResponseModel exceptionMsg(String message){ + ResponseModel responseModel = new ResponseModel(); + responseModel.setRepCode(RepCodeEnum.EXCEPTION.getCode()); + responseModel.setRepMsg(RepCodeEnum.EXCEPTION.getDesc() + ": " + message); + return responseModel; + } + + @Override + public String toString() { + return "ResponseModel{" + "repCode='" + repCode + '\'' + ", repMsg='" + + repMsg + '\'' + ", repData=" + repData + '}'; + } + + public boolean isSuccess(){ + return StringUtils.equals(repCode, RepCodeEnum.SUCCESS.getCode()); + } + + public String getRepCode() { + return repCode; + } + + public void setRepCode(String repCode) { + this.repCode = repCode; + } + public void setRepCodeEnum(RepCodeEnum repCodeEnum) { + this.repCode=repCodeEnum.getCode(); + this.repMsg=repCodeEnum.getDesc(); + } + + public String getRepMsg() { + return repMsg; + } + + public void setRepMsg(String repMsg) { + this.repMsg = repMsg; + } + + public Object getRepData() { + return repData; + } + + public void setRepData(Object repData) { + this.repData = repData; + } + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java new file mode 100644 index 0000000000..3c6e0c330e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/CaptchaVO.java @@ -0,0 +1,251 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.model.vo; + +import java.awt.*; +import java.io.Serializable; +import java.util.List; + +public class CaptchaVO implements Serializable { + + /** + * 验证码id(后台申请) + */ + private String captchaId; + + private String projectCode; + + /** + * 验证码类型:(clickWord,blockPuzzle) + */ + private String captchaType; + + private String captchaOriginalPath; + + private String captchaFontType; + + private Integer captchaFontSize; + + private String secretKey; + + /** + * 原生图片base64 + */ + private String originalImageBase64; + + /** + * 滑块点选坐标 + */ + private PointVO point; + + /** + * 滑块图片base64 + */ + private String jigsawImageBase64; + + /** + * 点选文字 + */ + private List wordList; + + /** + * 点选坐标 + */ + private List 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 getWordList() { + return wordList; + } + + public void setWordList(List wordList) { + this.wordList = wordList; + } + + public List getPointList() { + return pointList; + } + + public void setPointList(List 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; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java new file mode 100644 index 0000000000..5a943bf1c8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/model/vo/PointVO.java @@ -0,0 +1,95 @@ +package com.anji.captcha.model.vo; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Created by raodeming on 2020/5/16. + */ +public class PointVO { + private String secretKey; + + public int x; + + public int y; + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public PointVO(int x, int y, String secretKey) { + this.secretKey = secretKey; + this.x = x; + this.y = y; + } + + public PointVO() { + } + + public PointVO(int x, int y) { + this.x = x; + this.y = y; + } + + public String toJsonString() { + return String.format("{\"secretKey\":\"%s\",\"x\":%d,\"y\":%d}", secretKey, x, y); + } + + public PointVO parse(String jsonStr) { + Map m = new HashMap(); + Arrays.stream(jsonStr + .replaceFirst(",\\{", "\\{") + .replaceFirst("\\{", "") + .replaceFirst("\\}", "") + .replaceAll("\"", "") + .split(",")).forEach(item -> { + m.put(item.split(":")[0], item.split(":")[1]); + }); + //PointVO d = new PointVO(); + setX(Double.valueOf("" + m.get("x")).intValue()); + setY(Double.valueOf("" + m.get("y")).intValue()); + setSecretKey(m.getOrDefault("secretKey", "") + ""); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PointVO pointVO = (PointVO) o; + return x == pointVO.x && y == pointVO.y && Objects.equals(secretKey, pointVO.secretKey); + } + + @Override + public int hashCode() { + + return Objects.hash(secretKey, x, y); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java new file mode 100644 index 0000000000..fca735943d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java @@ -0,0 +1,338 @@ +package com.anji.captcha.properties; + +import com.anji.captcha.model.common.CaptchaTypeEnum; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.awt.*; + +import static com.anji.captcha.properties.AjCaptchaProperties.PREFIX; +import static com.anji.captcha.properties.AjCaptchaProperties.StorageType.local; + +@ConfigurationProperties(PREFIX) +public class AjCaptchaProperties { + public static final String PREFIX = "aj.captcha"; + + /** + * 验证码类型. + */ + private CaptchaTypeEnum type = CaptchaTypeEnum.DEFAULT; + + /** + * 滑动拼图底图路径. + */ + private String jigsaw = ""; + + /** + * 点选文字底图路径. + */ + private String picClick = ""; + + + /** + * 右下角水印文字(我的水印). + */ + private String waterMark = "我的水印"; + + /** + * 右下角水印字体(文泉驿正黑). + */ + private String waterFont = "WenQuanZhengHei.ttf"; + + /** + * 点选文字验证码的文字字体(文泉驿正黑). + */ + private String fontType = "WenQuanZhengHei.ttf"; + + /** + * 校验滑动拼图允许误差偏移量(默认5像素). + */ + private String slipOffset = "5"; + + /** + * aes加密坐标开启或者禁用(true|false). + */ + private Boolean aesStatus = true; + + /** + * 滑块干扰项(0/1/2) + */ + private String interferenceOptions = "0"; + + /** + * local缓存的阈值 + */ + private String cacheNumber = "1000"; + + /** + * 定时清理过期local缓存(单位秒) + */ + private String timingClear = "180"; + + /** + * 缓存类型redis/local/.... + */ + private StorageType cacheType = local; + /** + * 历史数据清除开关 + */ + private boolean historyDataClearEnable = false; + + /** + * 一分钟内接口请求次数限制 开关 + */ + private boolean reqFrequencyLimitEnable = false; + + /*** + * 一分钟内check接口失败次数 + */ + private int reqGetLockLimit = 5; + /** + * + */ + private int reqGetLockSeconds = 300; + + /*** + * get接口一分钟内限制访问数 + */ + private int reqGetMinuteLimit = 100; + private int reqCheckMinuteLimit = 100; + private int reqVerifyMinuteLimit = 100; + + /** + * 点选字体样式 + */ + private int fontStyle = Font.BOLD; + + /** + * 点选字体大小 + */ + private int fontSize = 25; + + /** + * 点选文字个数,存在问题,暂不要使用 + */ + private int clickWordCount = 4; + + public int getFontStyle() { + return fontStyle; + } + + public void setFontStyle(int fontStyle) { + this.fontStyle = fontStyle; + } + + public int getFontSize() { + return fontSize; + } + + public void setFontSize(int fontSize) { + this.fontSize = fontSize; + } + + public int getClickWordCount() { + return clickWordCount; + } + + public void setClickWordCount(int clickWordCount) { + this.clickWordCount = clickWordCount; + } + + public boolean isHistoryDataClearEnable() { + return historyDataClearEnable; + } + + public void setHistoryDataClearEnable(boolean historyDataClearEnable) { + this.historyDataClearEnable = historyDataClearEnable; + } + + public boolean isReqFrequencyLimitEnable() { + return reqFrequencyLimitEnable; + } + + public boolean getReqFrequencyLimitEnable() { + return reqFrequencyLimitEnable; + } + + public void setReqFrequencyLimitEnable(boolean reqFrequencyLimitEnable) { + this.reqFrequencyLimitEnable = reqFrequencyLimitEnable; + } + + public int getReqGetLockLimit() { + return reqGetLockLimit; + } + + public void setReqGetLockLimit(int reqGetLockLimit) { + this.reqGetLockLimit = reqGetLockLimit; + } + + public int getReqGetLockSeconds() { + return reqGetLockSeconds; + } + + public void setReqGetLockSeconds(int reqGetLockSeconds) { + this.reqGetLockSeconds = reqGetLockSeconds; + } + + public int getReqGetMinuteLimit() { + return reqGetMinuteLimit; + } + + public void setReqGetMinuteLimit(int reqGetMinuteLimit) { + this.reqGetMinuteLimit = reqGetMinuteLimit; + } + + public int getReqCheckMinuteLimit() { + return reqGetMinuteLimit; + } + + public void setReqCheckMinuteLimit(int reqCheckMinuteLimit) { + this.reqCheckMinuteLimit = reqCheckMinuteLimit; + } + + public int getReqVerifyMinuteLimit() { + return reqVerifyMinuteLimit; + } + + public void setReqVerifyMinuteLimit(int reqVerifyMinuteLimit) { + this.reqVerifyMinuteLimit = reqVerifyMinuteLimit; + } + + public enum StorageType { + /** + * 内存. + */ + local, + /** + * redis. + */ + redis, + /** + * 其他. + */ + other, + } + + public static String getPrefix() { + return PREFIX; + } + + public CaptchaTypeEnum getType() { + return type; + } + + public void setType(CaptchaTypeEnum type) { + this.type = type; + } + + public String getJigsaw() { + return jigsaw; + } + + public void setJigsaw(String jigsaw) { + this.jigsaw = jigsaw; + } + + public String getPicClick() { + return picClick; + } + + public void setPicClick(String picClick) { + this.picClick = picClick; + } + + public String getWaterMark() { + return waterMark; + } + + public void setWaterMark(String waterMark) { + this.waterMark = waterMark; + } + + public String getWaterFont() { + return waterFont; + } + + public void setWaterFont(String waterFont) { + this.waterFont = waterFont; + } + + public String getFontType() { + return fontType; + } + + public void setFontType(String fontType) { + this.fontType = fontType; + } + + public String getSlipOffset() { + return slipOffset; + } + + public void setSlipOffset(String slipOffset) { + this.slipOffset = slipOffset; + } + + public Boolean getAesStatus() { + return aesStatus; + } + + public void setAesStatus(Boolean aesStatus) { + this.aesStatus = aesStatus; + } + + public StorageType getCacheType() { + return cacheType; + } + + public void setCacheType(StorageType cacheType) { + this.cacheType = cacheType; + } + + public String getInterferenceOptions() { + return interferenceOptions; + } + + public void setInterferenceOptions(String interferenceOptions) { + this.interferenceOptions = interferenceOptions; + } + + public String getCacheNumber() { + return cacheNumber; + } + + public void setCacheNumber(String cacheNumber) { + this.cacheNumber = cacheNumber; + } + + public String getTimingClear() { + return timingClear; + } + + public void setTimingClear(String timingClear) { + this.timingClear = timingClear; + } + + @Override + public String toString() { + return "\nAjCaptchaProperties{" + + "type=" + type + + ", jigsaw='" + jigsaw + '\'' + + ", picClick='" + picClick + '\'' + + ", waterMark='" + waterMark + '\'' + + ", waterFont='" + waterFont + '\'' + + ", fontType='" + fontType + '\'' + + ", slipOffset='" + slipOffset + '\'' + + ", aesStatus=" + aesStatus + + ", interferenceOptions='" + interferenceOptions + '\'' + + ", cacheNumber='" + cacheNumber + '\'' + + ", timingClear='" + timingClear + '\'' + + ", cacheType=" + cacheType + + ", reqFrequencyLimitEnable=" + reqFrequencyLimitEnable + + ", reqGetLockLimit=" + reqGetLockLimit + + ", reqGetLockSeconds=" + reqGetLockSeconds + + ", reqGetMinuteLimit=" + reqGetMinuteLimit + + ", reqCheckMinuteLimit=" + reqCheckMinuteLimit + + ", reqVerifyMinuteLimit=" + reqVerifyMinuteLimit + + '}'; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java new file mode 100644 index 0000000000..fd401d6ea0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaCacheService.java @@ -0,0 +1,41 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.service; + +/** + * 验证码缓存接口 + * @author lide1202@hotmail.com + * @date 2018-08-21 + */ +public interface CaptchaCacheService { + + void set(String key, String value, long expiresInSeconds); + + boolean exists(String key); + + void delete(String key); + + String get(String key); + + /** + * 缓存类型-local/redis/memcache/.. + * 通过java SPI机制,接入方可自定义实现类 + * @return + */ + String type(); + + /*** + * + * @param key + * @param val + * @return + */ + default Long increment(String key, long val){ + return 0L; + }; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java new file mode 100644 index 0000000000..723bbb0d16 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/CaptchaService.java @@ -0,0 +1,58 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.service; + +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; + +import java.util.Properties; + +/** + * 验证码服务接口 + * @author lide1202@hotmail.com + * @date 2020-05-12 + */ +public interface CaptchaService { + /** + * 配置初始化 + */ + void init(Properties config); + + /** + * 获取验证码 + * @param captchaVO + * @return + */ + ResponseModel get(CaptchaVO captchaVO); + + /** + * 核对验证码(前端) + * @param captchaVO + * @return + */ + ResponseModel check(CaptchaVO captchaVO); + + /** + * 二次校验验证码(后端) + * @param captchaVO + * @return + */ + ResponseModel verification(CaptchaVO captchaVO); + + /*** + * 验证码类型 + * 通过java SPI机制,接入方可自定义实现类,实现新的验证类型 + * @return + */ + String captchaType(); + + /** + * 历史资源清除(过期的图片文件,生成的临时图片...) + * @param config 配置项 控制资源清理的粒度 + */ + void destroy(Properties config); +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java new file mode 100644 index 0000000000..b33d4ed533 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/AbstractCaptchaService.java @@ -0,0 +1,269 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.service.impl; + +import 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; + } + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java new file mode 100644 index 0000000000..2fd7b08934 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/BlockPuzzleCaptchaServiceImpl.java @@ -0,0 +1,422 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.service.impl; + +import com.anji.captcha.model.common.CaptchaTypeEnum; +import com.anji.captcha.model.common.RepCodeEnum; +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.model.vo.PointVO; +import com.anji.captcha.util.*; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.util.Base64; +import java.util.Objects; +import java.util.Properties; +import java.util.Random; + +/** + * 滑动验证码 + *

+ * 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(); + } + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java new file mode 100644 index 0000000000..264c6c09cd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaCacheServiceMemImpl.java @@ -0,0 +1,47 @@ +package com.anji.captcha.service.impl; + +import com.anji.captcha.service.CaptchaCacheService; +import com.anji.captcha.util.CacheUtil; + +/** + * 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。 + * 如果应用是单点的,也没有使用redis,那默认使用内存。 + * 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。 + * @Title: 默认使用内存当缓存 + * @author lide1202@hotmail.com + * @date 2020-05-12 + */ +public class CaptchaCacheServiceMemImpl implements CaptchaCacheService { + @Override + public void set(String key, String value, long expiresInSeconds) { + + CacheUtil.set(key, value, expiresInSeconds); + } + + @Override + public boolean exists(String key) { + return CacheUtil.exists(key); + } + + @Override + public void delete(String key) { + CacheUtil.delete(key); + } + + @Override + public String get(String key) { + return CacheUtil.get(key); + } + + @Override + public Long increment(String key, long val) { + Long ret = Long.valueOf(CacheUtil.get(key))+val; + CacheUtil.set(key,ret+"",0); + return ret; + } + + @Override + public String type() { + return "local"; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java new file mode 100644 index 0000000000..6f3ff70faf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/CaptchaServiceFactory.java @@ -0,0 +1,60 @@ +package com.anji.captcha.service.impl; + +import com.anji.captcha.model.common.Const; +import com.anji.captcha.service.CaptchaCacheService; +import com.anji.captcha.service.CaptchaService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; + +/** + * Created by raodeming on 2020/5/26. + */ +public class CaptchaServiceFactory { + + private static Logger logger = LoggerFactory.getLogger(CaptchaServiceFactory.class); + + public static CaptchaService getInstance(Properties config) { + //先把所有CaptchaService初始化,通过init方法,实例字体等,add by lide1202@hotmail.com + /*try{ + for(CaptchaService item: instances.values()){ + item.init(config); + } + }catch (Exception e){ + logger.warn("init captchaService fail:{}", e); + }*/ + + String captchaType = config.getProperty(Const.CAPTCHA_TYPE, "default"); + CaptchaService ret = instances.get(captchaType); + if (ret == null) { + throw new RuntimeException("unsupported-[captcha.type]=" + captchaType); + } + ret.init(config); + return ret; + } + + public static CaptchaCacheService getCache(String cacheType) { + return cacheService.get(cacheType); + } + + public volatile static Map instances = new HashMap(); + public volatile static Map cacheService = new HashMap(); + + static { + ServiceLoader cacheServices = ServiceLoader.load(CaptchaCacheService.class); + for (CaptchaCacheService item : cacheServices) { + cacheService.put(item.type(), item); + } + logger.info("supported-captchaCache-service:{}", cacheService.keySet().toString()); + ServiceLoader services = ServiceLoader.load(CaptchaService.class); + for (CaptchaService item : services) { + instances.put(item.captchaType(), item); + } + ; + logger.info("supported-captchaTypes-service:{}", instances.keySet().toString()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java new file mode 100644 index 0000000000..f208e0dd53 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/ClickWordCaptchaServiceImpl.java @@ -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.*; + +/** + * 点选文字验证码 + *

+ * 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 point = null; + List point1 = null; + String pointJson = null; + /** + * [ + * { + * "x": 85.0, + * "y": 34.0 + * }, + * { + * "x": 129.0, + * "y": 56.0 + * }, + * { + * "x": 233.0, + * "y": 27.0 + * } + * ] + */ + try { + point = JsonUtil.parseArray(s, PointVO.class); + //aes解密 + pointJson = decrypt(captchaVO.getPointJson(), point.get(0).getSecretKey()); + point1 = JsonUtil.parseArray(pointJson, PointVO.class); + } catch (Exception e) { + 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 wordList = new ArrayList(); + List pointList = new ArrayList(); + + Graphics backgroundGraphics = backgroundImage.getGraphics(); + int width = backgroundImage.getWidth(); + int height = backgroundImage.getHeight(); + + int wordCount = getWordTotalCount(); + //定义随机1到arr.length某一个字不参与校验 + int num = RandomUtils.getRandomInt(1, wordCount); + Set currentWords = getRandomWords(wordCount); + String secretKey = null; + if (captchaAesStatus) { + secretKey = AESUtil.getKey(); + } + /*for (int i = 0; i < wordCount; i++) { + String word; + do { + word = RandomUtils.getRandomHan(HAN_ZI); + currentWords.add(word); + } while (!currentWords.contains(word));*/ + int i = 0; + for (String word : currentWords) { + //随机字体坐标 + PointVO point = randomWordPoint(width, height, i, wordCount); + point.setSecretKey(secretKey); + //随机字体颜色 + if (isFontColorRandom()) { + backgroundGraphics.setColor(new Color(RandomUtils.getRandomInt(1, 255), + RandomUtils.getRandomInt(1, 255), RandomUtils.getRandomInt(1, 255))); + } else { + backgroundGraphics.setColor(Color.BLACK); + } + //设置角度 + AffineTransform affineTransform = new AffineTransform(); + affineTransform.rotate(Math.toRadians(RandomUtils.getRandomInt(-45, 45)), 0, 0); + Font rotatedFont = clickWordFont.deriveFont(affineTransform); + backgroundGraphics.setFont(rotatedFont); + backgroundGraphics.drawString(word, point.getX(), point.getY()); + + if ((num - 1) != i) { + wordList.add(word); + pointList.add(point); + } + i++; + } + + backgroundGraphics.setFont(waterMarkFont); + backgroundGraphics.setColor(Color.white); + backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7); + + //创建合并图片 + BufferedImage combinedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics combinedGraphics = combinedImage.getGraphics(); + combinedGraphics.drawImage(backgroundImage, 0, 0, null); + + dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(backgroundImage).replaceAll("\r|\n", "")); + //pointList信息不传到前端,只做后端check校验 + //dataVO.setPointList(pointList); + dataVO.setWordList(wordList); + dataVO.setToken(RandomUtils.getUUID()); + dataVO.setSecretKey(secretKey); + //将坐标信息存入redis中 + String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken()); + CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(pointList), EXPIRESIN_SECONDS); +// base64StrToImage(getImageToBase64Str(backgroundImage), "D:\\点击.png"); + return dataVO; + } + + private Set getRandomWords(int wordCount) { + Set words = new HashSet<>(); + int size = HAN_ZI.length(); + for (; ; ) { + String t = HAN_ZI.charAt(RandomUtils.getRandomInt(size)) + ""; + words.add(t); + if (words.size() >= wordCount) { + break; + } + } + return words; + } + + /** + * 随机字体循环排序下标 + * + * @param imageWidth 图片宽度 + * @param imageHeight 图片高度 + * @param wordSortIndex 字体循环排序下标(i) + * @param wordCount 字数量 + * @return + */ + private static PointVO randomWordPoint(int imageWidth, int imageHeight, int wordSortIndex, int wordCount) { + int avgWidth = imageWidth / (wordCount + 1); + int x, y; + if (avgWidth < HAN_ZI_SIZE_HALF) { + x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, imageWidth); + } else { + if (wordSortIndex == 0) { + x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF); + } else { + x = RandomUtils.getRandomInt(avgWidth * wordSortIndex + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF); + } + } + y = RandomUtils.getRandomInt(HAN_ZI_SIZE, imageHeight - HAN_ZI_SIZE); + return new PointVO(x, y, null); + } + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java new file mode 100644 index 0000000000..18c2180e66 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/DefaultCaptchaServiceImpl.java @@ -0,0 +1,98 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.service.impl; + +import com.anji.captcha.model.common.RepCodeEnum; +import com.anji.captcha.model.common.ResponseModel; +import com.anji.captcha.model.vo.CaptchaVO; +import com.anji.captcha.service.CaptchaService; +import com.anji.captcha.util.StringUtils; + +import java.util.Properties; + +/** + * Created by raodeming on 2019/12/25. + */ +public class DefaultCaptchaServiceImpl extends AbstractCaptchaService{ + + @Override + public String captchaType() { + return "default"; + } + + @Override + public void init(Properties config) { + for (String s : CaptchaServiceFactory.instances.keySet()) { + if(captchaType().equals(s)){ + continue; + } + getService(s).init(config); + } + } + + @Override + public void destroy(Properties config) { + for (String s : CaptchaServiceFactory.instances.keySet()) { + if(captchaType().equals(s)){ + continue; + } + getService(s).destroy(config); + } + } + + private CaptchaService getService(String captchaType){ + return CaptchaServiceFactory.instances.get(captchaType); + } + + @Override + public ResponseModel get(CaptchaVO captchaVO) { + if (captchaVO == null) { + return RepCodeEnum.NULL_ERROR.parseError("captchaVO"); + } + if (StringUtils.isEmpty(captchaVO.getCaptchaType())) { + return RepCodeEnum.NULL_ERROR.parseError("类型"); + } + return getService(captchaVO.getCaptchaType()).get(captchaVO); + } + + @Override + public ResponseModel check(CaptchaVO captchaVO) { + if (captchaVO == null) { + return RepCodeEnum.NULL_ERROR.parseError("captchaVO"); + } + if (StringUtils.isEmpty(captchaVO.getCaptchaType())) { + return RepCodeEnum.NULL_ERROR.parseError("类型"); + } + if (StringUtils.isEmpty(captchaVO.getToken())) { + return RepCodeEnum.NULL_ERROR.parseError("token"); + } + return getService(captchaVO.getCaptchaType()).check(captchaVO); + } + + @Override + public ResponseModel verification(CaptchaVO captchaVO) { + if (captchaVO == null) { + return RepCodeEnum.NULL_ERROR.parseError("captchaVO"); + } + if (StringUtils.isEmpty(captchaVO.getCaptchaVerification())) { + return RepCodeEnum.NULL_ERROR.parseError("二次校验参数"); + } + try { + String codeKey = String.format(REDIS_SECOND_CAPTCHA_KEY, captchaVO.getCaptchaVerification()); + if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) { + return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID); + } + //二次校验取值后,即刻失效 + CaptchaServiceFactory.getCache(cacheType).delete(codeKey); + } catch (Exception e) { + logger.error("验证码坐标解析失败", e); + return ResponseModel.errorMsg(e.getMessage()); + } + return ResponseModel.success(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java new file mode 100644 index 0000000000..ffbceb5a91 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/service/impl/FrequencyLimitHandler.java @@ -0,0 +1,154 @@ +package com.anji.captcha.service.impl; + +import 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; + } + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java new file mode 100644 index 0000000000..d8ea95b8d4 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/AESUtil.java @@ -0,0 +1,139 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.util; + + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.SecretKeySpec; +import java.math.BigInteger; +import java.util.Base64; + + +public class AESUtil { + //算法 + private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding"; + + /** + * 获取随机key + * @return + */ + public static String getKey() { + return RandomUtils.getRandomString(16); + } + + + /** + * 将byte[]转为各种进制的字符串 + * @param bytes byte[] + * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 + * @return 转换后的字符串 + */ + public static String binary(byte[] bytes, int radix){ + return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数 + } + + /** + * base 64 encode + * @param bytes 待编码的byte[] + * @return 编码后的base 64 code + */ + public static String base64Encode(byte[] bytes){ + //return Base64.encodeBase64String(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * base 64 decode + * @param base64Code 待解码的base 64 code + * @return 解码后的byte[] + * @throws Exception + */ + public static byte[] base64Decode(String base64Code) throws Exception{ + Base64.Decoder decoder = Base64.getDecoder(); + return StringUtils.isEmpty(base64Code) ? null : decoder.decode(base64Code); + } + + + /** + * AES加密 + * @param content 待加密的内容 + * @param encryptKey 加密密钥 + * @return 加密后的byte[] + * @throws Exception + */ + public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception { + KeyGenerator kgen = KeyGenerator.getInstance("AES"); + kgen.init(128); + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES")); + + return cipher.doFinal(content.getBytes("utf-8")); + } + + + /** + * AES加密为base 64 code + * @param content 待加密的内容 + * @param encryptKey 加密密钥 + * @return 加密后的base 64 code + * @throws Exception + */ + public static String aesEncrypt(String content, String encryptKey) throws Exception { + if (StringUtils.isBlank(encryptKey)) { + return content; + } + return base64Encode(aesEncryptToBytes(content, encryptKey)); + } + + /** + * AES解密 + * @param encryptBytes 待解密的byte[] + * @param decryptKey 解密密钥 + * @return 解密后的String + * @throws Exception + */ + public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception { + KeyGenerator kgen = KeyGenerator.getInstance("AES"); + kgen.init(128); + + Cipher cipher = Cipher.getInstance(ALGORITHMSTR); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES")); + byte[] decryptBytes = cipher.doFinal(encryptBytes); + return new String(decryptBytes); + } + + + /** + * 将base 64 code AES解密 + * @param encryptStr 待解密的base 64 code + * @param decryptKey 解密密钥 + * @return 解密后的string + * @throws Exception + */ + public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception { + if (StringUtils.isBlank(decryptKey)) { + return encryptStr; + } + return StringUtils.isEmpty(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey); + } + + /** + * 测试 + */ + public static void main(String[] args) throws Exception { + String randomString = RandomUtils.getRandomString(16); + String content = "hahhahaahhahni"; + System.out.println("加密前:" + content); + System.out.println("加密密钥和解密密钥:" + randomString); + String encrypt = aesEncrypt(content, randomString); + System.out.println("加密后:" + encrypt); + String decrypt = aesDecrypt(encrypt, randomString); + System.out.println("解密后:" + decrypt); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/Base64Utils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/Base64Utils.java new file mode 100644 index 0000000000..756689ed90 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/Base64Utils.java @@ -0,0 +1,53 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package com.anji.captcha.util; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public abstract class Base64Utils { + private static final Charset DEFAULT_CHARSET; + + public Base64Utils() { + } + + public static byte[] encode(byte[] src) { + return src.length == 0 ? src : Base64.getEncoder().encode(src); + } + + public static byte[] decode(byte[] src) { + return src.length == 0 ? src : Base64.getDecoder().decode(src); + } + + public static byte[] encodeUrlSafe(byte[] src) { + return src.length == 0 ? src : Base64.getUrlEncoder().encode(src); + } + + public static byte[] decodeUrlSafe(byte[] src) { + return src.length == 0 ? src : Base64.getUrlDecoder().decode(src); + } + + public static String encodeToString(byte[] src) { + return src.length == 0 ? "" : new String(encode(src), DEFAULT_CHARSET); + } + + public static byte[] decodeFromString(String src) { + return src.isEmpty() ? new byte[0] : decode(src.getBytes(DEFAULT_CHARSET)); + } + + public static String encodeToUrlSafeString(byte[] src) { + return new String(encodeUrlSafe(src), DEFAULT_CHARSET); + } + + public static byte[] decodeFromUrlSafeString(String src) { + return decodeUrlSafe(src.getBytes(DEFAULT_CHARSET)); + } + + static { + DEFAULT_CHARSET = StandardCharsets.UTF_8; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java new file mode 100644 index 0000000000..5f0fd450ca --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/CacheUtil.java @@ -0,0 +1,112 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.*; + +public final class CacheUtil { + private static final Logger logger = LoggerFactory.getLogger(CacheUtil.class); + + private static final Map CACHE_MAP = new ConcurrentHashMap(); + + /** + * 缓存最大个数 + */ + private static Integer CACHE_MAX_NUMBER = 1000; + + /** + * 初始化 + * @param cacheMaxNumber 缓存最大个数 + * @param second 定时任务 秒执行清除过期缓存 + */ + public static void init(int cacheMaxNumber, long second) { + CACHE_MAX_NUMBER = cacheMaxNumber; + if (second > 0L) { + /*Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + refresh(); + } + }, 0, second * 1000);*/ + 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(); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java new file mode 100644 index 0000000000..a3d82db41f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/FileCopyUtils.java @@ -0,0 +1,121 @@ +package com.anji.captcha.util; +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +import java.io.*; +import java.nio.file.Files; + +public abstract class FileCopyUtils { + public static final int BUFFER_SIZE = 4096; + + public FileCopyUtils() { + } + + public static int copy(File in, File out) throws IOException { + return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath())); + } + + public static void copy(byte[] in, File out) throws IOException { + copy((InputStream)(new ByteArrayInputStream(in)), (OutputStream)Files.newOutputStream(out.toPath())); + } + + public static byte[] copyToByteArray(File in) throws IOException { + return copyToByteArray(Files.newInputStream(in.toPath())); + } + + public static int copy(InputStream in, OutputStream out) throws IOException { + int var2; + try { + var2 = StreamUtils.copy(in, out); + } finally { + try { + in.close(); + } catch (IOException var12) { + } + + try { + out.close(); + } catch (IOException var11) { + } + + } + + return var2; + } + + public static void copy(byte[] in, OutputStream out) throws IOException { + try { + out.write(in); + } finally { + try { + out.close(); + } catch (IOException var8) { + } + + } + + } + + public static byte[] copyToByteArray(InputStream in) throws IOException { + if (in == null) { + return new byte[0]; + } else { + ByteArrayOutputStream out = new ByteArrayOutputStream(4096); + copy((InputStream)in, (OutputStream)out); + return out.toByteArray(); + } + } + + public static int copy(Reader in, Writer out) throws IOException { + try { + int byteCount = 0; + char[] buffer = new char[4096]; + + int bytesRead; + for(boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) { + out.write(buffer, 0, bytesRead); + } + + out.flush(); + int var5 = byteCount; + return var5; + } finally { + try { + in.close(); + } catch (IOException var15) { + } + + try { + out.close(); + } catch (IOException var14) { + } + + } + } + + public static void copy(String in, Writer out) throws IOException { + try { + out.write(in); + } finally { + try { + out.close(); + } catch (IOException var8) { + } + + } + + } + + public static String copyToString(Reader in) throws IOException { + if (in == null) { + return ""; + } else { + StringWriter out = new StringWriter(); + copy((Reader)in, (Writer)out); + return out.toString(); + } + } +} + diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java new file mode 100644 index 0000000000..9d9d88afe3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/ImageUtils.java @@ -0,0 +1,172 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.util; + +import com.anji.captcha.model.common.CaptchaBaseMapEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +public class ImageUtils { + private static Logger logger = LoggerFactory.getLogger(ImageUtils.class); + private static Map originalCacheMap = new ConcurrentHashMap(); //滑块底图 + private static Map slidingBlockCacheMap = new ConcurrentHashMap(); //滑块 + private static Map picClickCacheMap = new ConcurrentHashMap(); //点选文字 + private static Map fileNameMap = new ConcurrentHashMap<>(); + + public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) { + //滑动拼图 + if (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 originalMap, Map slidingBlockMap, Map picClickMap) { + originalCacheMap.putAll(originalMap); + slidingBlockCacheMap.putAll(slidingBlockMap); + picClickCacheMap.putAll(picClickMap); + fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0])); + fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0])); + fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0])); + 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 getResourcesImagesFile(String path) { + //默认提供六张底图 + Map imgMap = new HashMap<>(); + ClassLoader classLoader = ImageUtils.class.getClassLoader(); + for (int i = 1; i <= 6; i++) { + InputStream resourceAsStream = classLoader.getResourceAsStream(path.concat("/").concat(String.valueOf(i).concat(".png"))); + byte[] bytes = new byte[0]; + try { + bytes = FileCopyUtils.copyToByteArray(resourceAsStream); + } catch (IOException e) { + e.printStackTrace(); + } + String string = Base64Utils.encodeToString(bytes); + String filename = String.valueOf(i).concat(".png"); + imgMap.put(filename, string); + } + return imgMap; + } + + private static Map getImagesFile(String path) { + Map imgMap = new HashMap<>(); + File file = new File(path); + if (!file.exists()) { + return new HashMap<>(); + } + File[] files = file.listFiles(); + Arrays.stream(files).forEach(item -> { + try { + FileInputStream fileInputStream = new FileInputStream(item); + byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream); + String string = Base64Utils.encodeToString(bytes); + imgMap.put(item.getName(), string); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + return imgMap; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java new file mode 100644 index 0000000000..06ff7d2ea6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/JsonUtil.java @@ -0,0 +1,74 @@ +package com.anji.captcha.util; + +import com.anji.captcha.model.vo.PointVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 替换掉fastjson,自定义实现相关方法 + * note: 该实现不具有通用性,仅用于本项目。 + * + *@author WongBin + *@date 2021/1/8 + */ +public class JsonUtil { + private static Logger logger = LoggerFactory.getLogger(JsonUtil.class); + public static List parseArray(String text, Class clazz) { + if (text == null) { + return null; + } else { + String[] arr = text.replaceFirst("\\[","") + .replaceFirst("\\]","").split("\\}"); + List ret = new ArrayList<>(arr.length); + for (String s : arr) { + ret.add(parseObject(s,PointVO.class)); + } + return ret; + } + } + + public static PointVO parseObject(String text, Class clazz) { + if(text == null) { + return null; + } + /*if(!clazz.isAssignableFrom(PointVO.class)) { + throw new UnsupportedOperationException("不支持的输入类型:" + + clazz.getSimpleName()); + }*/ + try { + PointVO ret = clazz.newInstance(); + return ret.parse(text); + }catch (Exception ex){ + 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 list = (List)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()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java new file mode 100644 index 0000000000..be9824ab45 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/MD5Util.java @@ -0,0 +1,39 @@ +package com.anji.captcha.util; + +import java.security.MessageDigest; + +/** + * @Title: MD5工具类 + */ +public abstract class MD5Util { + /** + * 获取指定字符串的md5值 + * @param dataStr 明文 + * @return String + */ + public static String md5(String dataStr) { + try { + MessageDigest m = MessageDigest.getInstance("MD5"); + m.update(dataStr.getBytes("UTF8")); + byte[] s = m.digest(); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < s.length; i++) { + result.append(Integer.toHexString((0x000000FF & s[i]) | 0xFFFFFF00).substring(6)); + } + return result.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + /** + * 获取指定字符串的md5值, md5(str+salt) + * @param dataStr 明文 + * @return String + */ + public static String md5WithSalt(String dataStr,String salt) { + return md5(dataStr + salt); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java new file mode 100644 index 0000000000..48395d4e8a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/RandomUtils.java @@ -0,0 +1,95 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.util; + +import java.io.UnsupportedEncodingException; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + + +public class RandomUtils { + + /** + * 生成UUID + * + * @return + */ + public static String getUUID() { + String uuid = UUID.randomUUID().toString(); + uuid = uuid.replace("-", ""); + return uuid; + } + + /** + * 获取指定文字的随机中文 + * + * @return + */ + public static String getRandomHan(String hanZi) { + String ch = hanZi.charAt(new Random().nextInt(hanZi.length())) + ""; + return ch; + } + + public static int getRandomInt(int bound){ + return ThreadLocalRandom.current().nextInt(bound); + } + + /** + * 获取随机中文 + * + * @return + */ + public static String getRandomHan() { + String str = ""; + int highCode; + int lowCode; + + Random random = new Random(); + + highCode = (176 + Math.abs(random.nextInt(39))); //B0 + 0~39(16~55) 一级汉字所占区 + lowCode = (161 + Math.abs(random.nextInt(93))); //A1 + 0~93 每区有94个汉字 + + byte[] b = new byte[2]; + b[0] = (Integer.valueOf(highCode)).byteValue(); + b[1] = (Integer.valueOf(lowCode)).byteValue(); + + try { + str = new String(b, "GBK"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return str; + } + + /** + * 随机范围内数字 + * @param startNum + * @param endNum + * @return + */ + public static Integer getRandomInt(int startNum, int endNum) { + return ThreadLocalRandom.current().nextInt(endNum-startNum) + startNum; + } + + /** + * 获取随机字符串 + * @param length + * @return + */ + public static String getRandomString(int length){ + String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + Random random=new Random(); + StringBuffer sb=new StringBuffer(); + for(int i=0;i 0L) { + int bytesRead = in.read(buffer); + if (bytesRead == -1) { + break; + } + + if ((long)bytesRead <= bytesToCopy) { + out.write(buffer, 0, bytesRead); + bytesToCopy -= (long)bytesRead; + } else { + out.write(buffer, 0, (int)bytesToCopy); + bytesToCopy = 0L; + } + } + + return end - start + 1L - bytesToCopy; + } + } + + public static int drain(InputStream in) throws IOException { + byte[] buffer = new byte[4096]; + int byteCount; + int bytesRead; + for(byteCount = 0; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) { + } + + return byteCount; + } + + public static InputStream emptyInput() { + return new ByteArrayInputStream(EMPTY_CONTENT); + } + + public static InputStream nonClosing(InputStream in) { + return new NonClosingInputStream(in); + } + + public static OutputStream nonClosing(OutputStream out) { + return new NonClosingOutputStream(out); + } + + private static class NonClosingOutputStream extends FilterOutputStream { + public NonClosingOutputStream(OutputStream out) { + super(out); + } + + public void write(byte[] b, int off, int let) throws IOException { + this.out.write(b, off, let); + } + + public void close() throws IOException { + } + } + + private static class NonClosingInputStream extends FilterInputStream { + public NonClosingInputStream(InputStream in) { + super(in); + } + + public void close() throws IOException { + } + } +} + diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StringUtils.java b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StringUtils.java new file mode 100644 index 0000000000..e9286532f3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/com/anji/captcha/util/StringUtils.java @@ -0,0 +1,1742 @@ +/* + *Copyright © 2018 anji-plus + *安吉加加信息技术有限公司 + *http://www.anji-plus.com + *All rights reserved. + */ +package com.anji.captcha.util; + +public class StringUtils { + + /** + * The empty String "". + * @since 2.0 + */ + public static final String EMPTY = ""; + + /** + * Represents a failed index search. + * @since 2.1 + */ + public static final int INDEX_NOT_FOUND = -1; + + /** + *

The maximum size to which the padding constant(s) can expand.

+ */ + private static final int PAD_LIMIT = 8192; + + /** + *

StringUtils instances should NOT be constructed in + * standard programming. Instead, the class should be used as + * StringUtils.trim(" foo ");.

+ * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public StringUtils() { + super(); + } + + // Empty checks + //----------------------------------------------------------------------- + /** + *

Checks if a String is empty ("") or null.

+ * + *
+     * StringUtils.isEmpty(null)      = true
+     * StringUtils.isEmpty("")        = true
+     * StringUtils.isEmpty(" ")       = false
+     * StringUtils.isEmpty("bob")     = false
+     * StringUtils.isEmpty("  bob  ") = false
+     * 
+ * + *

NOTE: This method changed in Lang version 2.0. + * It no longer trims the String. + * That functionality is available in isBlank().

+ * + * @param str the String to check, may be null + * @return true if the String is empty or null + */ + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + /** + *

Checks if a String is not empty ("") and not null.

+ * + *
+     * StringUtils.isNotEmpty(null)      = false
+     * StringUtils.isNotEmpty("")        = false
+     * StringUtils.isNotEmpty(" ")       = true
+     * StringUtils.isNotEmpty("bob")     = true
+     * StringUtils.isNotEmpty("  bob  ") = true
+     * 
+ * + * @param str the String to check, may be null + * @return true if the String is not empty and not null + */ + public static boolean isNotEmpty(String str) { + return !StringUtils.isEmpty(str); + } + + /** + *

Checks if a String is whitespace, empty ("") or null.

+ * + *
+     * StringUtils.isBlank(null)      = true
+     * StringUtils.isBlank("null")      = true
+     * StringUtils.isBlank("")        = true
+     * StringUtils.isBlank(" ")       = true
+     * StringUtils.isBlank("bob")     = false
+     * StringUtils.isBlank("  bob  ") = false
+     * 
+ * + * @param str the String to check, may be null + * @return true if the String is null, empty or whitespace + * @since 2.0 + */ + public static boolean isBlank(String str) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return true; + } + if(equals("null", str.trim().toLowerCase())){ + return true; + } + for (int i = 0; i < strLen; i++) { + if ((Character.isWhitespace(str.charAt(i)) == false)) { + return false; + } + } + return true; + } + + /** + *

Checks if a String is not empty (""), not null and not whitespace only.

+ * + *
+     * StringUtils.isNotBlank(null)      = false
+     * StringUtils.isNotBlank("null")    = false
+     * StringUtils.isNotBlank("")        = false
+     * StringUtils.isNotBlank(" ")       = false
+     * StringUtils.isNotBlank("bob")     = true
+     * StringUtils.isNotBlank("  bob  ") = true
+     * 
+ * + * @param str the String to check, may be null + * @return true if the String is + * not empty and not null and not whitespace + * @since 2.0 + */ + public static boolean isNotBlank(String str) { + return !StringUtils.isBlank(str); + } + + // Trim + //----------------------------------------------------------------------- + /** + *

Removes control characters (char <= 32) from both + * ends of this String, handling null by returning + * an empty String ("").

+ * + *
+     * StringUtils.clean(null)          = ""
+     * StringUtils.clean("")            = ""
+     * StringUtils.clean("abc")         = "abc"
+     * StringUtils.clean("    abc    ") = "abc"
+     * StringUtils.clean("     ")       = ""
+     * 
+ * + * @see String#trim() + * @param str the String to clean, may be null + * @return the trimmed text, never null + * @deprecated Use the clearer named {@link #trimToEmpty(String)}. + * Method will be removed in Commons Lang 3.0. + */ + public static String clean(String str) { + return str == null ? EMPTY : str.trim(); + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String, handling null by returning + * null.

+ * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #strip(String)}.

+ * + *

To trim your choice of characters, use the + * {@link #strip(String, String)} methods.

+ * + *
+     * StringUtils.trim(null)          = null
+     * StringUtils.trim("")            = ""
+     * StringUtils.trim("     ")       = ""
+     * StringUtils.trim("abc")         = "abc"
+     * StringUtils.trim("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed string, null if null String input + */ + public static String trim(String str) { + return str == null ? null : str.trim(); + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning null if the String is + * empty ("") after the trim or if it is null. + * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToNull(String)}.

+ * + *
+     * StringUtils.trimToNull(null)          = null
+     * StringUtils.trimToNull("")            = null
+     * StringUtils.trimToNull("     ")       = null
+     * StringUtils.trimToNull("abc")         = "abc"
+     * StringUtils.trimToNull("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, + * null if only chars <= 32, empty or null String input + * @since 2.0 + */ + public static String trimToNull(String str) { + String ts = trim(str); + return isEmpty(ts) ? null : ts; + } + + /** + *

Removes control characters (char <= 32) from both + * ends of this String returning an empty String ("") if the String + * is empty ("") after the trim or if it is null. + * + *

The String is trimmed using {@link String#trim()}. + * Trim removes start and end characters <= 32. + * To strip whitespace use {@link #stripToEmpty(String)}.

+ * + *
+     * StringUtils.trimToEmpty(null)          = ""
+     * StringUtils.trimToEmpty("")            = ""
+     * StringUtils.trimToEmpty("     ")       = ""
+     * StringUtils.trimToEmpty("abc")         = "abc"
+     * StringUtils.trimToEmpty("    abc    ") = "abc"
+     * 
+ * + * @param str the String to be trimmed, may be null + * @return the trimmed String, or an empty String if null input + * @since 2.0 + */ + public static String trimToEmpty(String str) { + return str == null ? EMPTY : str.trim(); + } + + // Stripping + //----------------------------------------------------------------------- + /** + *

Strips whitespace from the start and end of a String.

+ * + *

This is similar to {@link #trim(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A null input String returns null.

+ * + *
+     * StringUtils.strip(null)     = null
+     * StringUtils.strip("")       = ""
+     * StringUtils.strip("   ")    = ""
+     * StringUtils.strip("abc")    = "abc"
+     * StringUtils.strip("  abc")  = "abc"
+     * StringUtils.strip("abc  ")  = "abc"
+     * StringUtils.strip(" abc ")  = "abc"
+     * StringUtils.strip(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to remove whitespace from, may be null + * @return the stripped String, null if null String input + */ + public static String strip(String str) { + return strip(str, null); + } + + /** + *

Strips whitespace from the start and end of a String returning + * null if the String is empty ("") after the strip.

+ * + *

This is similar to {@link #trimToNull(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripToNull(null)     = null
+     * StringUtils.stripToNull("")       = null
+     * StringUtils.stripToNull("   ")    = null
+     * StringUtils.stripToNull("abc")    = "abc"
+     * StringUtils.stripToNull("  abc")  = "abc"
+     * StringUtils.stripToNull("abc  ")  = "abc"
+     * StringUtils.stripToNull(" abc ")  = "abc"
+     * StringUtils.stripToNull(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the stripped String, + * null if whitespace, empty or null String input + * @since 2.0 + */ + public static String stripToNull(String str) { + if (str == null) { + return null; + } + str = strip(str, null); + return str.length() == 0 ? null : str; + } + + /** + *

Strips whitespace from the start and end of a String returning + * an empty String if null input.

+ * + *

This is similar to {@link #trimToEmpty(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripToEmpty(null)     = ""
+     * StringUtils.stripToEmpty("")       = ""
+     * StringUtils.stripToEmpty("   ")    = ""
+     * StringUtils.stripToEmpty("abc")    = "abc"
+     * StringUtils.stripToEmpty("  abc")  = "abc"
+     * StringUtils.stripToEmpty("abc  ")  = "abc"
+     * StringUtils.stripToEmpty(" abc ")  = "abc"
+     * StringUtils.stripToEmpty(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the trimmed String, or an empty String if null input + * @since 2.0 + */ + public static String stripToEmpty(String str) { + return str == null ? EMPTY : strip(str, null); + } + + /** + *

Strips any of a set of characters from the start and end of a String. + * This is similar to {@link String#trim()} but allows the characters + * to be stripped to be controlled.

+ * + *

A null input String returns null. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is null, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}. + * Alternatively use {@link #strip(String)}.

+ * + *
+     * StringUtils.strip(null, *)          = null
+     * StringUtils.strip("", *)            = ""
+     * StringUtils.strip("abc", null)      = "abc"
+     * StringUtils.strip("  abc", null)    = "abc"
+     * StringUtils.strip("abc  ", null)    = "abc"
+     * StringUtils.strip(" abc ", null)    = "abc"
+     * StringUtils.strip("  abcyx", "xyz") = "  abc"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, null if null String input + */ + public static String strip(String str, String stripChars) { + if (isEmpty(str)) { + return str; + } + str = stripStart(str, stripChars); + return stripEnd(str, stripChars); + } + + /** + *

Strips any of a set of characters from the start of a String.

+ * + *

A null input String returns null. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is null, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripStart(null, *)          = null
+     * StringUtils.stripStart("", *)            = ""
+     * StringUtils.stripStart("abc", "")        = "abc"
+     * StringUtils.stripStart("abc", null)      = "abc"
+     * StringUtils.stripStart("  abc", null)    = "abc"
+     * StringUtils.stripStart("abc  ", null)    = "abc  "
+     * StringUtils.stripStart(" abc ", null)    = "abc "
+     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, null if null String input + */ + public static String stripStart(String str, String stripChars) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + int start = 0; + if (stripChars == null) { + while ((start != strLen) && Character.isWhitespace(str.charAt(start))) { + start++; + } + } else if (stripChars.length() == 0) { + return str; + } else { + while ((start != strLen) && (stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND)) { + start++; + } + } + return str.substring(start); + } + + /** + *

Strips any of a set of characters from the end of a String.

+ * + *

A null input String returns null. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is null, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the set of characters to remove, null treated as whitespace + * @return the stripped String, null if null String input + */ + public static String stripEnd(String str, String stripChars) { + int end; + if (str == null || (end = str.length()) == 0) { + return str; + } + + if (stripChars == null) { + while ((end != 0) && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.length() == 0) { + return str; + } else { + while ((end != 0) && (stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND)) { + end--; + } + } + return str.substring(0, end); + } + + // StripAll + //----------------------------------------------------------------------- + /** + *

Strips whitespace from the start and end of every String in an array. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A new array is returned each time, except for length zero. + * A null array will return null. + * An empty array will return itself. + * A null array entry will be ignored.

+ * + *
+     * StringUtils.stripAll(null)             = null
+     * StringUtils.stripAll([])               = []
+     * StringUtils.stripAll(["abc", "  abc"]) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null])  = ["abc", null]
+     * 
+ * + * @param strs the array to remove whitespace from, may be null + * @return the stripped Strings, null if null array input + */ + public static String[] stripAll(String[] strs) { + return stripAll(strs, null); + } + + /** + *

Strips any of a set of characters from the start and end of every + * String in an array.

+ * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *

A new array is returned each time, except for length zero. + * A null array will return null. + * An empty array will return itself. + * A null array entry will be ignored. + * A null stripChars will strip whitespace as defined by + * {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripAll(null, *)                = null
+     * StringUtils.stripAll([], *)                  = []
+     * StringUtils.stripAll(["abc", "  abc"], null) = ["abc", "abc"]
+     * StringUtils.stripAll(["abc  ", null], null)  = ["abc", null]
+     * StringUtils.stripAll(["abc  ", null], "yz")  = ["abc  ", null]
+     * StringUtils.stripAll(["yabcz", null], "yz")  = ["abc", null]
+     * 
+ * + * @param strs the array to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped Strings, null if null array input + */ + public static String[] stripAll(String[] strs, String stripChars) { + int strsLen; + if (strs == null || (strsLen = strs.length) == 0) { + return strs; + } + String[] newArr = new String[strsLen]; + for (int i = 0; i < strsLen; i++) { + newArr[i] = strip(strs[i], stripChars); + } + return newArr; + } + + // Equals + //----------------------------------------------------------------------- + /** + *

Compares two Strings, returning true if they are equal.

+ * + *

nulls are handled without exceptions. Two null + * references are considered to be equal. The comparison is case sensitive.

+ * + *
+     * StringUtils.equals(null, null)   = true
+     * StringUtils.equals(null, "abc")  = false
+     * StringUtils.equals("abc", null)  = false
+     * StringUtils.equals("abc", "abc") = true
+     * StringUtils.equals("abc", "ABC") = false
+     * 
+ * + * @see String#equals(Object) + * @param str1 the first String, may be null + * @param str2 the second String, may be null + * @return true if the Strings are equal, case sensitive, or + * both null + */ + public static boolean equals(String str1, String str2) { + return str1 == null ? str2 == null : str1.equals(str2); + } + + /** + *

Compares two Strings, returning true if they are equal ignoring + * the case.

+ * + *

nulls are handled without exceptions. Two null + * references are considered equal. Comparison is case insensitive.

+ * + *
+     * StringUtils.equalsIgnoreCase(null, null)   = true
+     * StringUtils.equalsIgnoreCase(null, "abc")  = false
+     * StringUtils.equalsIgnoreCase("abc", null)  = false
+     * StringUtils.equalsIgnoreCase("abc", "abc") = true
+     * StringUtils.equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @see String#equalsIgnoreCase(String) + * @param str1 the first String, may be null + * @param str2 the second String, may be null + * @return true if the Strings are equal, case insensitive, or + * both null + */ + public static boolean equalsIgnoreCase(String str1, String str2) { + return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2); + } + + // IndexOf + //----------------------------------------------------------------------- + /** + *

Finds the first index within a String, handling null. + * This method uses {@link String#indexOf(int)}.

+ * + *

A null or empty ("") String will return INDEX_NOT_FOUND (-1).

+ * + *
+     * StringUtils.indexOf(null, *)         = -1
+     * StringUtils.indexOf("", *)           = -1
+     * StringUtils.indexOf("aabaabaa", 'a') = 0
+     * StringUtils.indexOf("aabaabaa", 'b') = 2
+     * 
+ * + * @param str the String to check, may be null + * @param searchChar the character to find + * @return the first index of the search character, + * -1 if no match or null string input + * @since 2.0 + */ + public static int indexOf(String str, char searchChar) { + if (isEmpty(str)) { + return INDEX_NOT_FOUND; + } + return str.indexOf(searchChar); + } + + /** + *

Finds the first index within a String from a start position, + * handling null. + * This method uses {@link String#indexOf(int, int)}.

+ * + *

A null or empty ("") String will return (INDEX_NOT_FOUND) -1. + * A negative start position is treated as zero. + * A start position greater than the string length returns -1.

+ * + *
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf("", *, *)            = -1
+     * StringUtils.indexOf("aabaabaa", 'b', 0)  = 2
+     * StringUtils.indexOf("aabaabaa", 'b', 3)  = 5
+     * StringUtils.indexOf("aabaabaa", 'b', 9)  = -1
+     * StringUtils.indexOf("aabaabaa", 'b', -1) = 2
+     * 
+ * + * @param str the String to check, may be null + * @param searchChar the character to find + * @param startPos the start position, negative treated as zero + * @return the first index of the search character, + * -1 if no match or null string input + * @since 2.0 + */ + public static int indexOf(String str, char searchChar, int startPos) { + if (isEmpty(str)) { + return INDEX_NOT_FOUND; + } + return str.indexOf(searchChar, startPos); + } + + /** + *

Finds the first index within a String, handling null. + * This method uses {@link String#indexOf(String)}.

+ * + *

A null String will return -1.

+ * + *
+     * StringUtils.indexOf(null, *)          = -1
+     * StringUtils.indexOf(*, null)          = -1
+     * StringUtils.indexOf("", "")           = 0
+     * StringUtils.indexOf("", *)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a")  = 0
+     * StringUtils.indexOf("aabaabaa", "b")  = 2
+     * StringUtils.indexOf("aabaabaa", "ab") = 1
+     * StringUtils.indexOf("aabaabaa", "")   = 0
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.0 + */ + public static int indexOf(String str, String searchStr) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return str.indexOf(searchStr); + } + + /** + *

Finds the n-th index within a String, handling null. + * This method uses {@link String#indexOf(String)}.

+ * + *

A null String will return -1.

+ * + *
+     * StringUtils.ordinalIndexOf(null, *, *)          = -1
+     * StringUtils.ordinalIndexOf(*, null, *)          = -1
+     * StringUtils.ordinalIndexOf("", "", *)           = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+     * StringUtils.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+     * StringUtils.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 1)   = 0
+     * StringUtils.ordinalIndexOf("aabaabaa", "", 2)   = 0
+     * 
+ * + *

Note that 'head(String str, int n)' may be implemented as:

+ * + *
+     *   str.substring(0, lastOrdinalIndexOf(str, "\n", n))
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param ordinal the n-th searchStr to find + * @return the n-th index of the search String, + * -1 (INDEX_NOT_FOUND) if no match or null string input + * @since 2.1 + */ + public static int ordinalIndexOf(String str, String searchStr, int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, false); + } + + /** + *

Finds the n-th index within a String, handling null. + * This method uses {@link String#indexOf(String)}.

+ * + *

A null String will return -1.

+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param ordinal the n-th searchStr to find + * @param lastIndex true if lastOrdinalIndexOf() otherwise false if ordinalIndexOf() + * @return the n-th index of the search String, + * -1 (INDEX_NOT_FOUND) if no match or null string input + */ + // Shared code between ordinalIndexOf(String,String,int) and lastOrdinalIndexOf(String,String,int) + private static int ordinalIndexOf(String str, String searchStr, int ordinal, boolean lastIndex) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return lastIndex ? str.length() : 0; + } + int found = 0; + int index = lastIndex ? str.length() : INDEX_NOT_FOUND; + do { + if(lastIndex) { + index = str.lastIndexOf(searchStr, index - 1); + } else { + index = str.indexOf(searchStr, index + 1); + } + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + /** + *

Finds the first index within a String, handling null. + * This method uses {@link String#indexOf(String, int)}.

+ * + *

A null String will return -1. + * A negative start position is treated as zero. + * An empty ("") search String always matches. + * A start position greater than the string length only matches + * an empty search String.

+ * + *
+     * StringUtils.indexOf(null, *, *)          = -1
+     * StringUtils.indexOf(*, null, *)          = -1
+     * StringUtils.indexOf("", "", 0)           = 0
+     * StringUtils.indexOf("", *, 0)            = -1 (except when * = "")
+     * StringUtils.indexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.indexOf("aabaabaa", "b", 0)  = 2
+     * StringUtils.indexOf("aabaabaa", "ab", 0) = 1
+     * StringUtils.indexOf("aabaabaa", "b", 3)  = 5
+     * StringUtils.indexOf("aabaabaa", "b", 9)  = -1
+     * StringUtils.indexOf("aabaabaa", "b", -1) = 2
+     * StringUtils.indexOf("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOf("abc", "", 9)        = 3
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.0 + */ + public static int indexOf(String str, String searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + // JDK1.2/JDK1.3 have a bug, when startPos > str.length for "", hence + if (searchStr.length() == 0 && startPos >= str.length()) { + return str.length(); + } + return str.indexOf(searchStr, startPos); + } + + /** + *

Case in-sensitive find of the first index within a String.

+ * + *

A null String will return -1. + * A negative start position is treated as zero. + * An empty ("") search String always matches. + * A start position greater than the string length only matches + * an empty search String.

+ * + *
+     * StringUtils.indexOfIgnoreCase(null, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null)          = -1
+     * StringUtils.indexOfIgnoreCase("", "")           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "a")  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "b")  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "ab") = 1
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.5 + */ + public static int indexOfIgnoreCase(String str, String searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + *

Case in-sensitive find of the first index within a String + * from the specified position.

+ * + *

A null String will return -1. + * A negative start position is treated as zero. + * An empty ("") search String always matches. + * A start position greater than the string length only matches + * an empty search String.

+ * + *
+     * StringUtils.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtils.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtils.indexOfIgnoreCase("abc", "", 9)        = 3
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.5 + */ + public static int indexOfIgnoreCase(String str, String searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos < 0) { + startPos = 0; + } + int endLimit = (str.length() - searchStr.length()) + 1; + if (startPos > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + for (int i = startPos; i < endLimit; i++) { + if (str.regionMatches(true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // LastIndexOf + //----------------------------------------------------------------------- + /** + *

Finds the last index within a String, handling null. + * This method uses {@link String#lastIndexOf(int)}.

+ * + *

A null or empty ("") String will return -1.

+ * + *
+     * StringUtils.lastIndexOf(null, *)         = -1
+     * StringUtils.lastIndexOf("", *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a') = 7
+     * StringUtils.lastIndexOf("aabaabaa", 'b') = 5
+     * 
+ * + * @param str the String to check, may be null + * @param searchChar the character to find + * @return the last index of the search character, + * -1 if no match or null string input + * @since 2.0 + */ + public static int lastIndexOf(String str, char searchChar) { + if (isEmpty(str)) { + return INDEX_NOT_FOUND; + } + return str.lastIndexOf(searchChar); + } + + /** + *

Finds the last index within a String from a start position, + * handling null. + * This method uses {@link String#lastIndexOf(int, int)}.

+ * + *

A null or empty ("") String will return -1. + * A negative start position returns -1. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf("", *,  *)           = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 4)  = 2
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 0)  = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'b', 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", 'b', -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", 'a', 0)  = 0
+     * 
+ * + * @param str the String to check, may be null + * @param searchChar the character to find + * @param startPos the start position + * @return the last index of the search character, + * -1 if no match or null string input + * @since 2.0 + */ + public static int lastIndexOf(String str, char searchChar, int startPos) { + if (isEmpty(str)) { + return INDEX_NOT_FOUND; + } + return str.lastIndexOf(searchChar, startPos); + } + + /** + *

Finds the last index within a String, handling null. + * This method uses {@link String#lastIndexOf(String)}.

+ * + *

A null String will return -1.

+ * + *
+     * StringUtils.lastIndexOf(null, *)          = -1
+     * StringUtils.lastIndexOf(*, null)          = -1
+     * StringUtils.lastIndexOf("", "")           = 0
+     * StringUtils.lastIndexOf("aabaabaa", "a")  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b")  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab") = 4
+     * StringUtils.lastIndexOf("aabaabaa", "")   = 8
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return the last index of the search String, + * -1 if no match or null string input + * @since 2.0 + */ + public static int lastIndexOf(String str, String searchStr) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return str.lastIndexOf(searchStr); + } + + /** + *

Finds the n-th last index within a String, handling null. + * This method uses {@link String#lastIndexOf(String)}.

+ * + *

A null String will return -1.

+ * + *
+     * StringUtils.lastOrdinalIndexOf(null, *, *)          = -1
+     * StringUtils.lastOrdinalIndexOf(*, null, *)          = -1
+     * StringUtils.lastOrdinalIndexOf("", "", *)           = 0
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 1)  = 7
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "a", 2)  = 6
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 1)  = 5
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "b", 2)  = 2
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 1) = 4
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "ab", 2) = 1
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 1)   = 8
+     * StringUtils.lastOrdinalIndexOf("aabaabaa", "", 2)   = 8
+     * 
+ * + *

Note that 'tail(String str, int n)' may be implemented as:

+ * + *
+     *   str.substring(lastOrdinalIndexOf(str, "\n", n) + 1)
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param ordinal the n-th last searchStr to find + * @return the n-th last index of the search String, + * -1 (INDEX_NOT_FOUND) if no match or null string input + * @since 2.5 + */ + public static int lastOrdinalIndexOf(String str, String searchStr, int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, true); + } + + /** + *

Finds the first index within a String, handling null. + * This method uses {@link String#lastIndexOf(String, int)}.

+ * + *

A null String will return -1. + * A negative start position returns -1. + * An empty ("") search String always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOf(null, *, *)          = -1
+     * StringUtils.lastIndexOf(*, null, *)          = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 8)  = 7
+     * StringUtils.lastIndexOf("aabaabaa", "b", 8)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "ab", 8) = 4
+     * StringUtils.lastIndexOf("aabaabaa", "b", 9)  = 5
+     * StringUtils.lastIndexOf("aabaabaa", "b", -1) = -1
+     * StringUtils.lastIndexOf("aabaabaa", "a", 0)  = 0
+     * StringUtils.lastIndexOf("aabaabaa", "b", 0)  = -1
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param startPos the start position, negative treated as zero + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.0 + */ + public static int lastIndexOf(String str, String searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return str.lastIndexOf(searchStr, startPos); + } + + /** + *

Case in-sensitive find of the last index within a String.

+ * + *

A null String will return -1. + * A negative start position returns -1. + * An empty ("") search String always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOfIgnoreCase(null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A")  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B")  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB") = 4
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.5 + */ + public static int lastIndexOfIgnoreCase(String str, String searchStr) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + *

Case in-sensitive find of the last index within a String + * from the specified position.

+ * + *

A null String will return -1. + * A negative start position returns -1. + * An empty ("") search String always matches unless the start position is negative. + * A start position greater than the string length searches the whole string.

+ * + *
+     * StringUtils.lastIndexOfIgnoreCase(null, *, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase(*, null, *)          = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8)  = 7
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9)  = 5
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", -1) = -1
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 0)  = -1
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @param startPos the start position + * @return the first index of the search String, + * -1 if no match or null string input + * @since 2.5 + */ + public static int lastIndexOfIgnoreCase(String str, String searchStr, int startPos) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (startPos > (str.length() - searchStr.length())) { + startPos = str.length() - searchStr.length(); + } + if (startPos < 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return startPos; + } + + for (int i = startPos; i >= 0; i--) { + if (str.regionMatches(true, i, searchStr, 0, searchStr.length())) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + // Contains + //----------------------------------------------------------------------- + /** + *

Checks if String contains a search character, handling null. + * This method uses {@link String#indexOf(int)}.

+ * + *

A null or empty ("") String will return false.

+ * + *
+     * StringUtils.contains(null, *)    = false
+     * StringUtils.contains("", *)      = false
+     * StringUtils.contains("abc", 'a') = true
+     * StringUtils.contains("abc", 'z') = false
+     * 
+ * + * @param str the String to check, may be null + * @param searchChar the character to find + * @return true if the String contains the search character, + * false if not or null string input + * @since 2.0 + */ + public static boolean contains(String str, char searchChar) { + if (isEmpty(str)) { + return false; + } + return str.indexOf(searchChar) >= 0; + } + + /** + *

Checks if String contains a search String, handling null. + * This method uses {@link String#indexOf(String)}.

+ * + *

A null String will return false.

+ * + *
+     * StringUtils.contains(null, *)     = false
+     * StringUtils.contains(*, null)     = false
+     * StringUtils.contains("", "")      = true
+     * StringUtils.contains("abc", "")   = true
+     * StringUtils.contains("abc", "a")  = true
+     * StringUtils.contains("abc", "z")  = false
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return true if the String contains the search String, + * false if not or null string input + * @since 2.0 + */ + public static boolean contains(String str, String searchStr) { + if (str == null || searchStr == null) { + return false; + } + return str.indexOf(searchStr) >= 0; + } + + /** + *

Checks if String contains a search String irrespective of case, + * handling null. Case-insensitivity is defined as by + * {@link String#equalsIgnoreCase(String)}. + * + *

A null String will return false.

+ * + *
+     * StringUtils.contains(null, *) = false
+     * StringUtils.contains(*, null) = false
+     * StringUtils.contains("", "") = true
+     * StringUtils.contains("abc", "") = true
+     * StringUtils.contains("abc", "a") = true
+     * StringUtils.contains("abc", "z") = false
+     * StringUtils.contains("abc", "A") = true
+     * StringUtils.contains("abc", "Z") = false
+     * 
+ * + * @param str the String to check, may be null + * @param searchStr the String to find, may be null + * @return true if the String contains the search String irrespective of + * case or false if not or null string input + */ + public static boolean containsIgnoreCase(String str, String searchStr) { + if (str == null || searchStr == null) { + return false; + } + int len = searchStr.length(); + int max = str.length() - len; + for (int i = 0; i <= max; i++) { + if (str.regionMatches(true, i, searchStr, 0, len)) { + return true; + } + } + return false; + } + + + // IndexOfAny strings + //----------------------------------------------------------------------- + /** + *

Find the first index of any of a set of potential substrings.

+ * + *

A null String will return -1. + * A null or zero length search array will return -1. + * A null search array entry will be ignored, but a search + * array containing "" will return 0 if str is not + * null. This method uses {@link String#indexOf(String)}.

+ * + *
+     * StringUtils.indexOfAny(null, *)                     = -1
+     * StringUtils.indexOfAny(*, null)                     = -1
+     * StringUtils.indexOfAny(*, [])                       = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["ab","cd"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["cd","ab"])   = 2
+     * StringUtils.indexOfAny("zzabyycdxx", ["mn","op"])   = -1
+     * StringUtils.indexOfAny("zzabyycdxx", ["zab","aby"]) = 1
+     * StringUtils.indexOfAny("zzabyycdxx", [""])          = 0
+     * StringUtils.indexOfAny("", [""])                    = 0
+     * StringUtils.indexOfAny("", ["a"])                   = -1
+     * 
+ * + * @param str the String to check, may be null + * @param searchStrs the Strings to search for, may be null + * @return the first index of any of the searchStrs in str, -1 if no match + */ + public static int indexOfAny(String str, String[] searchStrs) { + if ((str == null) || (searchStrs == null)) { + return INDEX_NOT_FOUND; + } + int sz = searchStrs.length; + + // String's can't have a MAX_VALUEth index. + int ret = Integer.MAX_VALUE; + + int tmp = 0; + for (int i = 0; i < sz; i++) { + String search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = str.indexOf(search); + if (tmp == INDEX_NOT_FOUND) { + continue; + } + + if (tmp < ret) { + ret = tmp; + } + } + + return (ret == Integer.MAX_VALUE) ? INDEX_NOT_FOUND : ret; + } + + /** + *

Find the latest index of any of a set of potential substrings.

+ * + *

A null String will return -1. + * A null search array will return -1. + * A null or zero length search array entry will be ignored, + * but a search array containing "" will return the length of str + * if str is not null. This method uses {@link String#indexOf(String)}

+ * + *
+     * StringUtils.lastIndexOfAny(null, *)                   = -1
+     * StringUtils.lastIndexOfAny(*, null)                   = -1
+     * StringUtils.lastIndexOfAny(*, [])                     = -1
+     * StringUtils.lastIndexOfAny(*, [null])                 = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["ab","cd"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["cd","ab"]) = 6
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn","op"]) = -1
+     * StringUtils.lastIndexOfAny("zzabyycdxx", ["mn",""])   = 10
+     * 
+ * + * @param str the String to check, may be null + * @param searchStrs the Strings to search for, may be null + * @return the last index of any of the Strings, -1 if no match + */ + public static int lastIndexOfAny(String str, String[] searchStrs) { + if ((str == null) || (searchStrs == null)) { + return INDEX_NOT_FOUND; + } + int sz = searchStrs.length; + int ret = INDEX_NOT_FOUND; + int tmp = 0; + for (int i = 0; i < sz; i++) { + String search = searchStrs[i]; + if (search == null) { + continue; + } + tmp = str.lastIndexOf(search); + if (tmp > ret) { + ret = tmp; + } + } + return ret; + } + + // Substring + //----------------------------------------------------------------------- + /** + *

Gets a substring from the specified String avoiding exceptions.

+ * + *

A negative start position can be used to start n + * characters from the end of the String.

+ * + *

A null String will return null. + * An empty ("") String will return "".

+ * + *
+     * StringUtils.substring(null, *)   = null
+     * StringUtils.substring("", *)     = ""
+     * StringUtils.substring("abc", 0)  = "abc"
+     * StringUtils.substring("abc", 2)  = "c"
+     * StringUtils.substring("abc", 4)  = ""
+     * StringUtils.substring("abc", -2) = "bc"
+     * StringUtils.substring("abc", -4) = "abc"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @return substring from start position, null if null String input + */ + public static String substring(String str, int start) { + if (str == null) { + return null; + } + + // handle negatives, which means last n characters + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + if (start < 0) { + start = 0; + } + if (start > str.length()) { + return EMPTY; + } + + return str.substring(start); + } + + /** + *

Gets a substring from the specified String avoiding exceptions.

+ * + *

A negative start position can be used to start/end n + * characters from the end of the String.

+ * + *

The returned substring starts with the character in the start + * position and ends before the end position. All position counting is + * zero-based -- i.e., to start at the beginning of the string use + * start = 0. Negative start and end positions can be used to + * specify offsets relative to the end of the String.

+ * + *

If start is not strictly to the left of end, "" + * is returned.

+ * + *
+     * StringUtils.substring(null, *, *)    = null
+     * StringUtils.substring("", * ,  *)    = "";
+     * StringUtils.substring("abc", 0, 2)   = "ab"
+     * StringUtils.substring("abc", 2, 0)   = ""
+     * StringUtils.substring("abc", 2, 4)   = "c"
+     * StringUtils.substring("abc", 4, 6)   = ""
+     * StringUtils.substring("abc", 2, 2)   = ""
+     * StringUtils.substring("abc", -2, -1) = "b"
+     * StringUtils.substring("abc", -4, 2)  = "ab"
+     * 
+ * + * @param str the String to get the substring from, may be null + * @param start the position to start from, negative means + * count back from the end of the String by this many characters + * @param end the position to end at (exclusive), negative means + * count back from the end of the String by this many characters + * @return substring from start position to end positon, + * null if null String input + */ + public static String substring(String str, int start, int end) { + if (str == null) { + return null; + } + + // handle negatives + if (end < 0) { + end = str.length() + end; // remember end is negative + } + if (start < 0) { + start = str.length() + start; // remember start is negative + } + + // check length next + if (end > str.length()) { + end = str.length(); + } + + // if start is greater than end, return "" + if (start > end) { + return EMPTY; + } + + if (start < 0) { + start = 0; + } + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + // Left/Right/Mid + //----------------------------------------------------------------------- + /** + *

Gets the leftmost len characters of a String.

+ * + *

If len characters are not available, or the + * String is null, the String will be returned without + * an exception. An empty String is returned if len is negative.

+ * + *
+     * StringUtils.left(null, *)    = null
+     * StringUtils.left(*, -ve)     = ""
+     * StringUtils.left("", *)      = ""
+     * StringUtils.left("abc", 0)   = ""
+     * StringUtils.left("abc", 2)   = "ab"
+     * StringUtils.left("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the leftmost characters from, may be null + * @param len the length of the required String + * @return the leftmost characters, null if null String input + */ + public static String left(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(0, len); + } + + /** + *

Gets the rightmost len characters of a String.

+ * + *

If len characters are not available, or the String + * is null, the String will be returned without an + * an exception. An empty String is returned if len is negative.

+ * + *
+     * StringUtils.right(null, *)    = null
+     * StringUtils.right(*, -ve)     = ""
+     * StringUtils.right("", *)      = ""
+     * StringUtils.right("abc", 0)   = ""
+     * StringUtils.right("abc", 2)   = "bc"
+     * StringUtils.right("abc", 4)   = "abc"
+     * 
+ * + * @param str the String to get the rightmost characters from, may be null + * @param len the length of the required String + * @return the rightmost characters, null if null String input + */ + public static String right(String str, int len) { + if (str == null) { + return null; + } + if (len < 0) { + return EMPTY; + } + if (str.length() <= len) { + return str; + } + return str.substring(str.length() - len); + } + + /** + *

Gets len characters from the middle of a String.

+ * + *

If len characters are not available, the remainder + * of the String will be returned without an exception. If the + * String is null, null will be returned. + * An empty String is returned if len is negative or exceeds the + * length of str.

+ * + *
+     * StringUtils.mid(null, *, *)    = null
+     * StringUtils.mid(*, *, -ve)     = ""
+     * StringUtils.mid("", 0, *)      = ""
+     * StringUtils.mid("abc", 0, 2)   = "ab"
+     * StringUtils.mid("abc", 0, 4)   = "abc"
+     * StringUtils.mid("abc", 2, 4)   = "c"
+     * StringUtils.mid("abc", 4, 2)   = ""
+     * StringUtils.mid("abc", -2, 2)  = "ab"
+     * 
+ * + * @param str the String to get the characters from, may be null + * @param pos the position to start from, negative treated as zero + * @param len the length of the required String + * @return the middle characters, null if null String input + */ + public static String mid(String str, int pos, int len) { + if (str == null) { + return null; + } + if (len < 0 || pos > str.length()) { + return EMPTY; + } + if (pos < 0) { + pos = 0; + } + if (str.length() <= (pos + len)) { + return str.substring(pos); + } + return str.substring(pos, pos + len); + } + + // SubStringAfter/SubStringBefore + //----------------------------------------------------------------------- + /** + *

Gets the substring before the first occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * A null separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+     * StringUtils.substringBefore(null, *)      = null
+     * StringUtils.substringBefore("", *)        = ""
+     * StringUtils.substringBefore("abc", "a")   = ""
+     * StringUtils.substringBefore("abcba", "b") = "a"
+     * StringUtils.substringBefore("abc", "c")   = "ab"
+     * StringUtils.substringBefore("abc", "d")   = "abc"
+     * StringUtils.substringBefore("abc", "")    = ""
+     * StringUtils.substringBefore("abc", null)  = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the first occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringBefore(String str, String separator) { + if (isEmpty(str) || separator == null) { + return str; + } + if (separator.length() == 0) { + return EMPTY; + } + int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the first occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * A null separator will return the empty string if the + * input string is not null.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+     * StringUtils.substringAfter(null, *)      = null
+     * StringUtils.substringAfter("", *)        = ""
+     * StringUtils.substringAfter(*, null)      = ""
+     * StringUtils.substringAfter("abc", "a")   = "bc"
+     * StringUtils.substringAfter("abcba", "b") = "cba"
+     * StringUtils.substringAfter("abc", "c")   = ""
+     * StringUtils.substringAfter("abc", "d")   = ""
+     * StringUtils.substringAfter("abc", "")    = "abc"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the first occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringAfter(String str, String separator) { + if (isEmpty(str)) { + return str; + } + if (separator == null) { + return EMPTY; + } + int pos = str.indexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + *

Gets the substring before the last occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * An empty or null separator will return the input string.

+ * + *

If nothing is found, the string input is returned.

+ * + *
+     * StringUtils.substringBeforeLast(null, *)      = null
+     * StringUtils.substringBeforeLast("", *)        = ""
+     * StringUtils.substringBeforeLast("abcba", "b") = "abc"
+     * StringUtils.substringBeforeLast("abc", "c")   = "ab"
+     * StringUtils.substringBeforeLast("a", "a")     = ""
+     * StringUtils.substringBeforeLast("a", "z")     = "a"
+     * StringUtils.substringBeforeLast("a", null)    = "a"
+     * StringUtils.substringBeforeLast("a", "")      = "a"
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring before the last occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringBeforeLast(String str, String separator) { + if (isEmpty(str) || isEmpty(separator)) { + return str; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + *

Gets the substring after the last occurrence of a separator. + * The separator is not returned.

+ * + *

A null string input will return null. + * An empty ("") string input will return the empty string. + * An empty or null separator will return the empty string if + * the input string is not null.

+ * + *

If nothing is found, the empty string is returned.

+ * + *
+     * StringUtils.substringAfterLast(null, *)      = null
+     * StringUtils.substringAfterLast("", *)        = ""
+     * StringUtils.substringAfterLast(*, "")        = ""
+     * StringUtils.substringAfterLast(*, null)      = ""
+     * StringUtils.substringAfterLast("abc", "a")   = "bc"
+     * StringUtils.substringAfterLast("abcba", "b") = "a"
+     * StringUtils.substringAfterLast("abc", "c")   = ""
+     * StringUtils.substringAfterLast("a", "a")     = ""
+     * StringUtils.substringAfterLast("a", "z")     = ""
+     * 
+ * + * @param str the String to get a substring from, may be null + * @param separator the String to search for, may be null + * @return the substring after the last occurrence of the separator, + * null if null String input + * @since 2.0 + */ + public static String substringAfterLast(String str, String separator) { + if (isEmpty(str)) { + return str; + } + if (isEmpty(separator)) { + return EMPTY; + } + int pos = str.lastIndexOf(separator); + if (pos == INDEX_NOT_FOUND || pos == (str.length() - separator.length())) { + return EMPTY; + } + return str.substring(pos + separator.length()); + } + + // Substring between + //----------------------------------------------------------------------- + /** + *

Gets the String that is nested in between two instances of the + * same String.

+ * + *

A null input String returns null. + * A null tag returns null.

+ * + *
+     * StringUtils.substringBetween(null, *)            = null
+     * StringUtils.substringBetween("", "")             = ""
+     * StringUtils.substringBetween("", "tag")          = null
+     * StringUtils.substringBetween("tagabctag", null)  = null
+     * StringUtils.substringBetween("tagabctag", "")    = ""
+     * StringUtils.substringBetween("tagabctag", "tag") = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param tag the String before and after the substring, may be null + * @return the substring, null if no match + * @since 2.0 + */ + public static String substringBetween(String str, String tag) { + return substringBetween(str, tag, tag); + } + + /** + *

Gets the String that is nested in between two Strings. + * Only the first match is returned.

+ * + *

A null input String returns null. + * A null open/close returns null (no match). + * An empty ("") open and close returns an empty string.

+ * + *
+     * StringUtils.substringBetween("wx[b]yz", "[", "]") = "b"
+     * StringUtils.substringBetween(null, *, *)          = null
+     * StringUtils.substringBetween(*, null, *)          = null
+     * StringUtils.substringBetween(*, *, null)          = null
+     * StringUtils.substringBetween("", "", "")          = ""
+     * StringUtils.substringBetween("", "", "]")         = null
+     * StringUtils.substringBetween("", "[", "]")        = null
+     * StringUtils.substringBetween("yabcz", "", "")     = ""
+     * StringUtils.substringBetween("yabcz", "y", "z")   = "abc"
+     * StringUtils.substringBetween("yabczyabcz", "y", "z")   = "abc"
+     * 
+ * + * @param str the String containing the substring, may be null + * @param open the String before the substring, may be null + * @param close the String after the substring, may be null + * @return the substring, null if no match + * @since 2.0 + */ + public static String substringBetween(String str, String open, String close) { + if (str == null || open == null || close == null) { + return null; + } + int start = str.indexOf(open); + if (start != INDEX_NOT_FOUND) { + int end = str.indexOf(close, start + open.length()); + if (end != INDEX_NOT_FOUND) { + return str.substring(start + open.length(), end); + } + } + return null; + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService new file mode 100644 index 0000000000..f31fbb43bb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.anji.captcha.service.CaptchaService @@ -0,0 +1,3 @@ +com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl +com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl +com.anji.captcha.service.impl.DefaultCaptchaServiceImpl \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 8411d2cc34..12cf6229c8 100644 --- a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ +com.anji.captcha.config.AjCaptchaAutoConfiguration cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png new file mode 100644 index 0000000000..022aabf93c Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/1.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png new file mode 100644 index 0000000000..914908e897 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/2.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png new file mode 100644 index 0000000000..f0f3ce581b Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/3.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png new file mode 100644 index 0000000000..c5697f3cb4 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/4.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png new file mode 100644 index 0000000000..e29e7a3c1b Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/5.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png new file mode 100644 index 0000000000..2425f412df Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/6.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png new file mode 100644 index 0000000000..5ea54d482a Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/original/bg8.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png new file mode 100644 index 0000000000..1905026606 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/1.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png new file mode 100644 index 0000000000..b1482d48b4 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/2.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png new file mode 100644 index 0000000000..cdbb0b18c4 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/3.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png new file mode 100644 index 0000000000..bc69c96224 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/4.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png new file mode 100644 index 0000000000..0080a54650 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/5.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png new file mode 100644 index 0000000000..b07c3b4046 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/jigsaw/slidingBlock/6.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png new file mode 100644 index 0000000000..50dfe28ef6 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/1.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png new file mode 100644 index 0000000000..15b38ad27b Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/2.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png new file mode 100644 index 0000000000..e2e487bd41 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/3.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png new file mode 100644 index 0000000000..c34baa4048 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/4.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png new file mode 100644 index 0000000000..0b3d11a27d Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/5.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png new file mode 100644 index 0000000000..67797a11d6 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/6.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png new file mode 100644 index 0000000000..c99fbcb035 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg10.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png new file mode 100644 index 0000000000..6a951d326c Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg11.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png new file mode 100644 index 0000000000..a38ada5042 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg12.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png new file mode 100644 index 0000000000..07af86a866 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg13.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png new file mode 100644 index 0000000000..95593759d0 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg14.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png new file mode 100644 index 0000000000..cb1ebb63ed Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg15.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png new file mode 100644 index 0000000000..106b4562bf Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg16.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png new file mode 100644 index 0000000000..bcdbe76551 Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg17.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png new file mode 100644 index 0000000000..ae94e09cff Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg18.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png new file mode 100644 index 0000000000..bef9318b5c Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg19.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png new file mode 100644 index 0000000000..36cfbdec6e Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/defaultImages/pic-click/bg20.png differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf new file mode 100644 index 0000000000..f84e9feb3c Binary files /dev/null and b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/WenQuanZhengHei.ttf differ diff --git a/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt new file mode 100644 index 0000000000..719f68f0be --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-captcha/src/main/resources/fonts/license.txt @@ -0,0 +1,55 @@ +文泉驿是一个开源汉字字体项目 + +由旅美学者房骞骞(FangQ) + +于2004年10月创建 + +集中力量解决GNU/Linux + +高质量中文字体匮乏的状况 + +目前,文泉驿已经开发并发布了 + +第一个完整覆盖GB18030汉字 + +(包含27000多个汉字) + +的多规格点阵汉字字型文件 + +第一个覆盖GBK字符集的 + +开源矢量字型文件(文泉驿正黑) + +并提供了目前包含字符数目最多的 + +开源字体——GNU Unifont——中 + +绝大多数中日韩文相关的符号 + +这些字型文件已经逐渐成为 + +主流Linux/Unix发行版 + +中文桌面的首选中文字体 + +目前Ubuntu、Fedora、Slackware + +Magic Linux、CDLinux + +使用文泉驿作为默认中文字体 + +Debian、Gentoo、Mandriva + +ArchLinux、Frugalware + +则提供了官方源支持 + +而FreeBSD则在其ports中有提供 + +所以,今天我们所要分享的就是 + +文泉驿正黑体 + +可在Linux/UNIX,Windows + +Mac OS和嵌入式操作系统中使用 \ No newline at end of file