@ -18,7 +18,7 @@
|
|||||||
<!-- 统一依赖管理 -->
|
<!-- 统一依赖管理 -->
|
||||||
<spring.boot.version>2.7.7</spring.boot.version>
|
<spring.boot.version>2.7.7</spring.boot.version>
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<knife4j.version>3.0.3</knife4j.version>
|
<knife4j.version>4.0.0</knife4j.version>
|
||||||
<swagger-annotations.version>1.6.8</swagger-annotations.version>
|
<swagger-annotations.version>1.6.8</swagger-annotations.version>
|
||||||
<servlet.versoin>2.5</servlet.versoin>
|
<servlet.versoin>2.5</servlet.versoin>
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
@ -41,6 +41,7 @@
|
|||||||
<!-- Bpm 工作流相关 -->
|
<!-- Bpm 工作流相关 -->
|
||||||
<flowable.version>6.8.0</flowable.version>
|
<flowable.version>6.8.0</flowable.version>
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
|
<captcha-plus.version>1.0.1</captcha-plus.version>
|
||||||
<jsoup.version>1.15.3</jsoup.version>
|
<jsoup.version>1.15.3</jsoup.version>
|
||||||
<lombok.version>1.18.24</lombok.version>
|
<lombok.version>1.18.24</lombok.version>
|
||||||
<mapstruct.version>1.5.3.Final</mapstruct.version>
|
<mapstruct.version>1.5.3.Final</mapstruct.version>
|
||||||
@ -63,7 +64,7 @@
|
|||||||
<minio.version>8.5.1</minio.version>
|
<minio.version>8.5.1</minio.version>
|
||||||
<aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
|
<aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
|
||||||
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
|
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
|
||||||
<tencentcloud-sdk-java.version>3.1.667</tencentcloud-sdk-java.version>
|
<tencentcloud-sdk-java.version>3.1.676</tencentcloud-sdk-java.version>
|
||||||
<justauth.version>1.4.0</justauth.version>
|
<justauth.version>1.4.0</justauth.version>
|
||||||
<jimureport.version>1.5.6</jimureport.version>
|
<jimureport.version>1.5.6</jimureport.version>
|
||||||
<xercesImpl.version>2.12.2</xercesImpl.version>
|
<xercesImpl.version>2.12.2</xercesImpl.version>
|
||||||
@ -166,7 +167,7 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
|
||||||
<version>${knife4j.version}</version>
|
<version>${knife4j.version}</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
@ -511,6 +512,12 @@
|
|||||||
<version>${netty-all.version}</version>
|
<version>${netty-all.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.xingyuv</groupId>
|
||||||
|
<artifactId>spring-boot-starter-captcha-plus</artifactId>
|
||||||
|
<version>${captcha-plus.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.lionsoul</groupId>
|
<groupId>org.lionsoul</groupId>
|
||||||
<artifactId>ip2region</artifactId>
|
<artifactId>ip2region</artifactId>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package cn.iocoder.yudao.framework.common.util.validation;
|
package cn.iocoder.yudao.framework.common.util.validation;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
import javax.validation.ConstraintViolation;
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alipay.sdk</groupId>
|
<groupId>com.alipay.sdk</groupId>
|
||||||
<artifactId>alipay-sdk-java</artifactId>
|
<artifactId>alipay-sdk-java</artifactId>
|
||||||
<version>4.35.9.ALL</version>
|
<version>4.35.32.ALL</version>
|
||||||
<exclusions>
|
<exclusions>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
@ -17,17 +17,14 @@
|
|||||||
</description>
|
</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.xingyuv</groupId>
|
||||||
|
<artifactId>spring-boot-starter-captcha-plus</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- Spring 核心 -->
|
<!-- Spring 核心 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- DB 相关 -->
|
<!-- DB 相关 -->
|
||||||
|
@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.captcha.config;
|
|||||||
import cn.hutool.core.util.ClassUtil;
|
import cn.hutool.core.util.ClassUtil;
|
||||||
import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
|
import cn.iocoder.yudao.framework.captcha.core.enums.CaptchaRedisKeyConstants;
|
||||||
import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
|
import cn.iocoder.yudao.framework.captcha.core.service.RedisCaptchaServiceImpl;
|
||||||
import com.anji.captcha.service.CaptchaCacheService;
|
import com.xingyuv.captcha.service.CaptchaCacheService;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package cn.iocoder.yudao.framework.captcha.core.enums;
|
package cn.iocoder.yudao.framework.captcha.core.enums;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
|
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
|
||||||
import com.anji.captcha.model.vo.PointVO;
|
import com.xingyuv.captcha.model.vo.PointVO;
|
||||||
import org.redisson.api.RLock;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
|
|
||||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package cn.iocoder.yudao.framework.captcha.core.service;
|
package cn.iocoder.yudao.framework.captcha.core.service;
|
||||||
|
|
||||||
import com.anji.captcha.service.CaptchaCacheService;
|
import com.xingyuv.captcha.service.CaptchaCacheService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
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 {
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package com.anji.captcha.config;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
public class AjCaptchaServiceAutoConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
public CaptchaService captchaService(AjCaptchaProperties prop) {
|
|
||||||
log.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 ((StrUtil.isNotBlank(prop.getJigsaw()) && prop.getJigsaw().startsWith("classpath:"))
|
|
||||||
|| (StrUtil.isNotBlank(prop.getPicClick()) && prop.getPicClick().startsWith("classpath:"))) {
|
|
||||||
//自定义resources目录下初始化底图
|
|
||||||
config.put(Const.CAPTCHA_INIT_ORIGINAL, "true");
|
|
||||||
initializeBaseMap(prop.getJigsaw(), prop.getPicClick());
|
|
||||||
}
|
|
||||||
return CaptchaServiceFactory.getInstance(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void initializeBaseMap(String jigsaw, String picClick) {
|
|
||||||
ImageUtils.cacheBootImage(getResourcesImagesFile(jigsaw + "/original/*.png"),
|
|
||||||
getResourcesImagesFile(jigsaw + "/slidingBlock/*.png"),
|
|
||||||
getResourcesImagesFile(picClick + "/*.png"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<String, String> getResourcesImagesFile(String path) {
|
|
||||||
Map<String, String> imgMap = new HashMap<>();
|
|
||||||
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
|
||||||
try {
|
|
||||||
Resource[] resources = resolver.getResources(path);
|
|
||||||
for (Resource resource : resources) {
|
|
||||||
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
|
||||||
String string = Base64Utils.encodeToString(bytes);
|
|
||||||
String filename = resource.getFilename();
|
|
||||||
imgMap.put(filename, string);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return imgMap;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package com.anji.captcha.model.common;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 底图类型枚举
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum CaptchaBaseMapEnum {
|
|
||||||
ORIGINAL("ORIGINAL", "滑动拼图底图"),
|
|
||||||
SLIDING_BLOCK("SLIDING_BLOCK", "滑动拼图滑块底图"),
|
|
||||||
PIC_CLICK("PIC_CLICK", "文字点选底图");
|
|
||||||
|
|
||||||
private final String codeValue;
|
|
||||||
private final String 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package com.anji.captcha.model.common;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@AllArgsConstructor
|
|
||||||
public enum CaptchaTypeEnum {
|
|
||||||
/**
|
|
||||||
* 滑块拼图.
|
|
||||||
*/
|
|
||||||
BLOCKPUZZLE("blockPuzzle", "滑块拼图"),
|
|
||||||
/**
|
|
||||||
* 文字点选.
|
|
||||||
*/
|
|
||||||
CLICKWORD("clickWord", "文字点选"),
|
|
||||||
/**
|
|
||||||
* 默认.
|
|
||||||
*/
|
|
||||||
DEFAULT("default", "默认");
|
|
||||||
|
|
||||||
private final String codeValue;
|
|
||||||
private final String 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() {
|
|
||||||
StringBuilder buffer = new StringBuilder();
|
|
||||||
for (CaptchaTypeEnum e : CaptchaTypeEnum.values()) {
|
|
||||||
buffer.append(e.codeValue).append("--").append(e.getCodeDesc()).append(", ");
|
|
||||||
}
|
|
||||||
buffer.deleteCharAt(buffer.lastIndexOf(","));
|
|
||||||
return buffer.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.model.common;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回应答码
|
|
||||||
*
|
|
||||||
* @author
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Getter
|
|
||||||
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 final String code;
|
|
||||||
private final String desc;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将入参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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.model.common;
|
|
||||||
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class RequestModel implements Serializable {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = -5800786065305114784L;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前请求接口路径 /business/accessUser/login
|
|
||||||
*/
|
|
||||||
private String servletPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {"reqData":{"password":"*****","userName":"admin"},"sign":"a304a7f296f565b6d2009797f68180f0","time":"1542456453355","token":""}
|
|
||||||
*/
|
|
||||||
private String requestString;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {"password":"****","userName":"admin"}
|
|
||||||
*/
|
|
||||||
private HashMap reqData;
|
|
||||||
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
private Long userId;
|
|
||||||
|
|
||||||
private String userName;
|
|
||||||
|
|
||||||
private List<Long> projectList;
|
|
||||||
|
|
||||||
//拥有哪些分组
|
|
||||||
private List<Long> groupIdList;
|
|
||||||
|
|
||||||
private String target;
|
|
||||||
|
|
||||||
private String sign;
|
|
||||||
|
|
||||||
private String time;
|
|
||||||
|
|
||||||
private String sourceIP;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验自身参数合法性
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public boolean isVaildateRequest() {
|
|
||||||
if (StrUtil.isBlank(sign) || StrUtil.isBlank(time)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getServletPath() {
|
|
||||||
return servletPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setServletPath(String servletPath) {
|
|
||||||
this.servletPath = servletPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.model.common;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isSuccess() {
|
|
||||||
return StrUtil.equals(repCode, RepCodeEnum.SUCCESS.getCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRepCode() {
|
|
||||||
return repCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRepCodeEnum(RepCodeEnum repCodeEnum) {
|
|
||||||
this.repCode = repCodeEnum.getCode();
|
|
||||||
this.repMsg = repCodeEnum.getDesc();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.model.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class CaptchaVO implements Serializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证码id(后台申请)
|
|
||||||
*/
|
|
||||||
private String captchaId;
|
|
||||||
|
|
||||||
private String projectCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证码类型:(clickWord,blockPuzzle)
|
|
||||||
*/
|
|
||||||
private String captchaType;
|
|
||||||
|
|
||||||
private String captchaOriginalPath;
|
|
||||||
|
|
||||||
private String captchaFontType;
|
|
||||||
|
|
||||||
private Integer captchaFontSize;
|
|
||||||
|
|
||||||
private String secretKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 原生图片base64
|
|
||||||
*/
|
|
||||||
private String originalImageBase64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滑块点选坐标
|
|
||||||
*/
|
|
||||||
private PointVO point;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滑块图片base64
|
|
||||||
*/
|
|
||||||
private String jigsawImageBase64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点选文字
|
|
||||||
*/
|
|
||||||
private List<String> wordList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点选坐标
|
|
||||||
*/
|
|
||||||
private List<Point> pointList;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点坐标(base64加密传输)
|
|
||||||
*/
|
|
||||||
private String pointJson;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UUID(每次请求的验证码唯一标识)
|
|
||||||
*/
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验结果
|
|
||||||
*/
|
|
||||||
private Boolean result = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 后台二次校验参数
|
|
||||||
*/
|
|
||||||
private String captchaVerification;
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 客户端UI组件id,组件初始化时设置一次,UUID
|
|
||||||
*/
|
|
||||||
private String clientUid;
|
|
||||||
/***
|
|
||||||
* 客户端的请求时间,预留字段
|
|
||||||
*/
|
|
||||||
private Long ts;
|
|
||||||
|
|
||||||
/***
|
|
||||||
* 客户端ip+userAgent
|
|
||||||
*/
|
|
||||||
private String browserInfo;
|
|
||||||
|
|
||||||
public void resetClientFlag() {
|
|
||||||
this.browserInfo = null;
|
|
||||||
this.clientUid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package com.anji.captcha.model.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class PointVO {
|
|
||||||
private String secretKey;
|
|
||||||
|
|
||||||
public int x;
|
|
||||||
|
|
||||||
public int y;
|
|
||||||
|
|
||||||
public PointVO(int x, int y, String secretKey) {
|
|
||||||
this.secretKey = secretKey;
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PointVO() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public PointVO(int x, int y) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toJsonString() {
|
|
||||||
return String.format("{\"secretKey\":\"%s\",\"x\":%d,\"y\":%d}", secretKey, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PointVO parse(String jsonStr) {
|
|
||||||
Map<String, Object> m = new HashMap();
|
|
||||||
Arrays.stream(jsonStr
|
|
||||||
.replaceFirst(",\\{", "\\{")
|
|
||||||
.replaceFirst("\\{", "")
|
|
||||||
.replaceFirst("\\}", "")
|
|
||||||
.replaceAll("\"", "")
|
|
||||||
.split(",")).forEach(item -> {
|
|
||||||
m.put(item.split(":")[0], item.split(":")[1]);
|
|
||||||
});
|
|
||||||
//PointVO d = new PointVO();
|
|
||||||
setX(Double.valueOf("" + m.get("x")).intValue());
|
|
||||||
setY(Double.valueOf("" + m.get("y")).intValue());
|
|
||||||
setSecretKey(m.getOrDefault("secretKey", "") + "");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (o == null || getClass() != o.getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PointVO pointVO = (PointVO) o;
|
|
||||||
return x == pointVO.x && y == pointVO.y && Objects.equals(secretKey, pointVO.secretKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
|
|
||||||
return Objects.hash(secretKey, x, y);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
package com.anji.captcha.properties;
|
|
||||||
|
|
||||||
import com.anji.captcha.model.common.CaptchaTypeEnum;
|
|
||||||
import lombok.Data;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@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 boolean getReqFrequencyLimitEnable() {
|
|
||||||
return reqFrequencyLimitEnable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum StorageType {
|
|
||||||
/**
|
|
||||||
* 内存.
|
|
||||||
*/
|
|
||||||
local,
|
|
||||||
/**
|
|
||||||
* redis.
|
|
||||||
*/
|
|
||||||
redis,
|
|
||||||
/**
|
|
||||||
* 其他.
|
|
||||||
*/
|
|
||||||
other,
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getPrefix() {
|
|
||||||
return PREFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
*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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
*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);
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by raodeming on 2019/12/25.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public abstract class AbstractCaptchaService implements CaptchaService {
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
log.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 ("local".equals(cacheType)) {
|
|
||||||
log.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 ("1".equals(config.getProperty(Const.HISTORY_DATA_CLEAR_ENABLE, "0"))) {
|
|
||||||
log.info("历史资源清除开关...开启..." + captchaType());
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
destroy(config);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if ("1".equals(config.getProperty(Const.REQ_FREQUENCY_LIMIT_ENABLE, "0"))) {
|
|
||||||
if (limitHandler == null) {
|
|
||||||
log.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 (StrUtil.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 (StrUtil.isNotEmpty(req.getBrowserInfo())) {
|
|
||||||
return MD5Util.md5(req.getBrowserInfo());
|
|
||||||
}
|
|
||||||
// 以客户端Ui组件id做识别标志
|
|
||||||
if (StrUtil.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,
|
|
||||||
Objects.requireNonNull(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) {
|
|
||||||
log.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 = Files.newOutputStream(tempFile.toPath());
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,425 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滑动验证码
|
|
||||||
* <p>
|
|
||||||
* Created by raodeming on 2019/12/25.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class BlockPuzzleCaptchaServiceImpl extends AbstractCaptchaService {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Properties config) {
|
|
||||||
super.init(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy(Properties config) {
|
|
||||||
log.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) {
|
|
||||||
log.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) {
|
|
||||||
log.error("滑动底图未初始化成功,请检查路径");
|
|
||||||
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
|
|
||||||
}
|
|
||||||
CaptchaVO captcha = pictureTemplatesCut(originalImage, jigsawImage, jigsawImageBase64);
|
|
||||||
if (captcha == null
|
|
||||||
|| StrUtil.isBlank(captcha.getJigsawImageBase64())
|
|
||||||
|| StrUtil.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) {
|
|
||||||
log.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) {
|
|
||||||
log.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) {
|
|
||||||
log.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);
|
|
||||||
log.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import com.anji.captcha.service.CaptchaCacheService;
|
|
||||||
import com.anji.captcha.util.CacheUtil;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对于分布式部署的应用,我们建议应用自己实现CaptchaCacheService,比如用Redis,参考service/spring-boot代码示例。
|
|
||||||
* 如果应用是单点的,也没有使用redis,那默认使用内存。
|
|
||||||
* 内存缓存只适合单节点部署的应用,否则验证码生产与验证在节点之间信息不同步,导致失败。
|
|
||||||
*
|
|
||||||
* @author lide1202@hotmail.com
|
|
||||||
* @Title: 默认使用内存当缓存
|
|
||||||
* @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.parseLong(Objects.requireNonNull(CacheUtil.get(key))) + val;
|
|
||||||
CacheUtil.set(key, ret + "", 0);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String type() {
|
|
||||||
return "local";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.ServiceLoader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by raodeming on 2020/5/26.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class CaptchaServiceFactory {
|
|
||||||
|
|
||||||
public static CaptchaService getInstance(Properties config) {
|
|
||||||
//先把所有CaptchaService初始化,通过init方法,实例字体等,add by lide1202@hotmail.com
|
|
||||||
/*try{
|
|
||||||
for(CaptchaService item: instances.values()){
|
|
||||||
item.init(config);
|
|
||||||
}
|
|
||||||
}catch (Exception e){
|
|
||||||
logger.warn("init captchaService fail:{}", e);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
String captchaType = config.getProperty(Const.CAPTCHA_TYPE, "default");
|
|
||||||
CaptchaService ret = instances.get(captchaType);
|
|
||||||
if (ret == null) {
|
|
||||||
throw new RuntimeException("unsupported-[captcha.type]=" + captchaType);
|
|
||||||
}
|
|
||||||
ret.init(config);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CaptchaCacheService getCache(String cacheType) {
|
|
||||||
return cacheService.get(cacheType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public volatile static Map<String, CaptchaService> instances = new HashMap<>();
|
|
||||||
public volatile static Map<String, CaptchaCacheService> cacheService = new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
ServiceLoader<CaptchaCacheService> cacheServices = ServiceLoader.load(CaptchaCacheService.class);
|
|
||||||
for (CaptchaCacheService item : cacheServices) {
|
|
||||||
cacheService.put(item.type(), item);
|
|
||||||
}
|
|
||||||
log.info("supported-captchaCache-service:{}", cacheService.keySet().toString());
|
|
||||||
ServiceLoader<CaptchaService> services = ServiceLoader.load(CaptchaService.class);
|
|
||||||
for (CaptchaService item : services) {
|
|
||||||
instances.put(item.captchaType(), item);
|
|
||||||
}
|
|
||||||
;
|
|
||||||
log.info("supported-captchaTypes-service:{}", instances.keySet().toString());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,321 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点选文字验证码
|
|
||||||
* <p>
|
|
||||||
* Created by raodeming on 2019/12/25.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
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.parseInt(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,
|
|
||||||
Objects.requireNonNull(getClass().getResourceAsStream("/fonts/" + clickWordFontStr)))
|
|
||||||
.deriveFont(Font.BOLD, size);
|
|
||||||
} else {
|
|
||||||
int style = Integer.parseInt(config.getProperty(Const.CAPTCHA_FONT_STYLE,Font.BOLD+""));
|
|
||||||
this.clickWordFont = new Font(clickWordFontStr, style, size);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("load font error:{}", ex);
|
|
||||||
}
|
|
||||||
this.wordTotalCount = Integer.parseInt(config.getProperty(Const.CAPTCHA_WORD_COUNT,"4"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy(Properties config) {
|
|
||||||
log.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) {
|
|
||||||
log.error("滑动底图未初始化成功,请检查路径");
|
|
||||||
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_BASEMAP_NULL);
|
|
||||||
}
|
|
||||||
CaptchaVO imageData = getImageData(bufferedImage);
|
|
||||||
if (imageData == null
|
|
||||||
|| StrUtil.isBlank(imageData.getOriginalImageBase64())) {
|
|
||||||
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_ERROR);
|
|
||||||
}
|
|
||||||
return ResponseModel.successData(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ResponseModel check(CaptchaVO captchaVO) {
|
|
||||||
ResponseModel r = super.check(captchaVO);
|
|
||||||
if (!validatedReq(r)) {
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
//取坐标信息
|
|
||||||
String codeKey = String.format(REDIS_CAPTCHA_KEY, captchaVO.getToken());
|
|
||||||
if (!CaptchaServiceFactory.getCache(cacheType).exists(codeKey)) {
|
|
||||||
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_INVALID);
|
|
||||||
}
|
|
||||||
String s = CaptchaServiceFactory.getCache(cacheType).get(codeKey);
|
|
||||||
//验证码只用一次,即刻失效
|
|
||||||
CaptchaServiceFactory.getCache(cacheType).delete(codeKey);
|
|
||||||
List<PointVO> point = null;
|
|
||||||
List<PointVO> point1 = null;
|
|
||||||
String pointJson = null;
|
|
||||||
/**
|
|
||||||
* [
|
|
||||||
* {
|
|
||||||
* "x": 85.0,
|
|
||||||
* "y": 34.0
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* "x": 129.0,
|
|
||||||
* "y": 56.0
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* "x": 233.0,
|
|
||||||
* "y": 27.0
|
|
||||||
* }
|
|
||||||
* ]
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
point = JsonUtil.parseArray(s, PointVO.class);
|
|
||||||
//aes解密
|
|
||||||
pointJson = decrypt(captchaVO.getPointJson(), point.get(0).getSecretKey());
|
|
||||||
point1 = JsonUtil.parseArray(pointJson, PointVO.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.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) {
|
|
||||||
log.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 (StrUtil.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) {
|
|
||||||
log.error("验证码坐标解析失败", e);
|
|
||||||
return ResponseModel.errorMsg(e.getMessage());
|
|
||||||
}
|
|
||||||
return ResponseModel.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWordTotalCount() {
|
|
||||||
return wordTotalCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setWordTotalCount(int wordTotalCount) {
|
|
||||||
this.wordTotalCount = wordTotalCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFontColorRandom() {
|
|
||||||
return fontColorRandom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFontColorRandom(boolean fontColorRandom) {
|
|
||||||
this.fontColorRandom = fontColorRandom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点选文字 字体总个数
|
|
||||||
*/
|
|
||||||
private int wordTotalCount = 4;
|
|
||||||
/**
|
|
||||||
* 点选文字 字体颜色是否随机
|
|
||||||
*/
|
|
||||||
private boolean fontColorRandom = Boolean.TRUE;
|
|
||||||
|
|
||||||
private CaptchaVO getImageData(BufferedImage backgroundImage) {
|
|
||||||
CaptchaVO dataVO = new CaptchaVO();
|
|
||||||
List<String> wordList = new ArrayList<String>();
|
|
||||||
List<PointVO> pointList = new ArrayList();
|
|
||||||
|
|
||||||
Graphics backgroundGraphics = backgroundImage.getGraphics();
|
|
||||||
int width = backgroundImage.getWidth();
|
|
||||||
int height = backgroundImage.getHeight();
|
|
||||||
|
|
||||||
int wordCount = getWordTotalCount();
|
|
||||||
//定义随机1到arr.length某一个字不参与校验
|
|
||||||
int num = RandomUtils.getRandomInt(1, wordCount);
|
|
||||||
Set<String> currentWords = getRandomWords(wordCount);
|
|
||||||
String secretKey = null;
|
|
||||||
if (captchaAesStatus) {
|
|
||||||
secretKey = AESUtil.getKey();
|
|
||||||
}
|
|
||||||
/*for (int i = 0; i < wordCount; i++) {
|
|
||||||
String word;
|
|
||||||
do {
|
|
||||||
word = RandomUtils.getRandomHan(HAN_ZI);
|
|
||||||
currentWords.add(word);
|
|
||||||
} while (!currentWords.contains(word));*/
|
|
||||||
int i = 0;
|
|
||||||
for (String word : currentWords) {
|
|
||||||
//随机字体坐标
|
|
||||||
PointVO point = randomWordPoint(width, height, i, wordCount);
|
|
||||||
point.setSecretKey(secretKey);
|
|
||||||
//随机字体颜色
|
|
||||||
if (isFontColorRandom()) {
|
|
||||||
backgroundGraphics.setColor(new Color(RandomUtils.getRandomInt(1, 255),
|
|
||||||
RandomUtils.getRandomInt(1, 255), RandomUtils.getRandomInt(1, 255)));
|
|
||||||
} else {
|
|
||||||
backgroundGraphics.setColor(Color.BLACK);
|
|
||||||
}
|
|
||||||
//设置角度
|
|
||||||
AffineTransform affineTransform = new AffineTransform();
|
|
||||||
affineTransform.rotate(Math.toRadians(RandomUtils.getRandomInt(-45, 45)), 0, 0);
|
|
||||||
Font rotatedFont = clickWordFont.deriveFont(affineTransform);
|
|
||||||
backgroundGraphics.setFont(rotatedFont);
|
|
||||||
backgroundGraphics.drawString(word, point.getX(), point.getY());
|
|
||||||
|
|
||||||
if ((num - 1) != i) {
|
|
||||||
wordList.add(word);
|
|
||||||
pointList.add(point);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundGraphics.setFont(waterMarkFont);
|
|
||||||
backgroundGraphics.setColor(Color.white);
|
|
||||||
backgroundGraphics.drawString(waterMark, width - getEnOrChLength(waterMark), height - (HAN_ZI_SIZE / 2) + 7);
|
|
||||||
|
|
||||||
//创建合并图片
|
|
||||||
BufferedImage combinedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics combinedGraphics = combinedImage.getGraphics();
|
|
||||||
combinedGraphics.drawImage(backgroundImage, 0, 0, null);
|
|
||||||
|
|
||||||
dataVO.setOriginalImageBase64(ImageUtils.getImageToBase64Str(backgroundImage).replaceAll("\r|\n", ""));
|
|
||||||
//pointList信息不传到前端,只做后端check校验
|
|
||||||
//dataVO.setPointList(pointList);
|
|
||||||
dataVO.setWordList(wordList);
|
|
||||||
dataVO.setToken(RandomUtils.getUUID());
|
|
||||||
dataVO.setSecretKey(secretKey);
|
|
||||||
//将坐标信息存入redis中
|
|
||||||
String codeKey = String.format(REDIS_CAPTCHA_KEY, dataVO.getToken());
|
|
||||||
CaptchaServiceFactory.getCache(cacheType).set(codeKey, JsonUtil.toJSONString(pointList), EXPIRESIN_SECONDS);
|
|
||||||
// base64StrToImage(getImageToBase64Str(backgroundImage), "D:\\点击.png");
|
|
||||||
return dataVO;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> getRandomWords(int wordCount) {
|
|
||||||
Set<String> words = new HashSet<>();
|
|
||||||
int size = HAN_ZI.length();
|
|
||||||
for (; ; ) {
|
|
||||||
String t = HAN_ZI.charAt(RandomUtils.getRandomInt(size)) + "";
|
|
||||||
words.add(t);
|
|
||||||
if (words.size() >= wordCount) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return words;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 随机字体循环排序下标
|
|
||||||
*
|
|
||||||
* @param imageWidth 图片宽度
|
|
||||||
* @param imageHeight 图片高度
|
|
||||||
* @param wordSortIndex 字体循环排序下标(i)
|
|
||||||
* @param wordCount 字数量
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private static PointVO randomWordPoint(int imageWidth, int imageHeight, int wordSortIndex, int wordCount) {
|
|
||||||
int avgWidth = imageWidth / (wordCount + 1);
|
|
||||||
int x, y;
|
|
||||||
if (avgWidth < HAN_ZI_SIZE_HALF) {
|
|
||||||
x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, imageWidth);
|
|
||||||
} else {
|
|
||||||
if (wordSortIndex == 0) {
|
|
||||||
x = RandomUtils.getRandomInt(1 + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF);
|
|
||||||
} else {
|
|
||||||
x = RandomUtils.getRandomInt(avgWidth * wordSortIndex + HAN_ZI_SIZE_HALF, avgWidth * (wordSortIndex + 1) - HAN_ZI_SIZE_HALF);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y = RandomUtils.getRandomInt(HAN_ZI_SIZE, imageHeight - HAN_ZI_SIZE);
|
|
||||||
return new PointVO(x, y, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by raodeming on 2019/12/25.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
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 (StrUtil.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 (StrUtil.isEmpty(captchaVO.getCaptchaType())) {
|
|
||||||
return RepCodeEnum.NULL_ERROR.parseError("类型");
|
|
||||||
}
|
|
||||||
if (StrUtil.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 (StrUtil.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) {
|
|
||||||
log.error("验证码坐标解析失败", e);
|
|
||||||
return ResponseModel.errorMsg(e.getMessage());
|
|
||||||
}
|
|
||||||
return ResponseModel.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,154 +0,0 @@
|
|||||||
package com.anji.captcha.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
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 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 (StrUtil.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.parseLong(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.parseLong(failCnts) > Long.parseLong(config.getProperty(Const.REQ_GET_LOCK_LIMIT, "5"))) {
|
|
||||||
// get接口锁定5分钟
|
|
||||||
cacheService.set(lockKey, "1", Long.parseLong(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 (StrUtil.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.parseLong(v) > Long.parseLong(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.parseLong(v) > Long.parseLong(config.getProperty(Const.REQ_VALIDATE_MINUTE_LIMIT, "600"))) {
|
|
||||||
return ResponseModel.errorMsg(RepCodeEnum.API_REQ_LIMIT_VERIFY_ERROR);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.util;
|
|
||||||
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.KeyGenerator;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
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 StrUtil.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(StandardCharsets.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 (StrUtil.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 (StrUtil.isBlank(decryptKey)) {
|
|
||||||
return encryptStr;
|
|
||||||
}
|
|
||||||
return StrUtil.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.util;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
public final class CacheUtil {
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CacheUtil.class);
|
|
||||||
|
|
||||||
private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<String, Object>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存最大个数
|
|
||||||
*/
|
|
||||||
private static Integer CACHE_MAX_NUMBER = 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化
|
|
||||||
*
|
|
||||||
* @param cacheMaxNumber 缓存最大个数
|
|
||||||
* @param second 定时任务 秒执行清除过期缓存
|
|
||||||
*/
|
|
||||||
public static void init(int cacheMaxNumber, long second) {
|
|
||||||
CACHE_MAX_NUMBER = cacheMaxNumber;
|
|
||||||
if (second > 0L) {
|
|
||||||
/*Timer timer = new Timer();
|
|
||||||
timer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
}, 0, second * 1000);*/
|
|
||||||
ScheduledExecutorService 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存刷新,清除过期数据
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
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();
|
|
||||||
return byteCount;
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
*Copyright © 2018 anji-plus
|
|
||||||
*安吉加加信息技术有限公司
|
|
||||||
*http://www.anji-plus.com
|
|
||||||
*All rights reserved.
|
|
||||||
*/
|
|
||||||
package com.anji.captcha.util;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.anji.captcha.model.common.CaptchaBaseMapEnum;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.util.Base64Utils;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class ImageUtils {
|
|
||||||
private static Map<String, String> originalCacheMap = new ConcurrentHashMap(); //滑块底图
|
|
||||||
private static Map<String, String> slidingBlockCacheMap = new ConcurrentHashMap(); //滑块
|
|
||||||
private static Map<String, String> picClickCacheMap = new ConcurrentHashMap(); //点选文字
|
|
||||||
private static Map<String, String[]> fileNameMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public static void cacheImage(String captchaOriginalPathJigsaw, String captchaOriginalPathClick) {
|
|
||||||
//滑动拼图
|
|
||||||
if (StrUtil.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 (StrUtil.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]));
|
|
||||||
log.info("初始化底图:{}", JsonUtil.toJSONString(fileNameMap));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void cacheBootImage(Map<String, String> originalMap, Map<String, String> slidingBlockMap, Map<String, String> picClickMap) {
|
|
||||||
originalCacheMap.putAll(originalMap);
|
|
||||||
slidingBlockCacheMap.putAll(slidingBlockMap);
|
|
||||||
picClickCacheMap.putAll(picClickMap);
|
|
||||||
fileNameMap.put(CaptchaBaseMapEnum.ORIGINAL.getCodeValue(), originalCacheMap.keySet().toArray(new String[0]));
|
|
||||||
fileNameMap.put(CaptchaBaseMapEnum.SLIDING_BLOCK.getCodeValue(), slidingBlockCacheMap.keySet().toArray(new String[0]));
|
|
||||||
fileNameMap.put(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue(), picClickCacheMap.keySet().toArray(new String[0]));
|
|
||||||
log.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);
|
|
||||||
return slidingBlockCacheMap.get(strings[randomInt]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BufferedImage getPicClick() {
|
|
||||||
String[] strings = fileNameMap.get(CaptchaBaseMapEnum.PIC_CLICK.getCodeValue());
|
|
||||||
if (null == strings || strings.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Integer randomInt = RandomUtils.getRandomInt(0, strings.length);
|
|
||||||
String s = picClickCacheMap.get(strings[randomInt]);
|
|
||||||
return getBase64StrToImage(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片转base64 字符串
|
|
||||||
*
|
|
||||||
* @param templateImage
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getImageToBase64Str(BufferedImage templateImage) {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
try {
|
|
||||||
ImageIO.write(templateImage, "png", baos);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
byte[] bytes = baos.toByteArray();
|
|
||||||
|
|
||||||
Base64.Encoder encoder = Base64.getEncoder();
|
|
||||||
|
|
||||||
return encoder.encodeToString(bytes).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64 字符串转图片
|
|
||||||
*
|
|
||||||
* @param base64String
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static BufferedImage getBase64StrToImage(String base64String) {
|
|
||||||
try {
|
|
||||||
Base64.Decoder decoder = Base64.getDecoder();
|
|
||||||
byte[] bytes = decoder.decode(base64String);
|
|
||||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
|
||||||
return ImageIO.read(inputStream);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static Map<String, String> getResourcesImagesFile(String path) {
|
|
||||||
//默认提供六张底图
|
|
||||||
Map<String, String> imgMap = new HashMap<>();
|
|
||||||
ClassLoader classLoader = ImageUtils.class.getClassLoader();
|
|
||||||
for (int i = 1; i <= 6; i++) {
|
|
||||||
InputStream resourceAsStream = classLoader.getResourceAsStream(path.concat("/").concat(String.valueOf(i).concat(".png")));
|
|
||||||
byte[] bytes = new byte[0];
|
|
||||||
try {
|
|
||||||
bytes = FileCopyUtils.copyToByteArray(resourceAsStream);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
String string = Base64Utils.encodeToString(bytes);
|
|
||||||
String filename = String.valueOf(i).concat(".png");
|
|
||||||
imgMap.put(filename, string);
|
|
||||||
}
|
|
||||||
return imgMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<String, String> getImagesFile(String path) {
|
|
||||||
Map<String, String> imgMap = new HashMap<>();
|
|
||||||
File file = new File(path);
|
|
||||||
if (!file.exists()) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
File[] files = file.listFiles();
|
|
||||||
Arrays.stream(files).forEach(item -> {
|
|
||||||
try {
|
|
||||||
FileInputStream fileInputStream = new FileInputStream(item);
|
|
||||||
byte[] bytes = FileCopyUtils.copyToByteArray(fileInputStream);
|
|
||||||
String string = Base64Utils.encodeToString(bytes);
|
|
||||||
imgMap.put(item.getName(), string);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return imgMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package com.anji.captcha.util;
|
|
||||||
|
|
||||||
import com.anji.captcha.model.vo.PointVO;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 替换掉fastjson,自定义实现相关方法
|
|
||||||
* note: 该实现不具有通用性,仅用于本项目。
|
|
||||||
*
|
|
||||||
* @author WongBin
|
|
||||||
* @date 2021/1/8
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class JsonUtil {
|
|
||||||
public static List<PointVO> parseArray(String text, Class<PointVO> clazz) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
String[] arr = text.replaceFirst("\\[", "")
|
|
||||||
.replaceFirst("\\]", "").split("\\}");
|
|
||||||
List<PointVO> ret = new ArrayList<>(arr.length);
|
|
||||||
for (String s : arr) {
|
|
||||||
ret.add(parseObject(s, PointVO.class));
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PointVO parseObject(String text, Class<PointVO> clazz) {
|
|
||||||
if (text == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
/*if(!clazz.isAssignableFrom(PointVO.class)) {
|
|
||||||
throw new UnsupportedOperationException("不支持的输入类型:"
|
|
||||||
+ clazz.getSimpleName());
|
|
||||||
}*/
|
|
||||||
try {
|
|
||||||
PointVO ret = clazz.newInstance();
|
|
||||||
return ret.parse(text);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("json解析异常", ex);
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toJSONString(Object object) {
|
|
||||||
if (object == null) {
|
|
||||||
return "{}";
|
|
||||||
}
|
|
||||||
if (object instanceof PointVO) {
|
|
||||||
PointVO t = (PointVO) object;
|
|
||||||
return t.toJsonString();
|
|
||||||
}
|
|
||||||
if (object instanceof List) {
|
|
||||||
List<PointVO> list = (List<PointVO>) object;
|
|
||||||
StringBuilder buf = new StringBuilder("[");
|
|
||||||
list.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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
package com.anji.captcha.util;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
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(StandardCharsets.UTF_8));
|
|
||||||
byte[] s = m.digest();
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
for (byte b : s) {
|
|
||||||
result.append(Integer.toHexString((0x000000FF & b) | 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
*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) {
|
|
||||||
return hanZi.charAt(new Random().nextInt(hanZi.length())) + "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getRandomInt(int bound) {
|
|
||||||
return ThreadLocalRandom.current().nextInt(bound);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取随机中文
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getRandomHan() {
|
|
||||||
String str = "";
|
|
||||||
int highCode;
|
|
||||||
int lowCode;
|
|
||||||
|
|
||||||
Random random = new Random();
|
|
||||||
|
|
||||||
highCode = (176 + Math.abs(random.nextInt(39))); //B0 + 0~39(16~55) 一级汉字所占区
|
|
||||||
lowCode = (161 + Math.abs(random.nextInt(93))); //A1 + 0~93 每区有94个汉字
|
|
||||||
|
|
||||||
byte[] b = new byte[2];
|
|
||||||
b[0] = (Integer.valueOf(highCode)).byteValue();
|
|
||||||
b[1] = (Integer.valueOf(lowCode)).byteValue();
|
|
||||||
|
|
||||||
try {
|
|
||||||
str = new String(b, "GBK");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 随机范围内数字
|
|
||||||
*
|
|
||||||
* @param startNum
|
|
||||||
* @param endNum
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Integer getRandomInt(int startNum, int endNum) {
|
|
||||||
return ThreadLocalRandom.current().nextInt(endNum - startNum) + startNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取随机字符串
|
|
||||||
*
|
|
||||||
* @param length
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getRandomString(int length) {
|
|
||||||
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
||||||
Random random = new Random();
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
int number = random.nextInt(62);
|
|
||||||
sb.append(str.charAt(number));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,138 +0,0 @@
|
|||||||
package com.anji.captcha.util;
|
|
||||||
//
|
|
||||||
// Source code recreated from a .class file by IntelliJ IDEA
|
|
||||||
// (powered by Fernflower decompiler)
|
|
||||||
//
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
public abstract class StreamUtils {
|
|
||||||
public static final int BUFFER_SIZE = 4096;
|
|
||||||
private static final byte[] EMPTY_CONTENT = new byte[0];
|
|
||||||
|
|
||||||
public StreamUtils() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] copyToByteArray(InputStream in) throws IOException {
|
|
||||||
if (in == null) {
|
|
||||||
return new byte[0];
|
|
||||||
} else {
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream(4096);
|
|
||||||
copy((InputStream) in, out);
|
|
||||||
return out.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String copyToString(InputStream in, Charset charset) throws IOException {
|
|
||||||
if (in == null) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
StringBuilder out = new StringBuilder();
|
|
||||||
InputStreamReader reader = new InputStreamReader(in, charset);
|
|
||||||
char[] buffer = new char[4096];
|
|
||||||
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = reader.read(buffer)) != -1) {
|
|
||||||
out.append(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void copy(byte[] in, OutputStream out) throws IOException {
|
|
||||||
out.write(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void copy(String in, Charset charset, OutputStream out) throws IOException {
|
|
||||||
Writer writer = new OutputStreamWriter(out, charset);
|
|
||||||
writer.write(in);
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int copy(InputStream in, OutputStream out) throws IOException {
|
|
||||||
int byteCount = 0;
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
|
|
||||||
int bytesRead;
|
|
||||||
for (boolean var4 = true; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
out.flush();
|
|
||||||
return byteCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
|
|
||||||
long skipped = in.skip(start);
|
|
||||||
if (skipped < start) {
|
|
||||||
throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required");
|
|
||||||
} else {
|
|
||||||
long bytesToCopy = end - start + 1L;
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
|
|
||||||
while (bytesToCopy > 0L) {
|
|
||||||
int bytesRead = in.read(buffer);
|
|
||||||
if (bytesRead == -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((long) bytesRead <= bytesToCopy) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
bytesToCopy -= (long) bytesRead;
|
|
||||||
} else {
|
|
||||||
out.write(buffer, 0, (int) bytesToCopy);
|
|
||||||
bytesToCopy = 0L;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return end - start + 1L - bytesToCopy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int drain(InputStream in) throws IOException {
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int byteCount;
|
|
||||||
int bytesRead;
|
|
||||||
for (byteCount = 0; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return byteCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InputStream emptyInput() {
|
|
||||||
return new ByteArrayInputStream(EMPTY_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InputStream nonClosing(InputStream in) {
|
|
||||||
return new NonClosingInputStream(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OutputStream nonClosing(OutputStream out) {
|
|
||||||
return new NonClosingOutputStream(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NonClosingOutputStream extends FilterOutputStream {
|
|
||||||
public NonClosingOutputStream(OutputStream out) {
|
|
||||||
super(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(byte[] b, int off, int let) throws IOException {
|
|
||||||
this.out.write(b, off, let);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() throws IOException {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NonClosingInputStream extends FilterInputStream {
|
|
||||||
public NonClosingInputStream(InputStream in) {
|
|
||||||
super(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() throws IOException {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl
|
|
||||||
com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl
|
|
||||||
com.anji.captcha.service.impl.DefaultCaptchaServiceImpl
|
|
@ -1,2 +1 @@
|
|||||||
com.anji.captcha.config.AjCaptchaAutoConfiguration
|
|
||||||
cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
|
cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
|
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 58 KiB |
@ -1,55 +0,0 @@
|
|||||||
文泉驿是一个开源汉字字体项目
|
|
||||||
|
|
||||||
由旅美学者房骞骞(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和嵌入式操作系统中使用
|
|
@ -21,6 +21,12 @@
|
|||||||
<artifactId>yudao-common</artifactId>
|
<artifactId>yudao-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Web 相关 -->
|
<!-- Web 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@ -35,7 +41,7 @@
|
|||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
<groupId>com.github.xiaoymin</groupId>
|
||||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.swagger</groupId>
|
<groupId>io.swagger</groupId>
|
||||||
@ -72,6 +78,7 @@
|
|||||||
<groupId>org.jsoup</groupId>
|
<groupId>org.jsoup</groupId>
|
||||||
<artifactId>jsoup</artifactId>
|
<artifactId>jsoup</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -9,15 +9,16 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
import springfox.documentation.builders.ApiInfoBuilder;
|
||||||
import springfox.documentation.builders.ExampleBuilder;
|
import springfox.documentation.builders.ParameterBuilder;
|
||||||
import springfox.documentation.builders.PathSelectors;
|
import springfox.documentation.builders.PathSelectors;
|
||||||
import springfox.documentation.builders.RequestParameterBuilder;
|
import springfox.documentation.schema.ModelRef;
|
||||||
import springfox.documentation.service.*;
|
import springfox.documentation.service.*;
|
||||||
import springfox.documentation.spi.DocumentationType;
|
import springfox.documentation.spi.DocumentationType;
|
||||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||||
import springfox.documentation.spring.web.plugins.Docket;
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ import static springfox.documentation.builders.RequestHandlerSelectors.basePacka
|
|||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@EnableSwagger2
|
@EnableSwagger2WebMvc
|
||||||
@EnableKnife4j
|
@EnableKnife4j
|
||||||
@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
|
@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
|
||||||
// 允许使用 swagger.enable=false 禁用 Swagger
|
// 允许使用 swagger.enable=false 禁用 Swagger
|
||||||
@ -59,7 +60,7 @@ public class YudaoSwaggerAutoConfiguration {
|
|||||||
.securitySchemes(securitySchemes())
|
.securitySchemes(securitySchemes())
|
||||||
.securityContexts(securityContexts())
|
.securityContexts(securityContexts())
|
||||||
// ④ 全局参数(多租户 header)
|
// ④ 全局参数(多租户 header)
|
||||||
.globalRequestParameters(globalRequestParameters());
|
.globalOperationParameters(globalRequestParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== apiInfo ==========
|
// ========== apiInfo ==========
|
||||||
@ -95,7 +96,7 @@ public class YudaoSwaggerAutoConfiguration {
|
|||||||
return Collections.singletonList(SecurityContext.builder()
|
return Collections.singletonList(SecurityContext.builder()
|
||||||
.securityReferences(securityReferences())
|
.securityReferences(securityReferences())
|
||||||
// 通过 PathSelectors.regex("^(?!auth).*$"),排除包含 "auth" 的接口不需要使用securitySchemes
|
// 通过 PathSelectors.regex("^(?!auth).*$"),排除包含 "auth" 的接口不需要使用securitySchemes
|
||||||
.operationSelector(o -> o.requestMappingPattern().matches("^(?!auth).*$"))
|
.forPaths(PathSelectors.regex("^(?!auth).*$"))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,11 +110,17 @@ public class YudaoSwaggerAutoConfiguration {
|
|||||||
|
|
||||||
// ========== globalRequestParameters ==========
|
// ========== globalRequestParameters ==========
|
||||||
|
|
||||||
private static List<RequestParameter> globalRequestParameters() {
|
private static List<Parameter> globalRequestParameters() {
|
||||||
RequestParameterBuilder tenantParameter = new RequestParameterBuilder()
|
List<Parameter> tenantParameter = new ArrayList<>();
|
||||||
.name(HEADER_TENANT_ID).description("租户编号")
|
tenantParameter.add(new ParameterBuilder()
|
||||||
.in(ParameterType.HEADER).example(new ExampleBuilder().value(1L).build());
|
.name(HEADER_TENANT_ID)
|
||||||
return Collections.singletonList(tenantParameter.build());
|
.description("租户编号")
|
||||||
|
.modelRef(new ModelRef("long"))
|
||||||
|
.defaultValue("1")
|
||||||
|
.parameterType("header")
|
||||||
|
.required(true)
|
||||||
|
.build());
|
||||||
|
return tenantParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
|||||||
import org.springframework.beans.BeansException;
|
import org.springframework.beans.BeansException;
|
||||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||||
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
|
|
||||||
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -20,7 +19,7 @@ public class SpringFoxHandlerProviderBeanPostProcessor implements BeanPostProces
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||||
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
|
if (bean instanceof WebMvcRequestHandlerProvider) {
|
||||||
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
|
||||||
}
|
}
|
||||||
return bean;
|
return bean;
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
import { reactive } from 'vue'
|
import type { VxeCrudSchema } from '@/hooks/web/useVxeCrudSchemas'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
|
||||||
import { DICT_TYPE } from '@/utils/dict'
|
|
||||||
import { required } from '@/utils/formRules'
|
|
||||||
import { VxeCrudSchema, useVxeCrudSchemas } from '@/hooks/web/useVxeCrudSchemas'
|
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
// 表单校验
|
// 表单校验
|
||||||
export const rules = reactive({
|
export const rules = reactive({
|
||||||
|
@ -75,11 +75,6 @@
|
|||||||
</XModal>
|
</XModal>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts" name="${simpleClassName}">
|
<script setup lang="ts" name="${simpleClassName}">
|
||||||
// 全局相关的 import
|
|
||||||
import { ref, unref } from 'vue'
|
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
|
||||||
import { useXTable } from '@/hooks/web/useXTable'
|
|
||||||
import { FormExpose } from '@/components/Form'
|
import { FormExpose } from '@/components/Form'
|
||||||
// 业务相关的 import
|
// 业务相关的 import
|
||||||
import { rules, allSchemas } from './${classNameVar}.data'
|
import { rules, allSchemas } from './${classNameVar}.data'
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package cn.iocoder.yudao.module.system.controller.admin.captcha;
|
package cn.iocoder.yudao.module.system.controller.admin.captcha;
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.extra.servlet.ServletUtil;
|
||||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||||
import com.anji.captcha.model.common.ResponseModel;
|
import com.xingyuv.captcha.model.common.ResponseModel;
|
||||||
import com.anji.captcha.model.vo.CaptchaVO;
|
import com.xingyuv.captcha.model.vo.CaptchaVO;
|
||||||
import com.anji.captcha.service.CaptchaService;
|
import com.xingyuv.captcha.service.CaptchaService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -19,9 +20,6 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
/**
|
/**
|
||||||
* 验证码
|
* 验证码
|
||||||
*
|
*
|
||||||
* 问题:为什么不直接使用 anji 提供的 CaptchaController,而要另外继承?
|
|
||||||
* 回答:管理使用 /admin-api/* 作为前缀,所以需要继承!
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Api(tags = "管理后台 - 验证码")
|
@Api(tags = "管理后台 - 验证码")
|
||||||
@ -52,8 +50,7 @@ public class CaptchaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getRemoteId(HttpServletRequest request) {
|
public static String getRemoteId(HttpServletRequest request) {
|
||||||
String xfwd = request.getHeader("X-Forwarded-For");
|
String ip = ServletUtil.getClientIP(request);
|
||||||
String ip = getRemoteIpFromXfwd(xfwd);
|
|
||||||
String ua = request.getHeader("user-agent");
|
String ua = request.getHeader("user-agent");
|
||||||
if (StrUtil.isNotBlank(ip)) {
|
if (StrUtil.isNotBlank(ip)) {
|
||||||
return ip + ua;
|
return ip + ua;
|
||||||
@ -61,12 +58,4 @@ public class CaptchaController {
|
|||||||
return request.getRemoteAddr() + ua;
|
return request.getRemoteAddr() + ua;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getRemoteIpFromXfwd(String xfwd) {
|
|
||||||
if (StrUtil.isNotBlank(xfwd)) {
|
|
||||||
String[] ipList = xfwd.split(",");
|
|
||||||
return StrUtil.trim(ipList[0]);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,9 @@ import cn.iocoder.yudao.module.system.service.member.MemberService;
|
|||||||
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
|
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
|
||||||
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
|
||||||
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
|
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
|
||||||
import com.anji.captcha.model.common.ResponseModel;
|
import com.xingyuv.captcha.model.common.ResponseModel;
|
||||||
import com.anji.captcha.model.vo.CaptchaVO;
|
import com.xingyuv.captcha.model.vo.CaptchaVO;
|
||||||
import com.anji.captcha.service.CaptchaService;
|
import com.xingyuv.captcha.service.CaptchaService;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
259
yudao-ui-admin-vue3/.eslintrc-auto-import.json
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
{
|
||||||
|
"globals": {
|
||||||
|
"EffectScope": true,
|
||||||
|
"ElMessage": true,
|
||||||
|
"ElMessageBox": true,
|
||||||
|
"ElTag": true,
|
||||||
|
"asyncComputed": true,
|
||||||
|
"autoResetRef": true,
|
||||||
|
"computed": true,
|
||||||
|
"computedAsync": true,
|
||||||
|
"computedEager": true,
|
||||||
|
"computedInject": true,
|
||||||
|
"computedWithControl": true,
|
||||||
|
"controlledComputed": true,
|
||||||
|
"controlledRef": true,
|
||||||
|
"createApp": true,
|
||||||
|
"createEventHook": true,
|
||||||
|
"createGlobalState": true,
|
||||||
|
"createInjectionState": true,
|
||||||
|
"createReactiveFn": true,
|
||||||
|
"createSharedComposable": true,
|
||||||
|
"createUnrefFn": true,
|
||||||
|
"customRef": true,
|
||||||
|
"debouncedRef": true,
|
||||||
|
"debouncedWatch": true,
|
||||||
|
"defineAsyncComponent": true,
|
||||||
|
"defineComponent": true,
|
||||||
|
"eagerComputed": true,
|
||||||
|
"effectScope": true,
|
||||||
|
"extendRef": true,
|
||||||
|
"getCurrentInstance": true,
|
||||||
|
"getCurrentScope": true,
|
||||||
|
"h": true,
|
||||||
|
"ignorableWatch": true,
|
||||||
|
"inject": true,
|
||||||
|
"isDefined": true,
|
||||||
|
"isProxy": true,
|
||||||
|
"isReactive": true,
|
||||||
|
"isReadonly": true,
|
||||||
|
"isRef": true,
|
||||||
|
"makeDestructurable": true,
|
||||||
|
"markRaw": true,
|
||||||
|
"nextTick": true,
|
||||||
|
"onActivated": true,
|
||||||
|
"onBeforeMount": true,
|
||||||
|
"onBeforeUnmount": true,
|
||||||
|
"onBeforeUpdate": true,
|
||||||
|
"onClickOutside": true,
|
||||||
|
"onDeactivated": true,
|
||||||
|
"onErrorCaptured": true,
|
||||||
|
"onKeyStroke": true,
|
||||||
|
"onLongPress": true,
|
||||||
|
"onMounted": true,
|
||||||
|
"onRenderTracked": true,
|
||||||
|
"onRenderTriggered": true,
|
||||||
|
"onScopeDispose": true,
|
||||||
|
"onServerPrefetch": true,
|
||||||
|
"onStartTyping": true,
|
||||||
|
"onUnmounted": true,
|
||||||
|
"onUpdated": true,
|
||||||
|
"pausableWatch": true,
|
||||||
|
"provide": true,
|
||||||
|
"reactify": true,
|
||||||
|
"reactifyObject": true,
|
||||||
|
"reactive": true,
|
||||||
|
"reactiveComputed": true,
|
||||||
|
"reactiveOmit": true,
|
||||||
|
"reactivePick": true,
|
||||||
|
"readonly": true,
|
||||||
|
"ref": true,
|
||||||
|
"refAutoReset": true,
|
||||||
|
"refDebounced": true,
|
||||||
|
"refDefault": true,
|
||||||
|
"refThrottled": true,
|
||||||
|
"refWithControl": true,
|
||||||
|
"resolveComponent": true,
|
||||||
|
"resolveRef": true,
|
||||||
|
"resolveUnref": true,
|
||||||
|
"shallowReactive": true,
|
||||||
|
"shallowReadonly": true,
|
||||||
|
"shallowRef": true,
|
||||||
|
"syncRef": true,
|
||||||
|
"syncRefs": true,
|
||||||
|
"templateRef": true,
|
||||||
|
"throttledRef": true,
|
||||||
|
"throttledWatch": true,
|
||||||
|
"toRaw": true,
|
||||||
|
"toReactive": true,
|
||||||
|
"toRef": true,
|
||||||
|
"toRefs": true,
|
||||||
|
"triggerRef": true,
|
||||||
|
"tryOnBeforeMount": true,
|
||||||
|
"tryOnBeforeUnmount": true,
|
||||||
|
"tryOnMounted": true,
|
||||||
|
"tryOnScopeDispose": true,
|
||||||
|
"tryOnUnmounted": true,
|
||||||
|
"unref": true,
|
||||||
|
"unrefElement": true,
|
||||||
|
"until": true,
|
||||||
|
"useActiveElement": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useAsyncQueue": true,
|
||||||
|
"useAsyncState": true,
|
||||||
|
"useAttrs": true,
|
||||||
|
"useBase64": true,
|
||||||
|
"useBattery": true,
|
||||||
|
"useBluetooth": true,
|
||||||
|
"useBreakpoints": true,
|
||||||
|
"useBroadcastChannel": true,
|
||||||
|
"useBrowserLocation": true,
|
||||||
|
"useCached": true,
|
||||||
|
"useClipboard": true,
|
||||||
|
"useColorMode": true,
|
||||||
|
"useConfirmDialog": true,
|
||||||
|
"useCounter": true,
|
||||||
|
"useCssModule": true,
|
||||||
|
"useCssVar": true,
|
||||||
|
"useCssVars": true,
|
||||||
|
"useCurrentElement": true,
|
||||||
|
"useCycleList": true,
|
||||||
|
"useDark": true,
|
||||||
|
"useDateFormat": true,
|
||||||
|
"useDebounce": true,
|
||||||
|
"useDebounceFn": true,
|
||||||
|
"useDebouncedRefHistory": true,
|
||||||
|
"useDeviceMotion": true,
|
||||||
|
"useDeviceOrientation": true,
|
||||||
|
"useDevicePixelRatio": true,
|
||||||
|
"useDevicesList": true,
|
||||||
|
"useDisplayMedia": true,
|
||||||
|
"useDocumentVisibility": true,
|
||||||
|
"useDraggable": true,
|
||||||
|
"useDropZone": true,
|
||||||
|
"useElementBounding": true,
|
||||||
|
"useElementByPoint": true,
|
||||||
|
"useElementHover": true,
|
||||||
|
"useElementSize": true,
|
||||||
|
"useElementVisibility": true,
|
||||||
|
"useEventBus": true,
|
||||||
|
"useEventListener": true,
|
||||||
|
"useEventSource": true,
|
||||||
|
"useEyeDropper": true,
|
||||||
|
"useFavicon": true,
|
||||||
|
"useFetch": true,
|
||||||
|
"useFileDialog": true,
|
||||||
|
"useFileSystemAccess": true,
|
||||||
|
"useFocus": true,
|
||||||
|
"useFocusWithin": true,
|
||||||
|
"useFps": true,
|
||||||
|
"useFullscreen": true,
|
||||||
|
"useGamepad": true,
|
||||||
|
"useGeolocation": true,
|
||||||
|
"useIdle": true,
|
||||||
|
"useImage": true,
|
||||||
|
"useInfiniteScroll": true,
|
||||||
|
"useIntersectionObserver": true,
|
||||||
|
"useInterval": true,
|
||||||
|
"useIntervalFn": true,
|
||||||
|
"useKeyModifier": true,
|
||||||
|
"useLastChanged": true,
|
||||||
|
"useLocalStorage": true,
|
||||||
|
"useMagicKeys": true,
|
||||||
|
"useManualRefHistory": true,
|
||||||
|
"useMediaControls": true,
|
||||||
|
"useMediaQuery": true,
|
||||||
|
"useMemoize": true,
|
||||||
|
"useMemory": true,
|
||||||
|
"useMounted": true,
|
||||||
|
"useMouse": true,
|
||||||
|
"useMouseInElement": true,
|
||||||
|
"useMousePressed": true,
|
||||||
|
"useMutationObserver": true,
|
||||||
|
"useNavigatorLanguage": true,
|
||||||
|
"useNetwork": true,
|
||||||
|
"useNow": true,
|
||||||
|
"useObjectUrl": true,
|
||||||
|
"useOffsetPagination": true,
|
||||||
|
"useOnline": true,
|
||||||
|
"usePageLeave": true,
|
||||||
|
"useParallax": true,
|
||||||
|
"usePermission": true,
|
||||||
|
"usePointer": true,
|
||||||
|
"usePointerSwipe": true,
|
||||||
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredDark": true,
|
||||||
|
"usePreferredLanguages": true,
|
||||||
|
"useRafFn": true,
|
||||||
|
"useRefHistory": true,
|
||||||
|
"useResizeObserver": true,
|
||||||
|
"useRoute": true,
|
||||||
|
"useRouter": true,
|
||||||
|
"useScreenOrientation": true,
|
||||||
|
"useScreenSafeArea": true,
|
||||||
|
"useScriptTag": true,
|
||||||
|
"useScroll": true,
|
||||||
|
"useScrollLock": true,
|
||||||
|
"useSessionStorage": true,
|
||||||
|
"useShare": true,
|
||||||
|
"useSlots": true,
|
||||||
|
"useSpeechRecognition": true,
|
||||||
|
"useSpeechSynthesis": true,
|
||||||
|
"useStepper": true,
|
||||||
|
"useStorage": true,
|
||||||
|
"useStorageAsync": true,
|
||||||
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
|
"useSwipe": true,
|
||||||
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
|
"useTextSelection": true,
|
||||||
|
"useTextareaAutosize": true,
|
||||||
|
"useThrottle": true,
|
||||||
|
"useThrottleFn": true,
|
||||||
|
"useThrottledRefHistory": true,
|
||||||
|
"useTimeAgo": true,
|
||||||
|
"useTimeout": true,
|
||||||
|
"useTimeoutFn": true,
|
||||||
|
"useTimeoutPoll": true,
|
||||||
|
"useTimestamp": true,
|
||||||
|
"useTitle": true,
|
||||||
|
"useToggle": true,
|
||||||
|
"useTransition": true,
|
||||||
|
"useUrlSearchParams": true,
|
||||||
|
"useUserMedia": true,
|
||||||
|
"useVModel": true,
|
||||||
|
"useVModels": true,
|
||||||
|
"useVibrate": true,
|
||||||
|
"useVirtualList": true,
|
||||||
|
"useWakeLock": true,
|
||||||
|
"useWebNotification": true,
|
||||||
|
"useWebSocket": true,
|
||||||
|
"useWebWorker": true,
|
||||||
|
"useWebWorkerFn": true,
|
||||||
|
"useWindowFocus": true,
|
||||||
|
"useWindowScroll": true,
|
||||||
|
"useWindowSize": true,
|
||||||
|
"watch": true,
|
||||||
|
"watchArray": true,
|
||||||
|
"watchAtMost": true,
|
||||||
|
"watchDebounced": true,
|
||||||
|
"watchEffect": true,
|
||||||
|
"watchIgnorable": true,
|
||||||
|
"watchOnce": true,
|
||||||
|
"watchPausable": true,
|
||||||
|
"watchPostEffect": true,
|
||||||
|
"watchSyncEffect": true,
|
||||||
|
"watchThrottled": true,
|
||||||
|
"watchTriggerable": true,
|
||||||
|
"watchWithFilter": true,
|
||||||
|
"whenever": true
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,8 @@ module.exports = defineConfig({
|
|||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'prettier',
|
'prettier',
|
||||||
'plugin:prettier/recommended'
|
'plugin:prettier/recommended',
|
||||||
|
'./.eslintrc-auto-import.json'
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'vue/script-setup-uses-vars': 'error',
|
'vue/script-setup-uses-vars': 'error',
|
||||||
|
@ -6,15 +6,14 @@ import progress from 'vite-plugin-progress'
|
|||||||
import EslintPlugin from 'vite-plugin-eslint'
|
import EslintPlugin from 'vite-plugin-eslint'
|
||||||
import PurgeIcons from 'vite-plugin-purge-icons'
|
import PurgeIcons from 'vite-plugin-purge-icons'
|
||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs'
|
import { ViteEjsPlugin } from 'vite-plugin-ejs'
|
||||||
|
import ElementPlus from 'unplugin-element-plus/vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
import viteCompression from 'vite-plugin-compression'
|
import viteCompression from 'vite-plugin-compression'
|
||||||
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
|
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
|
||||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||||
import {
|
|
||||||
createStyleImportPlugin,
|
|
||||||
ElementPlusResolve,
|
|
||||||
VxeTableResolve
|
|
||||||
} from 'vite-plugin-style-import'
|
|
||||||
export function createVitePlugins(VITE_APP_TITLE: string) {
|
export function createVitePlugins(VITE_APP_TITLE: string) {
|
||||||
const root = process.cwd()
|
const root = process.cwd()
|
||||||
// 路径查找
|
// 路径查找
|
||||||
@ -28,24 +27,47 @@ export function createVitePlugins(VITE_APP_TITLE: string) {
|
|||||||
progress(),
|
progress(),
|
||||||
PurgeIcons(),
|
PurgeIcons(),
|
||||||
vueSetupExtend(),
|
vueSetupExtend(),
|
||||||
createStyleImportPlugin({
|
ElementPlus(),
|
||||||
resolves: [ElementPlusResolve(), VxeTableResolve()],
|
AutoImport({
|
||||||
libs: [
|
include: [
|
||||||
|
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
||||||
|
/\.vue$/,
|
||||||
|
/\.vue\?vue/, // .vue
|
||||||
|
/\.md$/ // .md
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
'vue-router',
|
||||||
{
|
{
|
||||||
libraryName: 'element-plus',
|
'@/hooks/web/useI18n': ['useI18n'],
|
||||||
esModule: true,
|
'@/hooks/web/useXTable': ['useXTable'],
|
||||||
resolveStyle: (name) => {
|
'@/hooks/web/useMessage': ['useMessage'],
|
||||||
return `element-plus/es/components/${name.substring(3)}/style/css`
|
'@/hooks/web/useVxeCrudSchemas': ['useVxeCrudSchemas'],
|
||||||
}
|
'@/utils/formRules': ['required'],
|
||||||
},
|
'@/utils/dict': ['DICT_TYPE']
|
||||||
{
|
|
||||||
libraryName: 'vxe-table',
|
|
||||||
esModule: true,
|
|
||||||
resolveStyle: (name) => {
|
|
||||||
return `vxe-table/es/${name}/style.css`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
dts: 'src/types/auto-imports.d.ts',
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
eslintrc: {
|
||||||
|
enabled: false, // Default `false`
|
||||||
|
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
|
||||||
|
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
// 要搜索组件的目录的相对路径
|
||||||
|
dirs: ['src/components'],
|
||||||
|
// 组件的有效文件扩展名
|
||||||
|
extensions: ['vue', 'md'],
|
||||||
|
// 搜索子目录
|
||||||
|
deep: true,
|
||||||
|
include: [/\.vue$/, /\.vue\?vue/],
|
||||||
|
// 生成自定义 `auto-components.d.ts` 全局声明
|
||||||
|
dts: 'src/types/auto-components.d.ts',
|
||||||
|
// 自定义组件的解析器
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
exclude: [/[\\/]node_modules[\\/]/]
|
||||||
}),
|
}),
|
||||||
EslintPlugin({
|
EslintPlugin({
|
||||||
cache: false,
|
cache: false,
|
||||||
|
@ -21,6 +21,7 @@ const include = [
|
|||||||
'vxe-table/es/style',
|
'vxe-table/es/style',
|
||||||
'web-storage-cache',
|
'web-storage-cache',
|
||||||
'element-plus',
|
'element-plus',
|
||||||
|
'element-plus/es',
|
||||||
'element-plus/es/locale/lang/zh-cn',
|
'element-plus/es/locale/lang/zh-cn',
|
||||||
'element-plus/es/locale/lang/en',
|
'element-plus/es/locale/lang/en',
|
||||||
'@iconify/iconify',
|
'@iconify/iconify',
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
export const styleImportPlugin = {
|
|
||||||
libs: [
|
|
||||||
{
|
|
||||||
libraryName: 'element-plus',
|
|
||||||
esModule: true,
|
|
||||||
resolveStyle: (name) => {
|
|
||||||
return `element-plus/es/components/${name.substring(3)}/style/css`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
libraryName: 'vxe-table',
|
|
||||||
esModule: true,
|
|
||||||
resolveStyle: (name) => {
|
|
||||||
return `vxe-table/es/${name}/style.css`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yudao-ui-admin-vue3",
|
"name": "yudao-ui-admin-vue3",
|
||||||
"version": "1.6.6-snapshot.1912",
|
"version": "1.6.6-snapshot.1925",
|
||||||
"description": "基于vue3、vite4、element-plus、typesScript",
|
"description": "基于vue3、vite4、element-plus、typesScript",
|
||||||
"author": "xingyu",
|
"author": "xingyu",
|
||||||
"private": false,
|
"private": false,
|
||||||
@ -25,12 +25,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/iconify": "^3.0.1",
|
"@iconify/iconify": "^3.0.1",
|
||||||
"@vueuse/core": "^9.10.0",
|
"@vueuse/core": "^9.11.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||||
"@zxcvbn-ts/core": "^2.1.0",
|
"@zxcvbn-ts/core": "^2.1.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.2.2",
|
"axios": "^1.2.3",
|
||||||
"cropperjs": "^1.5.13",
|
"cropperjs": "^1.5.13",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mitt": "^3.0.0",
|
"mitt": "^3.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^2.0.28",
|
"pinia": "^2.0.29",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
@ -57,7 +57,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.4.2",
|
"@commitlint/cli": "^17.4.2",
|
||||||
"@commitlint/config-conventional": "^17.4.2",
|
"@commitlint/config-conventional": "^17.4.2",
|
||||||
"@iconify/json": "^2.2.6",
|
"@iconify/json": "^2.2.8",
|
||||||
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||||
"@purge-icons/generated": "^0.9.0",
|
"@purge-icons/generated": "^0.9.0",
|
||||||
"@types/intro.js": "^5.1.0",
|
"@types/intro.js": "^5.1.0",
|
||||||
@ -66,14 +66,14 @@
|
|||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||||
"@typescript-eslint/parser": "^5.48.1",
|
"@typescript-eslint/parser": "^5.48.2",
|
||||||
"@vitejs/plugin-legacy": "^3.0.1",
|
"@vitejs/plugin-legacy": "^3.0.1",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"consola": "^2.15.3",
|
"consola": "^2.15.3",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-config-prettier": "^8.6.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-define-config": "^1.14.0",
|
"eslint-define-config": "^1.14.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
@ -82,8 +82,8 @@
|
|||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.6",
|
||||||
"prettier": "^2.8.2",
|
"prettier": "^2.8.3",
|
||||||
"rimraf": "^4.0.4",
|
"rimraf": "^4.1.1",
|
||||||
"rollup": "^3.10.0",
|
"rollup": "^3.10.0",
|
||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"stylelint": "^14.16.1",
|
"stylelint": "^14.16.1",
|
||||||
@ -94,13 +94,15 @@
|
|||||||
"stylelint-order": "^6.0.1",
|
"stylelint-order": "^6.0.1",
|
||||||
"terser": "^5.16.1",
|
"terser": "^5.16.1",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
|
"unplugin-auto-import": "^0.12.1",
|
||||||
|
"unplugin-element-plus": "^0.4.1",
|
||||||
|
"unplugin-vue-components": "^0.22.12",
|
||||||
"vite": "4.0.4",
|
"vite": "4.0.4",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-ejs": "^1.6.4",
|
"vite-plugin-ejs": "^1.6.4",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-progress": "^0.0.6",
|
"vite-plugin-progress": "^0.0.6",
|
||||||
"vite-plugin-purge-icons": "^0.9.2",
|
"vite-plugin-purge-icons": "^0.9.2",
|
||||||
"vite-plugin-style-import": "2.0.0",
|
|
||||||
"vite-plugin-svg-icons": "^2.0.1",
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||||
"vite-plugin-windicss": "^1.8.10",
|
"vite-plugin-windicss": "^1.8.10",
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import { isDark } from '@/utils/is'
|
import { isDark } from '@/utils/is'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { ConfigGlobal } from '@/components/ConfigGlobal'
|
|
||||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
@ -33,7 +33,7 @@ export const getTenantIdByNameApi = (name: string) => {
|
|||||||
|
|
||||||
// 登出
|
// 登出
|
||||||
export const loginOutApi = () => {
|
export const loginOutApi = () => {
|
||||||
return request.delete({ url: '/system/auth/logout' })
|
return request.post({ url: '/system/auth/logout' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户权限信息
|
// 获取用户权限信息
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { provide, computed, watch, onMounted } from 'vue'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { ElConfigProvider } from 'element-plus'
|
|
||||||
import { useLocaleStore } from '@/store/modules/locale'
|
import { useLocaleStore } from '@/store/modules/locale'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { setCssVar } from '@/utils'
|
import { setCssVar } from '@/utils'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { ElementPlusSize } from '@/types/elementPlus'
|
import { ElementPlusSize } from '@/types/elementPlus'
|
||||||
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
|
||||||
const { variables } = useDesign()
|
const { variables } = useDesign()
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElCard } from 'element-plus'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { Sticky } from '@/components/Sticky'
|
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, computed, watch, onMounted, unref, toRef, PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { isNumber } from '@/utils/is'
|
import { isNumber } from '@/utils/is'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
@ -1,19 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { ElMessage } from 'element-plus'
|
||||||
ElInput,
|
import { PropType } from 'vue'
|
||||||
ElInputNumber,
|
|
||||||
ElDialog,
|
|
||||||
ElTabs,
|
|
||||||
ElTabPane,
|
|
||||||
ElSelect,
|
|
||||||
ElOption,
|
|
||||||
ElForm,
|
|
||||||
ElFormItem,
|
|
||||||
ElRadioGroup,
|
|
||||||
ElRadioButton,
|
|
||||||
ElMessage
|
|
||||||
} from 'element-plus'
|
|
||||||
import { ref, computed, onMounted, reactive, watch, PropType } from 'vue'
|
|
||||||
interface shortcutsType {
|
interface shortcutsType {
|
||||||
text: string
|
text: string
|
||||||
value: string
|
value: string
|
||||||
|
@ -116,11 +116,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { dataURLtoBlob } from '@/utils/filt'
|
import { dataURLtoBlob } from '@/utils/filt'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ElUpload, ElAvatar, ElTooltip, ElSpace } from 'element-plus'
|
|
||||||
import { Dialog } from '@/components/Dialog'
|
|
||||||
import { CropperImage } from '@/components/Cropper'
|
|
||||||
import type { CropendResult, Cropper } from './types'
|
import type { CropendResult, Cropper } from './types'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
|
||||||
|
@ -11,21 +11,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { CSSProperties, PropType } from 'vue'
|
||||||
computed,
|
|
||||||
CSSProperties,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
PropType,
|
|
||||||
ref,
|
|
||||||
unref,
|
|
||||||
useAttrs
|
|
||||||
} from 'vue'
|
|
||||||
import Cropper from 'cropperjs'
|
import Cropper from 'cropperjs'
|
||||||
import 'cropperjs/dist/cropper.css'
|
import 'cropperjs/dist/cropper.css'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
|
||||||
type Options = Cropper.Options
|
type Options = Cropper.Options
|
||||||
|
|
||||||
|
@ -13,9 +13,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { ref, watch, watchEffect } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import CopperModal from './CopperModal.vue'
|
import CopperModal from './CopperModal.vue'
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { ref, unref, PropType, computed, useAttrs, useSlots } from 'vue'
|
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { DescriptionsSchema } from '@/types/descriptions'
|
import { DescriptionsSchema } from '@/types/descriptions'
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ const props = defineProps({
|
|||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<Recordable>,
|
type: Object as PropType<any>,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElDialog, ElScrollbar } from 'element-plus'
|
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
|
||||||
import { isNumber } from '@/utils/is'
|
import { isNumber } from '@/utils/is'
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
@ -1,56 +1,53 @@
|
|||||||
<script setup lang="ts">
|
<script lang="tsx">
|
||||||
import { onMounted, onUpdated, PropType, ref } from 'vue'
|
import { defineComponent, PropType, ref } from 'vue'
|
||||||
import { getDictOptions, DictDataType } from '@/utils/dict'
|
import { isHexColor } from '@/utils/color'
|
||||||
import { ElTag } from 'element-plus'
|
import { ElTag } from 'element-plus'
|
||||||
const props = defineProps({
|
import { DictDataType, getDictOptions } from '@/utils/dict'
|
||||||
type: {
|
|
||||||
type: String as PropType<string>,
|
export default defineComponent({
|
||||||
required: true
|
name: 'DictTag',
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String as PropType<string>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
value: {
|
setup(props) {
|
||||||
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
const dictData = ref<DictDataType>()
|
||||||
required: true
|
const getDictObj = (dictType: string, value: string) => {
|
||||||
|
const dictOptions = getDictOptions(dictType)
|
||||||
|
dictOptions.forEach((dict: DictDataType) => {
|
||||||
|
if (dict.value === value) {
|
||||||
|
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
|
||||||
|
dict.colorType = ''
|
||||||
|
}
|
||||||
|
dictData.value = dict
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const rederDictTag = () => {
|
||||||
|
if (!props.type) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
getDictObj(props.type, props.value.toString())
|
||||||
|
return (
|
||||||
|
<ElTag
|
||||||
|
type={dictData.value?.colorType}
|
||||||
|
color={
|
||||||
|
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass)
|
||||||
|
? dictData.value?.cssClass
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dictData.value?.label}
|
||||||
|
</ElTag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return () => rederDictTag()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const dictData = ref<DictDataType>()
|
|
||||||
const getDictObj = (dictType: string, value: string) => {
|
|
||||||
const dictOptions = getDictOptions(dictType)
|
|
||||||
dictOptions.forEach((dict: DictDataType) => {
|
|
||||||
if (dict.value === value) {
|
|
||||||
dictData.value = dict
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
return getDictObj(props.type, props.value?.toString())
|
|
||||||
})
|
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
getDictObj(props.type, props.value?.toString())
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
|
||||||
<!-- 默认样式 -->
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
dictData?.colorType === 'default' ||
|
|
||||||
dictData?.colorType === '' ||
|
|
||||||
dictData?.colorType === undefined
|
|
||||||
"
|
|
||||||
:key="dictData?.value.toString()"
|
|
||||||
:class="dictData?.cssClass"
|
|
||||||
>
|
|
||||||
{{ dictData?.label }}
|
|
||||||
</span>
|
|
||||||
<!-- Tag 样式 -->
|
|
||||||
<ElTag
|
|
||||||
v-else
|
|
||||||
:disable-transitions="true"
|
|
||||||
:key="dictData?.value + ''"
|
|
||||||
:type="dictData?.colorType === 'primary' ? 'success' : dictData?.colorType"
|
|
||||||
:class="dictData?.cssClass"
|
|
||||||
>
|
|
||||||
{{ dictData?.label }}
|
|
||||||
</ElTag>
|
|
||||||
</template>
|
|
||||||
|
@ -4,7 +4,7 @@ import echarts from '@/plugins/echarts'
|
|||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
import 'echarts-wordcloud'
|
import 'echarts-wordcloud'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { computed, PropType, ref, unref, watch, onMounted, onBeforeUnmount, onActivated } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { isString } from '@/utils/is'
|
import { isString } from '@/utils/is'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
import { IDomEditor, IEditorConfig, i18nChangeLanguage } from '@wangeditor/editor'
|
import { IDomEditor, IEditorConfig, i18nChangeLanguage } from '@wangeditor/editor'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
@ -3,7 +3,6 @@ import pageError from '@/assets/svgs/404.svg'
|
|||||||
import networkError from '@/assets/svgs/500.svg'
|
import networkError from '@/assets/svgs/500.svg'
|
||||||
import noPermission from '@/assets/svgs/403.svg'
|
import noPermission from '@/assets/svgs/403.svg'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
|
||||||
|
|
||||||
interface ErrorMap {
|
interface ErrorMap {
|
||||||
url: string
|
url: string
|
||||||
|