diff --git a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 9844b3f72..f46f03b7f 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -1,41 +1,21 @@ package org.dromara.web.controller; +import cloud.tianai.captcha.application.vo.ImageCaptchaVO; +import cloud.tianai.captcha.common.response.ApiResponse; +import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack; import cn.dev33.satoken.annotation.SaIgnore; -import cn.hutool.captcha.generator.CodeGenerator; -import cn.hutool.captcha.generator.MathGenerator; -import cn.hutool.captcha.generator.RandomGenerator; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.RandomUtil; +import org.dromara.common.captcha.utils.CaptchaUtils; +import org.dromara.common.captcha.utils.CaptchaUtils.CaptchaVo; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.dromara.common.core.constant.Constants; -import org.dromara.common.core.constant.GlobalConstants; import org.dromara.common.core.domain.R; -import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; -import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.regex.RegexValidator; import org.dromara.common.mail.config.properties.MailProperties; -import org.dromara.common.mail.core.MailBuilder; import org.dromara.common.redis.annotation.RateLimiter; -import org.dromara.common.redis.enums.LimitType; -import org.dromara.common.redis.utils.RedisUtils; -import org.dromara.common.web.config.properties.CaptchaProperties; -import org.dromara.common.web.core.WaveAndCircleCaptcha; -import org.dromara.sms4j.api.SmsBlend; -import org.dromara.sms4j.api.entity.SmsResponse; -import org.dromara.sms4j.core.factory.SmsFactory; -import org.springframework.expression.Expression; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.awt.*; -import java.time.Duration; -import java.util.LinkedHashMap; +import org.springframework.web.bind.annotation.*; /** * 验证码操作处理 @@ -47,9 +27,10 @@ import java.util.LinkedHashMap; @Validated @RequiredArgsConstructor @RestController +@RequestMapping("/resource/captcha") public class CaptchaController { - private final CaptchaProperties captchaProperties; + private final CaptchaUtils captchaUtils; private final MailProperties mailProperties; /** @@ -59,25 +40,9 @@ public class CaptchaController { * @return 操作结果 */ @RateLimiter(key = "#phoneNumber", time = 60, count = 1) - @GetMapping("/resource/sms/code") - public R smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) { - if (!RegexValidator.isMobile(phoneNumber)) { - return R.fail("请输入正确的手机号!"); - } - String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber; - String code = RandomUtil.randomNumbers(4); - // 验证码模板id 自行处理 (查数据库或写死均可) - String templateId = ""; - LinkedHashMap map = new LinkedHashMap<>(1); - map.put("code", code); - SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); - SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map); - if (!smsResponse.isSuccess()) { - log.error("验证码短信发送异常 => {}", smsResponse); - Object data = smsResponse.getData(); - return R.fail(data == null ? "验证码短信发送失败" : data.toString()); - } - RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); + @GetMapping("/sms") + public R sendSmsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) { + captchaUtils.generateSmsCaptcha(phoneNumber); return R.ok(); } @@ -87,96 +52,59 @@ public class CaptchaController { * @param email 邮箱 * @return 操作结果 */ - @GetMapping("/resource/email/code") - public R emailCode(@NotBlank(message = "{user.email.not.blank}") String email) { + @GetMapping("/email") + public R sendEmailCode(@NotBlank(message = "{user.email.not.blank}") String email) { if (!mailProperties.getEnabled()) { return R.fail("当前系统没有开启邮箱功能!"); } if (!RegexValidator.isEmail(email)) { return R.fail("请输入正确的邮箱地址!"); } - SpringUtils.getAopProxy(this).emailCodeImpl(email); + SpringUtils.getAopProxy(this).captchaUtils.generateEmailCaptcha(email); return R.ok(); } - /** - * 发送邮箱验证码的实际执行方法,拆分出来避免开关关闭时仍触发限流。 - * - * @param email 邮箱 - */ - @RateLimiter(key = "#email", time = 60, count = 1) - public void emailCodeImpl(String email) { - String key = GlobalConstants.CAPTCHA_CODE_KEY + email; - String code = RandomUtil.randomNumbers(4); - try { - MailBuilder.of() - .to(email) - .subject("登录验证码") - .text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。") - .send(); - RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); - } catch (Exception e) { - log.error("验证码邮件发送异常 => {}", e.getMessage()); - throw new ServiceException(e.getMessage()); - } - } - /** * 获取图片验证码。 * * @return 验证码信息 */ - @GetMapping("/auth/code") - public R getCode() { - boolean captchaEnabled = captchaProperties.getEnable(); - if (!captchaEnabled) { - return R.ok(new CaptchaVo(false, null, null)); - } - return R.ok(SpringUtils.getAopProxy(this).getCodeImpl()); + @GetMapping("/image") + public R getImageCaptcha() { + return R.ok(captchaUtils.generateImageCaptcha()); } /** - * 实际生成图片验证码并缓存结果。 + * 获取【行为滑块验证码】生成接口 + *

返回滑块背景图、缺口图、验证码key

* - * @return 验证码信息 + * @return 行为验证码vo */ - @RateLimiter(time = 60, count = 10, limitType = LimitType.IP) - public CaptchaVo getCodeImpl() { - // 保存验证码信息 - String uuid = IdUtil.simpleUUID(); - String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; - // 生成验证码 - String captchaType = captchaProperties.getType(); - CodeGenerator codeGenerator; - if ("math".equals(captchaType)) { - codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false); - } else { - codeGenerator = new RandomGenerator(captchaProperties.getCharLength()); - } - WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60); - // captcha.setBackground(Color.WHITE); // 不设置就是透明底 - captcha.setFont(new Font("Arial", Font.BOLD, 45)); - captcha.setGenerator(codeGenerator); - captcha.createCode(); - // 如果是数学验证码,使用SpEL表达式处理验证码结果 - String code = captcha.getCode(); - if ("math".equals(captchaType)) { - ExpressionParser parser = new SpelExpressionParser(); - Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); - code = exp.getValue(String.class); - } - RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); - return new CaptchaVo(true, uuid, captcha.getImageBase64()); + @RequestMapping("/behavior/generate") + public ApiResponse generateBehaviorCaptcha() { + return captchaUtils.generateBehaviorCaptchaTicket(); } /** - * 图片验证码响应对象。 + * 校验【行为滑块验证码】 + * POST语义更合适,提交校验参数 * - * @param captchaEnabled 是否启用验证码 - * @param uuid 验证码标识 - * @param img Base64 图片数据 + * @param data 验证码缓存key + * @return 校验结果 */ - public record CaptchaVo(Boolean captchaEnabled, String uuid, String img) { + @PostMapping("/behavior/verify") + public ApiResponse verifyBehaviorCaptcha(@RequestBody behaviorData data) { + return captchaUtils.verifyBehaviorCaptchaFirstStage(data.id(), data.data()); } + /** + * 行为验证码验证接口传回BO + * + * @param id 验证码唯一ID,请求登录接口时携带该标识,后端根据ID读取缓存验证码做校验 + * @param data 验证码业务数据,包含Base64图片、滑块滑动轨迹信息等,详见 {@link ImageCaptchaTrack} + */ + public record behaviorData(String id, ImageCaptchaTrack data) { + } + + } diff --git a/ruoyi-admin/src/main/resources/font/AaKuangPaiShouShu-2.ttf b/ruoyi-admin/src/main/resources/font/AaKuangPaiShouShu-2.ttf new file mode 100644 index 000000000..7122b1db3 Binary files /dev/null and b/ruoyi-admin/src/main/resources/font/AaKuangPaiShouShu-2.ttf differ