@ -0,0 +1,24 @@ |
|||
package com.win.framework.captcha.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
|
|||
@ConfigurationProperties(prefix = "win.captcha") |
|||
@Data |
|||
public class CaptchaProperties { |
|||
|
|||
/** |
|||
* 验证码的开关 |
|||
*/ |
|||
private Boolean enable; |
|||
|
|||
/** |
|||
* 验证码类型 math 数组计算 char 字符验证 |
|||
*/ |
|||
private String captchaType; |
|||
|
|||
/** |
|||
* 验证码有效期 |
|||
*/ |
|||
private Integer captchaExpiration; |
|||
} |
@ -1,29 +1,77 @@ |
|||
package com.win.framework.captcha.config; |
|||
|
|||
import com.win.framework.captcha.core.service.RedisCaptchaServiceImpl; |
|||
import com.xingyuv.captcha.properties.AjCaptchaProperties; |
|||
import com.xingyuv.captcha.service.CaptchaCacheService; |
|||
import com.xingyuv.captcha.service.impl.CaptchaServiceFactory; |
|||
import com.google.code.kaptcha.impl.DefaultKaptcha; |
|||
import com.google.code.kaptcha.util.Config; |
|||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.data.redis.core.StringRedisTemplate; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.Properties; |
|||
|
|||
import static com.google.code.kaptcha.Constants.*; |
|||
|
|||
@AutoConfiguration |
|||
public class WinCaptchaConfiguration { |
|||
|
|||
@Resource |
|||
private StringRedisTemplate stringRedisTemplate; |
|||
|
|||
@Bean |
|||
public CaptchaCacheService captchaCacheService(AjCaptchaProperties config) { |
|||
// 缓存类型 redis/local/....
|
|||
CaptchaCacheService ret = CaptchaServiceFactory.getCache(config.getCacheType().name()); |
|||
if (ret instanceof RedisCaptchaServiceImpl) { |
|||
((RedisCaptchaServiceImpl) ret).setStringRedisTemplate(stringRedisTemplate); |
|||
} |
|||
return ret; |
|||
@Bean(name = "captchaProducer") |
|||
public DefaultKaptcha getKaptchaBean() { |
|||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); |
|||
Properties properties = new Properties(); |
|||
// 是否有边框 默认为true 我们可以自己设置yes,no
|
|||
properties.setProperty(KAPTCHA_BORDER, "yes"); |
|||
// 验证码文本字符颜色 默认为Color.BLACK
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); |
|||
// 验证码图片宽度 默认为200
|
|||
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); |
|||
// 验证码图片高度 默认为50
|
|||
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); |
|||
// 验证码文本字符大小 默认为40
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); |
|||
// KAPTCHA_SESSION_KEY
|
|||
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode"); |
|||
// 验证码文本字符长度 默认为5
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); |
|||
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); |
|||
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
|||
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); |
|||
Config config = new Config(properties); |
|||
defaultKaptcha.setConfig(config); |
|||
return defaultKaptcha; |
|||
} |
|||
|
|||
@Bean(name = "captchaProducerMath") |
|||
public DefaultKaptcha getKaptchaBeanMath() { |
|||
DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); |
|||
Properties properties = new Properties(); |
|||
// 是否有边框 默认为true 我们可以自己设置yes,no
|
|||
properties.setProperty(KAPTCHA_BORDER, "yes"); |
|||
// 边框颜色 默认为Color.BLACK
|
|||
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90"); |
|||
// 验证码文本字符颜色 默认为Color.BLACK
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); |
|||
// 验证码图片宽度 默认为200
|
|||
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160"); |
|||
// 验证码图片高度 默认为50
|
|||
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60"); |
|||
// 验证码文本字符大小 默认为40
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35"); |
|||
// KAPTCHA_SESSION_KEY
|
|||
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath"); |
|||
// 验证码文本生成器
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.win.tms.framework.config.KaptchaTextCreator"); |
|||
// 验证码文本字符间距 默认为2
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3"); |
|||
// 验证码文本字符长度 默认为5
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6"); |
|||
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
|
|||
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); |
|||
// 验证码噪点颜色 默认为Color.BLACK
|
|||
properties.setProperty(KAPTCHA_NOISE_COLOR, "white"); |
|||
// 干扰实现类
|
|||
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise"); |
|||
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
|
|||
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); |
|||
Config config = new Config(properties); |
|||
defaultKaptcha.setConfig(config); |
|||
return defaultKaptcha; |
|||
} |
|||
} |
|||
|
@ -1,28 +0,0 @@ |
|||
package com.win.framework.captcha.core.enums; |
|||
|
|||
/** |
|||
* 验证码 Redis Key 枚举类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public interface CaptchaRedisKeyConstants { |
|||
|
|||
/** |
|||
* 验证码的请求限流 |
|||
* |
|||
* KEY 格式:AJ.CAPTCHA.REQ.LIMIT-%s-%s |
|||
* VALUE 数据类型:String // 例如说:验证失败 5 次,get 接口锁定
|
|||
* 过期时间:60 秒 |
|||
*/ |
|||
String AJ_CAPTCHA_REQ_LIMIT = "AJ.CAPTCHA.REQ.LIMIT-%s-%s"; |
|||
|
|||
/** |
|||
* 验证码的坐标 |
|||
* |
|||
* KEY 格式:RUNNING:CAPTCHA:%s // AbstractCaptchaService.REDIS_CAPTCHA_KEY
|
|||
* VALUE 数据类型:String // PointVO.class {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5}
|
|||
* 过期时间:120 秒 |
|||
*/ |
|||
String AJ_CAPTCHA_RUNNING = "RUNNING:CAPTCHA:%s"; |
|||
|
|||
} |
@ -1,57 +0,0 @@ |
|||
package com.win.framework.captcha.core.service; |
|||
|
|||
import com.xingyuv.captcha.service.CaptchaCacheService; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.NoArgsConstructor; |
|||
import org.springframework.data.redis.core.StringRedisTemplate; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* 基于 Redis 实现验证码的存储 |
|||
* |
|||
* @author 星语 |
|||
*/ |
|||
@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建
|
|||
@AllArgsConstructor |
|||
public class RedisCaptchaServiceImpl implements CaptchaCacheService { |
|||
|
|||
@Resource // 保证 aj-captcha 的 SPI 创建时的注入
|
|||
private StringRedisTemplate stringRedisTemplate; |
|||
|
|||
@Override |
|||
public String type() { |
|||
return "redis"; |
|||
} |
|||
|
|||
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) { |
|||
this.stringRedisTemplate = stringRedisTemplate; |
|||
} |
|||
|
|||
@Override |
|||
public void set(String key, String value, long expiresInSeconds) { |
|||
stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); |
|||
} |
|||
|
|||
@Override |
|||
public boolean exists(String key) { |
|||
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); |
|||
} |
|||
|
|||
@Override |
|||
public void delete(String key) { |
|||
stringRedisTemplate.delete(key); |
|||
} |
|||
|
|||
@Override |
|||
public String get(String key) { |
|||
return stringRedisTemplate.opsForValue().get(key); |
|||
} |
|||
|
|||
@Override |
|||
public Long increment(String key, long val) { |
|||
return stringRedisTemplate.opsForValue().increment(key,val); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,15 @@ |
|||
package com.win.framework.captcha.enums; |
|||
|
|||
/** |
|||
* 验证码 Redis Key 枚举类 |
|||
* |
|||
* @author 闻荫源码 |
|||
*/ |
|||
public interface CaptchaRedisKeyConstants { |
|||
|
|||
/** |
|||
* 验证码 redis key |
|||
*/ |
|||
String CAPTCHA_CODE_KEY = "captcha_codes:"; |
|||
|
|||
} |
@ -1 +0,0 @@ |
|||
com.win.framework.captcha.core.service.RedisCaptchaServiceImpl |
@ -1 +1,2 @@ |
|||
com.win.framework.captcha.config.CaptchaProperties |
|||
com.win.framework.captcha.config.WinCaptchaConfiguration |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 29 KiB |
@ -1,4 +1,4 @@ |
|||
package com.win.module.system.util.redis; |
|||
package com.win.framework.redis.util; |
|||
|
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.data.redis.core.BoundSetOperations; |
@ -1,2 +1,3 @@ |
|||
com.win.framework.redis.config.WinRedisAutoConfiguration |
|||
com.win.framework.redis.config.WinCacheAutoConfiguration |
|||
com.win.framework.redis.config.WinCacheAutoConfiguration |
|||
com.win.framework.redis.util.RedisCache |
@ -1,61 +1,83 @@ |
|||
package com.win.module.system.controller.admin.captcha; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.win.framework.common.util.servlet.ServletUtils; |
|||
import com.win.framework.operatelog.core.annotations.OperateLog; |
|||
import com.xingyuv.captcha.model.common.ResponseModel; |
|||
import com.xingyuv.captcha.model.vo.CaptchaVO; |
|||
import com.xingyuv.captcha.service.CaptchaService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import cn.hutool.core.codec.Base64; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import com.google.code.kaptcha.Producer; |
|||
import com.win.framework.captcha.config.CaptchaProperties; |
|||
import com.win.framework.captcha.enums.CaptchaRedisKeyConstants; |
|||
import com.win.framework.common.pojo.CommonResult; |
|||
import com.win.framework.redis.util.RedisCache; |
|||
import com.win.module.system.enums.ErrorCodeConstants; |
|||
import org.springframework.util.FastByteArrayOutputStream; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import javax.annotation.Resource; |
|||
import javax.annotation.security.PermitAll; |
|||
import javax.servlet.http.HttpServletRequest; |
|||
import javax.imageio.ImageIO; |
|||
import javax.servlet.http.HttpServletResponse; |
|||
import java.awt.image.BufferedImage; |
|||
import java.io.IOException; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static com.win.framework.common.pojo.CommonResult.error; |
|||
import static com.win.framework.common.pojo.CommonResult.success; |
|||
|
|||
/** |
|||
* 验证码 |
|||
* |
|||
* @author 闻荫源码 |
|||
* 验证码操作处理 |
|||
* |
|||
* @author win |
|||
*/ |
|||
@Tag(name = "管理后台 - 验证码") |
|||
@RestController("adminCaptchaController") |
|||
@RequestMapping("/system/captcha") |
|||
@RestController |
|||
public class CaptchaController { |
|||
|
|||
@Resource(name = "captchaProducer") |
|||
private Producer captchaProducer; |
|||
@Resource |
|||
private CaptchaService captchaService; |
|||
|
|||
@PostMapping({"/get"}) |
|||
@Operation(summary = "获得验证码") |
|||
@PermitAll |
|||
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
|
|||
public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) { |
|||
assert request.getRemoteHost() != null; |
|||
data.setBrowserInfo(getRemoteId(request)); |
|||
return captchaService.get(data); |
|||
} |
|||
|
|||
@PostMapping("/check") |
|||
@Operation(summary = "校验验证码") |
|||
@PermitAll |
|||
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
|
|||
public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) { |
|||
data.setBrowserInfo(getRemoteId(request)); |
|||
return captchaService.check(data); |
|||
} |
|||
private RedisCache redisCache; |
|||
@Resource(name = "captchaProducerMath") |
|||
private Producer captchaProducerMath; |
|||
@Resource |
|||
private CaptchaProperties captchaProperties; |
|||
|
|||
public static String getRemoteId(HttpServletRequest request) { |
|||
String ip = ServletUtils.getClientIP(request); |
|||
String ua = request.getHeader("user-agent"); |
|||
if (StrUtil.isNotBlank(ip)) { |
|||
return ip + ua; |
|||
/** |
|||
* 生成验证码 |
|||
*/ |
|||
@GetMapping("/captchaImage") |
|||
public CommonResult<Object> getCode(HttpServletResponse response) throws IOException { |
|||
Map<String, Object> data = new HashMap<>(); |
|||
boolean captchaEnabled = captchaProperties.getEnable(); |
|||
data.put("captchaEnabled", captchaEnabled); |
|||
if (!captchaEnabled) { |
|||
return success(data); |
|||
} |
|||
// 保存验证码信息
|
|||
String uuid = IdUtil.simpleUUID(); |
|||
String verifyKey = CaptchaRedisKeyConstants.CAPTCHA_CODE_KEY + uuid; |
|||
String capStr; |
|||
String code = null; |
|||
BufferedImage image = null; |
|||
// 生成验证码
|
|||
String captchaType = captchaProperties.getCaptchaType(); |
|||
if ("math".equals(captchaType)) { |
|||
String capText = captchaProducerMath.createText(); |
|||
capStr = capText.substring(0, capText.lastIndexOf("@")); |
|||
code = capText.substring(capText.lastIndexOf("@") + 1); |
|||
image = captchaProducerMath.createImage(capStr); |
|||
} else if ("char".equals(captchaType)) { |
|||
capStr = code = captchaProducer.createText(); |
|||
image = captchaProducer.createImage(capStr); |
|||
} |
|||
return request.getRemoteAddr() + ua; |
|||
redisCache.setCacheObject(verifyKey, code, captchaProperties.getCaptchaExpiration(), TimeUnit.MINUTES); |
|||
// 转换流信息写出
|
|||
FastByteArrayOutputStream os = new FastByteArrayOutputStream(); |
|||
try { |
|||
ImageIO.write(image, "jpg", os); |
|||
} catch (IOException e) { |
|||
return error(ErrorCodeConstants.GENERATE_IMAGE_ERROR); |
|||
} |
|||
data.put("uuid", uuid); |
|||
data.put("img", Base64.encode(os.toByteArray())); |
|||
return success(data); |
|||
} |
|||
|
|||
} |
|||
|