diff --git a/.gitignore b/.gitignore index 40c92d4d2b5dd6a891c13617b53903b99820eb30..fdfdab7028a7395759056fc4a8eaba8d692e1902 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,27 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### .idea -target +*.iws *.iml -/view/flutter/**/gen/* -**/*.log -dist +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### Mac files ### +.DS_Store diff --git a/core/captcha-solon-plugin/pom.xml b/core/captcha-solon-plugin/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5300d62583e9a5152cbd7c60a23d49752670138c --- /dev/null +++ b/core/captcha-solon-plugin/pom.xml @@ -0,0 +1,156 @@ + + + 4.0.0 + + com.anji-plus + captcha-solon-plugin + 1.3.0 + + captcha-spring-boot-starter + anji-plus captcha captcha-spring-boot-starter + https://github.com/anji-plus/captcha + jar + + + 1.8 + 1.5.27 + 1.7.25 + + + + + com.anji-plus + captcha + 1.3.0 + + + + org.noear + solon + ${solon.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + scm:git:git://github.com/anji-plus/OSSRH-56732.git + scm:git:ssh://github.com/anji-plus/OSSRH-56732.git + https://github.com/anji-plus/OSSRH-56732/tree/master + + + + + develop.anji-plus.com + MS@anji-plus.com + https://github.com/anji-plus + + + noear + noear@live.cn + https://github.com/noear + + + + + + snapshots + + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4 + + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + + + UTF-8 + -Xdoclint:none + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + + + releases + https://oss.sonatype.org/content/repositories/snapshots + + + releases + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + + \ No newline at end of file diff --git a/core/captcha-solon-plugin/src/main/java/com/anji/captcha/XPluginImp.java b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/XPluginImp.java new file mode 100644 index 0000000000000000000000000000000000000000..2590334e23e1a57ba15228bebcbe55f9d892a74c --- /dev/null +++ b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/XPluginImp.java @@ -0,0 +1,22 @@ +package com.anji.captcha; + +import com.anji.captcha.config.AjCaptchaServiceConfiguration; +import com.anji.captcha.config.AjCaptchaStorageConfiguration; +import com.anji.captcha.properties.AjCaptchaProperties; +import org.noear.solon.SolonApp; +import org.noear.solon.core.Plugin; +import com.anji.captcha.controller.CaptchaController; + +/** + * @author noear + * @since 1.5 + */ +public class XPluginImp implements Plugin { + @Override + public void start(SolonApp app) { + app.beanMake(AjCaptchaProperties.class); + app.beanMake(AjCaptchaServiceConfiguration.class); + app.beanMake(AjCaptchaStorageConfiguration.class); + app.beanMake(CaptchaController.class); + } +} diff --git a/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaServiceConfiguration.java b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaServiceConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..3b7f25284ab2a5dbd581c1823e342572b9b82636 --- /dev/null +++ b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaServiceConfiguration.java @@ -0,0 +1,99 @@ +package com.anji.captcha.config; + +import com.anji.captcha.properties.AjCaptchaProperties; +import com.anji.captcha.service.CaptchaService; +import com.anji.captcha.service.impl.CaptchaServiceFactory; +import com.anji.captcha.util.Base64Utils; +import com.anji.captcha.util.FileCopyUtils; +import com.anji.captcha.util.ImageUtils; +import com.anji.captcha.util.StringUtils; +import org.noear.solon.Utils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.util.ResourceScaner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URL; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author noear + * @since 1.5 + */ +@Configuration +public class AjCaptchaServiceConfiguration { + private static Logger logger = LoggerFactory.getLogger(AjCaptchaServiceConfiguration.class); + + public AjCaptchaServiceConfiguration() { + } + + @Bean + public CaptchaService captchaService(AjCaptchaProperties prop) { + logger.info("自定义配置项:{}", prop.toString()); + + Properties config = new Properties(); + config.put("captcha.cacheType", prop.getCacheType().name()); + config.put("captcha.water.mark", prop.getWaterMark()); + config.put("captcha.font.type", prop.getFontType()); + config.put("captcha.type", prop.getType().getCodeValue()); + config.put("captcha.interference.options", prop.getInterferenceOptions()); + config.put("captcha.captchaOriginalPath.jigsaw", prop.getJigsaw()); + config.put("captcha.captchaOriginalPath.pic-click", prop.getPicClick()); + config.put("captcha.slip.offset", prop.getSlipOffset()); + config.put("captcha.aes.status", String.valueOf(prop.getAesStatus())); + config.put("captcha.water.font", prop.getWaterFont()); + config.put("captcha.cache.number", prop.getCacheNumber()); + config.put("captcha.timing.clear", prop.getTimingClear()); + config.put("captcha.history.data.clear.enable", prop.isHistoryDataClearEnable() ? "1" : "0"); + config.put("captcha.req.frequency.limit.enable", prop.getReqFrequencyLimitEnable() ? "1" : "0"); + config.put("captcha.req.get.lock.limit", prop.getReqGetLockLimit() + ""); + config.put("captcha.req.get.lock.seconds", prop.getReqGetLockSeconds() + ""); + config.put("captcha.req.get.minute.limit", prop.getReqGetMinuteLimit() + ""); + config.put("captcha.req.check.minute.limit", prop.getReqCheckMinuteLimit() + ""); + config.put("captcha.req.verify.minute.limit", prop.getReqVerifyMinuteLimit() + ""); + if (StringUtils.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:") || StringUtils.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:")) { + config.put("captcha.init.original", "true"); + initializeBaseMap(prop.getJigsaw(), prop.getPicClick()); + } + + CaptchaService s = CaptchaServiceFactory.getInstance(config); + return s; + } + + private static void initializeBaseMap(String jigsaw, String picClick) { + jigsaw = jigsaw.substring(10); + picClick = picClick.substring(10); + + Map originalMap = getResourcesImagesFile(jigsaw + "/original"); + Map slidingBlockMap = getResourcesImagesFile(jigsaw + "/slidingBlock"); + Map picClickMap = getResourcesImagesFile(picClick + ""); + + ImageUtils.cacheBootImage(originalMap, slidingBlockMap, picClickMap); + } + + public static Map getResourcesImagesFile(String path) { + Map imgMap = new HashMap(); + + //todo: 这里可能会有问题 + try { + List resources = ResourceScaner.scan(path, n -> n.endsWith(".png")) + .stream() + .map(k -> Utils.getResource(k)) + .collect(Collectors.toList()); + + for (URL resource : resources) { + byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream()); + String string = Base64Utils.encodeToString(bytes); + String filename = new File(resource.getFile()).getName(); + imgMap.put(filename, string); + } + } catch (Exception var11) { + var11.printStackTrace(); + } + + return imgMap; + } +} diff --git a/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaStorageConfiguration.java b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaStorageConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..3988f1c3acd0b26fab6dd9ec1a300e9b6849b82b --- /dev/null +++ b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/config/AjCaptchaStorageConfiguration.java @@ -0,0 +1,22 @@ +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.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Configuration; + +/** + * @author noear + * @since 1.5 + */ +@Configuration +public class AjCaptchaStorageConfiguration { + public AjCaptchaStorageConfiguration() { + } + + @Bean("AjCaptchaCacheService") + public CaptchaCacheService captchaCacheService(AjCaptchaProperties ajCaptchaProperties) { + return CaptchaServiceFactory.getCache(ajCaptchaProperties.getCacheType().name()); + } +} diff --git a/core/captcha-solon-plugin/src/main/java/com/anji/captcha/controller/CaptchaController.java b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/controller/CaptchaController.java new file mode 100644 index 0000000000000000000000000000000000000000..12167dce83bc2486201177305f1fc2fee845ba4b --- /dev/null +++ b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/controller/CaptchaController.java @@ -0,0 +1,42 @@ +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 org.noear.solon.annotation.*; +import org.noear.solon.core.handle.Context; + +/** + * @author noear + * @since 1.5 + */ +@Controller +@Mapping("/captcha") +public class CaptchaController { + @Inject + private CaptchaService captchaService; + + @Post + @Mapping("/get") + public ResponseModel get(CaptchaVO data, Context request) { + assert request.realIp() != null; + + data.setBrowserInfo(getRemoteId(request)); + return this.captchaService.get(data); + } + + @Post + @Mapping("/check") + public ResponseModel check(CaptchaVO data, Context request) { + data.setBrowserInfo(getRemoteId(request)); + return this.captchaService.check(data); + } + + public ResponseModel verify( CaptchaVO data, Context request) { + return this.captchaService.verification(data); + } + + public static final String getRemoteId(Context ctx) { + return ctx.realIp() + ctx.userAgent(); + } +} diff --git a/core/captcha-solon-plugin/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..49e8185919b1ca6551041f5bf76087add99fa299 --- /dev/null +++ b/core/captcha-solon-plugin/src/main/java/com/anji/captcha/properties/AjCaptchaProperties.java @@ -0,0 +1,229 @@ +package com.anji.captcha.properties; + +import com.anji.captcha.model.common.CaptchaTypeEnum; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * @author noear + * @since 1.5 + */ +@Inject("${aj.captcha}") +@Configuration +public class AjCaptchaProperties { + public static final String PREFIX = "aj.captcha"; + private CaptchaTypeEnum type; + private String jigsaw; + private String picClick; + private String waterMark; + private String waterFont; + private String fontType; + private String slipOffset; + private Boolean aesStatus; + private String interferenceOptions; + private String cacheNumber; + private String timingClear; + private StorageType cacheType; + private boolean historyDataClearEnable; + private boolean reqFrequencyLimitEnable; + private int reqGetLockLimit; + private int reqGetLockSeconds; + private int reqGetMinuteLimit; + private int reqCheckMinuteLimit; + private int reqVerifyMinuteLimit; + + public AjCaptchaProperties() { + this.type = CaptchaTypeEnum.DEFAULT; + this.jigsaw = ""; + this.picClick = ""; + this.waterMark = "我的水印"; + this.waterFont = "WenQuanZhengHei.ttf"; + this.fontType = "WenQuanZhengHei.ttf"; + this.slipOffset = "5"; + this.aesStatus = true; + this.interferenceOptions = "0"; + this.cacheNumber = "1000"; + this.timingClear = "180"; + this.cacheType = StorageType.local; + this.historyDataClearEnable = false; + this.reqFrequencyLimitEnable = false; + this.reqGetLockLimit = 5; + this.reqGetLockSeconds = 300; + this.reqGetMinuteLimit = 100; + this.reqCheckMinuteLimit = 100; + this.reqVerifyMinuteLimit = 100; + } + + public boolean isHistoryDataClearEnable() { + return this.historyDataClearEnable; + } + + public void setHistoryDataClearEnable(boolean historyDataClearEnable) { + this.historyDataClearEnable = historyDataClearEnable; + } + + public boolean isReqFrequencyLimitEnable() { + return this.reqFrequencyLimitEnable; + } + + public boolean getReqFrequencyLimitEnable() { + return this.reqFrequencyLimitEnable; + } + + public void setReqFrequencyLimitEnable(boolean reqFrequencyLimitEnable) { + this.reqFrequencyLimitEnable = reqFrequencyLimitEnable; + } + + public int getReqGetLockLimit() { + return this.reqGetLockLimit; + } + + public void setReqGetLockLimit(int reqGetLockLimit) { + this.reqGetLockLimit = reqGetLockLimit; + } + + public int getReqGetLockSeconds() { + return this.reqGetLockSeconds; + } + + public void setReqGetLockSeconds(int reqGetLockSeconds) { + this.reqGetLockSeconds = reqGetLockSeconds; + } + + public int getReqGetMinuteLimit() { + return this.reqGetMinuteLimit; + } + + public void setReqGetMinuteLimit(int reqGetMinuteLimit) { + this.reqGetMinuteLimit = reqGetMinuteLimit; + } + + public int getReqCheckMinuteLimit() { + return this.reqGetMinuteLimit; + } + + public void setReqCheckMinuteLimit(int reqCheckMinuteLimit) { + this.reqCheckMinuteLimit = reqCheckMinuteLimit; + } + + public int getReqVerifyMinuteLimit() { + return this.reqVerifyMinuteLimit; + } + + public void setReqVerifyMinuteLimit(int reqVerifyMinuteLimit) { + this.reqVerifyMinuteLimit = reqVerifyMinuteLimit; + } + + public static String getPREFIX() { + return "aj.captcha"; + } + + public CaptchaTypeEnum getType() { + return this.type; + } + + public void setType(CaptchaTypeEnum type) { + this.type = type; + } + + public String getJigsaw() { + return this.jigsaw; + } + + public void setJigsaw(String jigsaw) { + this.jigsaw = jigsaw; + } + + public String getPicClick() { + return this.picClick; + } + + public void setPicClick(String picClick) { + this.picClick = picClick; + } + + public String getWaterMark() { + return this.waterMark; + } + + public void setWaterMark(String waterMark) { + this.waterMark = waterMark; + } + + public String getWaterFont() { + return this.waterFont; + } + + public void setWaterFont(String waterFont) { + this.waterFont = waterFont; + } + + public String getFontType() { + return this.fontType; + } + + public void setFontType(String fontType) { + this.fontType = fontType; + } + + public String getSlipOffset() { + return this.slipOffset; + } + + public void setSlipOffset(String slipOffset) { + this.slipOffset = slipOffset; + } + + public Boolean getAesStatus() { + return this.aesStatus; + } + + public void setAesStatus(Boolean aesStatus) { + this.aesStatus = aesStatus; + } + + public StorageType getCacheType() { + return this.cacheType; + } + + public void setCacheType(AjCaptchaProperties.StorageType cacheType) { + this.cacheType = cacheType; + } + + public String getInterferenceOptions() { + return this.interferenceOptions; + } + + public void setInterferenceOptions(String interferenceOptions) { + this.interferenceOptions = interferenceOptions; + } + + public String getCacheNumber() { + return this.cacheNumber; + } + + public void setCacheNumber(String cacheNumber) { + this.cacheNumber = cacheNumber; + } + + public String getTimingClear() { + return this.timingClear; + } + + public void setTimingClear(String timingClear) { + this.timingClear = timingClear; + } + + public String toString() { + return "\nAjCaptchaProperties{type=" + this.type + ", jigsaw='" + this.jigsaw + '\'' + ", picClick='" + this.picClick + '\'' + ", waterMark='" + this.waterMark + '\'' + ", waterFont='" + this.waterFont + '\'' + ", fontType='" + this.fontType + '\'' + ", slipOffset='" + this.slipOffset + '\'' + ", aesStatus=" + this.aesStatus + ", interferenceOptions='" + this.interferenceOptions + '\'' + ", cacheNumber='" + this.cacheNumber + '\'' + ", timingClear='" + this.timingClear + '\'' + ", cacheType=" + this.cacheType + ", reqFrequencyLimitEnable=" + this.reqFrequencyLimitEnable + ", reqGetLockLimit=" + this.reqGetLockLimit + ", reqGetLockSeconds=" + this.reqGetLockSeconds + ", reqGetMinuteLimit=" + this.reqGetMinuteLimit + ", reqCheckMinuteLimit=" + this.reqCheckMinuteLimit + ", reqVerifyMinuteLimit=" + this.reqVerifyMinuteLimit + '}'; + } + + public static enum StorageType { + local, + redis, + other; + + private StorageType() { + } + } +} diff --git a/core/captcha-solon-plugin/src/main/resources/META-INF/solon/com-anji-captcha-solon-plugin.properties b/core/captcha-solon-plugin/src/main/resources/META-INF/solon/com-anji-captcha-solon-plugin.properties new file mode 100644 index 0000000000000000000000000000000000000000..7da0890b7bd31d761dfbd5d311873e6829377a49 --- /dev/null +++ b/core/captcha-solon-plugin/src/main/resources/META-INF/solon/com-anji-captcha-solon-plugin.properties @@ -0,0 +1 @@ +solon.plugin=com.anji.captcha.XPluginImp \ No newline at end of file diff --git a/pom.xml b/pom.xml index ac7634024984c73afaf739069040fd8a0b2fea68..69b9c33f0041b6376723328951cd5a68b7d64ef2 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,9 @@ core/captcha core/captcha-spring-boot-starter + core/captcha-solon-plugin service/springmvc service/springboot + service/solon \ No newline at end of file diff --git a/service/solon/README.md b/service/solon/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a22cca8ab0c9ebee967f1587759f53c63f54c703 --- /dev/null +++ b/service/solon/README.md @@ -0,0 +1,253 @@ +# 2 对接流程 +## 2.1 接入流程 +### 2.1.1 后端接入 + 用户提交表单会携带验证码相关参数,产品应用在相关接口处将该参数传给 集成jar包相关接口做二次校验,以确保该次验证是正确有效的。 +### 2.1.2 前端接入 + 引入相关组件,调用初始化函数,通过配置的一些参数信息。将行为验证码渲染出来。 +## 2.2 后端接入 +### 2.2.1 引入maven依赖 +目前已上传maven仓库,源码已分享 +```java + + com.github.anji-plus + captcha + 1.2.6 + +``` +### 2.2.2 缓存实现 +```java +#分布式环境要自己实现,参考service\springboot示例中CaptchaCacheServiceRedisImpl。默认使用内存。 +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(); +} +``` + +### 2.2.3 二次校验接口 +登录为例,用户在提交表单到产品应用后台,会携带一个验证码相关的参数。产品应用会在登录接口login中将该参数传给集成jar包中相关接口做二次校验。 +接口地址:https://****/captcha/verify +### 2.2.4 请求方式 +HTTP POST, 接口仅支持POST请求, 且仅接受 application/json 编码的参数 +### 2.2.5 请求参数 +| 参数 | 类型 | 必填 | 备注 | +| ------------ | ------------ | ------------ | ------------ | +| captchaVerification | String | Y | 验证数据,aes加密,数据在前端success函数回调参数中获取 | + + +### 2.2.6 响应参数 +| 参数 | 类型 | 必填 | 备注 | +| ------------ | ------------ | ------------ | ------------ | +| repCode | String | Y | 异常代号 | +| success | Boolean | Y | 成功或者失败 | +| error | Boolean | Y | 接口报错 | +| repMsg | String | Y | 错误信息 | + + +### 2.2.7 异常代号 + +| error | 说明 | +| ------------ | ------------ | +| 0000 | 无异常,代表成功 | +| 9999 | 服务器内部异常 | +| 0011 | 参数不能为空 | +| 6110 | 验证码已失效,请重新获取 | +| 6111 | 验证失败 | +| 6112 | 获取验证码失败,请联系管理员 | + +## 2.3 前端接入 +### 2.3.1 兼容性 +IE8+、Chrome、Firefox.(其他未测试) +### 2.3.2 初始化组件 +引入前端vue组件, npm install axios crypto-js -S +// 基础用例 + +```javascript +