提交 3449882a authored 作者: xingyu's avatar xingyu

refactor: captcha-plus

上级 4f43bc2d
...@@ -49,6 +49,7 @@ ...@@ -49,6 +49,7 @@
<!-- Bpm 工作流相关 --> <!-- Bpm 工作流相关 -->
<flowable.version>6.8.0</flowable.version> <flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 --> <!-- 工具类相关 -->
<captcha-plus.version>1.0.0</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>
...@@ -564,6 +565,12 @@ ...@@ -564,6 +565,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>
......
...@@ -19,15 +19,13 @@ ...@@ -19,15 +19,13 @@
<dependencies> <dependencies>
<!-- Spring 核心 --> <!-- Spring 核心 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>com.xingyuv</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter-captcha-plus</artifactId>
<scope>provided</scope>
</dependency> </dependency>
<!-- Spring 核心 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency> </dependency>
<!-- DB 相关 --> <!-- DB 相关 -->
......
...@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.captcha.core.service; ...@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.captcha.core.service;
import com.anji.captcha.service.CaptchaCacheService; import com.anji.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;
......
package com.anji.captcha.config;
import com.anji.captcha.config.AjCaptchaServiceAutoConfiguration;
import com.anji.captcha.config.AjCaptchaStorageAutoConfiguration;
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 {
}
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;
}
}
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());
}
}
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();
}
}
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();
}
}
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";
}
/*
*Copyright © 2018 anji-plus
*安吉加加信息技术有限公司
*http://www.anji-plus.com
*All rights reserved.
*/
package com.anji.captcha.model.common;
import com.anji.captcha.model.common.ResponseModel;
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 com.anji.captcha.model.common.ResponseModel parseError(Object... fieldNames) {
com.anji.captcha.model.common.ResponseModel errorMessage = new ResponseModel();
String newDesc = MessageFormat.format(this.desc, fieldNames);
errorMessage.setRepCode(this.code);
errorMessage.setRepMsg(newDesc);
return errorMessage;
}
}
/*
*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;
}
}
/*
*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();
}
}
/*
*Copyright © 2018 anji-plus
*安吉加加信息技术有限公司
*http://www.anji-plus.com
*All rights reserved.
*/
package com.anji.captcha.model.vo;
import com.anji.captcha.model.vo.PointVO;
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;
}
}
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;
/**
* 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);
}
}
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.*;
@Data
@ConfigurationProperties(AjCaptchaProperties.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 = StorageType.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;
}
}
/*
*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;
}
}
/*
*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);
}
/*
*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.service.impl.CaptchaServiceFactory;
import com.anji.captcha.service.impl.FrequencyLimitHandler;
import com.anji.captcha.util.AESUtil;
import com.anji.captcha.util.CacheUtil;
import com.anji.captcha.util.ImageUtils;
import com.anji.captcha.util.MD5Util;
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 com.anji.captcha.service.impl.FrequencyLimitHandler.DefaultLimitHandler(config, getCacheService(cacheType));
}
}
}
protected CaptchaCacheService getCacheService(String cacheType) {
return CaptchaServiceFactory.getCache(cacheType);
}
@Override
public void destroy(Properties config) {
}
private static com.anji.captcha.service.impl.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;
}
}
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";
}
}
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());
}
}
/*
*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 com.anji.captcha.service.impl.AbstractCaptchaService;
import com.anji.captcha.service.impl.CaptchaServiceFactory;
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();
}
}
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;
}
}
}
\ No newline at end of file
/*
*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.util.RandomUtils;
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);
}
}
/*
*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();
}
}
package com.anji.captcha.util;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import com.anji.captcha.util.StreamUtils;
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();
}
}
}
/*
*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 = com.anji.captcha.util.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 = com.anji.captcha.util.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;
}
}
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());
}
}
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);
}
}
/*
*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();
}
}
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 {
}
}
}
com.anji.captcha.service.impl.BlockPuzzleCaptchaServiceImpl
com.anji.captcha.service.impl.ClickWordCaptchaServiceImpl
com.anji.captcha.service.impl.DefaultCaptchaServiceImpl
\ No newline at end of file
com.anji.captcha.config.AjCaptchaAutoConfiguration
cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration cn.iocoder.yudao.framework.captcha.config.YudaoCaptchaConfiguration
\ No newline at end of file
文泉驿是一个开源汉字字体项目
由旅美学者房骞骞(FangQ)
于2004年10月创建
集中力量解决GNU/Linux
高质量中文字体匮乏的状况
目前,文泉驿已经开发并发布了
第一个完整覆盖GB18030汉字
(包含27000多个汉字)
的多规格点阵汉字字型文件
第一个覆盖GBK字符集的
开源矢量字型文件(文泉驿正黑)
并提供了目前包含字符数目最多的
开源字体——GNU Unifont——中
绝大多数中日韩文相关的符号
这些字型文件已经逐渐成为
主流Linux/Unix发行版
中文桌面的首选中文字体
目前Ubuntu、Fedora、Slackware
Magic Linux、CDLinux
使用文泉驿作为默认中文字体
Debian、Gentoo、Mandriva
ArchLinux、Frugalware
则提供了官方源支持
而FreeBSD则在其ports中有提供
所以,今天我们所要分享的就是
文泉驿正黑体
可在Linux/UNIX,Windows
Mac OS和嵌入式操作系统中使用
\ No newline at end of file
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.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO; import com.anji.captcha.model.vo.CaptchaVO;
...@@ -52,8 +53,7 @@ public class CaptchaController { ...@@ -52,8 +53,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 +61,4 @@ public class CaptchaController { ...@@ -61,12 +61,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;
}
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论