diff --git a/continew-module-system/src/main/java/top/continew/admin/auth/model/req/AccountLoginReq.java b/continew-module-system/src/main/java/top/continew/admin/auth/model/req/AccountLoginReq.java index 202d062a2..b4d841400 100644 --- a/continew-module-system/src/main/java/top/continew/admin/auth/model/req/AccountLoginReq.java +++ b/continew-module-system/src/main/java/top/continew/admin/auth/model/req/AccountLoginReq.java @@ -18,7 +18,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.checkerframework.checker.units.qual.N; import java.io.Serial; import java.io.Serializable; @@ -50,17 +52,20 @@ public class AccountLoginReq implements Serializable { @NotBlank(message = "密码不能为空") private String password; + @Schema(description = "是否开启验证码", example = "true") + private Boolean unCaptcha; + /** * 验证码 */ @Schema(description = "验证码", example = "ABCD") - @NotBlank(message = "验证码不能为空") +// @NotBlank(message = "验证码不能为空") private String captcha; /** * 验证码标识 */ @Schema(description = "验证码标识", example = "090b9a2c-1691-4fca-99db-e4ed0cff362f") - @NotBlank(message = "验证码标识不能为空") +// @NotBlank(message = "验证码标识不能为空") private String uuid; } diff --git a/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java b/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java index b61db781c..f57347039 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java @@ -38,4 +38,9 @@ public enum OptionCategoryEnum { * 邮箱配置 */ MAIL, + + /** + * 验证码配置 + */ + CAPTCHA, } diff --git a/continew-webapi/src/main/java/top/continew/admin/controller/auth/AuthController.java b/continew-webapi/src/main/java/top/continew/admin/controller/auth/AuthController.java index b53cb8308..d4171e69c 100644 --- a/continew-webapi/src/main/java/top/continew/admin/controller/auth/AuthController.java +++ b/continew-webapi/src/main/java/top/continew/admin/controller/auth/AuthController.java @@ -69,11 +69,13 @@ public class AuthController { @Operation(summary = "账号登录", description = "根据账号和密码进行登录认证") @PostMapping("/account") public LoginResp accountLogin(@Validated @RequestBody AccountLoginReq loginReq, HttpServletRequest request) { - String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid(); - String captcha = RedisUtils.get(captchaKey); - ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); - RedisUtils.delete(captchaKey); - ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR); + if (!loginReq.getUnCaptcha()) { + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + loginReq.getUuid(); + String captcha = RedisUtils.get(captchaKey); + ValidationUtils.throwIfBlank(captcha, CAPTCHA_EXPIRED); + RedisUtils.delete(captchaKey); + ValidationUtils.throwIfNotEqualIgnoreCase(loginReq.getCaptcha(), captcha, CAPTCHA_ERROR); + } // 用户登录 String rawPassword = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(loginReq.getPassword())); ValidationUtils.throwIfBlank(rawPassword, "密码解密失败"); diff --git a/continew-webapi/src/main/java/top/continew/admin/controller/common/CaptchaController.java b/continew-webapi/src/main/java/top/continew/admin/controller/common/CaptchaController.java index 394695e4a..6db43c16d 100644 --- a/continew-webapi/src/main/java/top/continew/admin/controller/common/CaptchaController.java +++ b/continew-webapi/src/main/java/top/continew/admin/controller/common/CaptchaController.java @@ -88,6 +88,15 @@ public class CaptchaController { private final GraphicCaptchaService graphicCaptchaService; private final OptionService optionService; + + @Log(ignore = true) + @Operation(summary = "获取验证码配置", description = "获取验证码配置(预留后续扩展多种验证码)") + @GetMapping("/config") + public R getCaptchaConfig() { + Map captchaConfig = optionService.getByCategory(OptionCategoryEnum.CAPTCHA); + return R.ok(captchaConfig); + } + @Log(ignore = true) @Operation(summary = "获取行为验证码", description = "获取行为验证码(Base64编码)") @GetMapping("/behavior") @@ -95,7 +104,7 @@ public Object getBehaviorCaptcha(CaptchaVO captchaReq, HttpServletRequest reques captchaReq.setBrowserInfo(JakartaServletUtil.getClientIP(request) + request.getHeader(HttpHeaders.USER_AGENT)); ResponseModel responseModel = behaviorCaptchaService.get(captchaReq); CheckUtils.throwIf(() -> !StrUtil.equals(RepCodeEnum.SUCCESS.getCode(), responseModel - .getRepCode()), responseModel.getRepMsg()); + .getRepCode()), responseModel.getRepMsg()); return responseModel.getRepData(); } @@ -115,7 +124,7 @@ public CaptchaResp getImageCaptcha() { String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + uuid; Captcha captcha = graphicCaptchaService.getCaptcha(); long expireTime = LocalDateTimeUtil.toEpochMilli(LocalDateTime.now() - .plusMinutes(captchaProperties.getExpirationInMinutes())); + .plusMinutes(captchaProperties.getExpirationInMinutes())); RedisUtils.set(captchaKey, captcha.text(), Duration.ofMinutes(captchaProperties.getExpirationInMinutes())); return CaptchaResp.builder().uuid(uuid).img(captcha.toBase64()).expireTime(expireTime).build(); } @@ -129,24 +138,24 @@ public CaptchaResp getImageCaptcha() { * 2、同一邮箱所有模板 24 小时 100 条
* 3、同一 IP 每分钟限制发送 30 条 *

- * + * * @param email 邮箱 * @return / */ @Operation(summary = "获取邮箱验证码", description = "发送验证码到指定邮箱") @GetMapping("/mail") @RateLimiters({ - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "MIN", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 2, interval = 1, unit = RateIntervalUnit.MINUTES, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "HOUR", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 8, interval = 1, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "DAY'", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 20, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#email", rate = 100, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#email", rate = 30, interval = 1, unit = RateIntervalUnit.MINUTES, type = LimitType.IP, message = "获取验证码操作太频繁,请稍后再试")}) + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "MIN", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 2, interval = 1, unit = RateIntervalUnit.MINUTES, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "HOUR", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 8, interval = 1, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "DAY'", key = "#email + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.mail.templatePath')", rate = 20, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#email", rate = 100, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#email", rate = 30, interval = 1, unit = RateIntervalUnit.MINUTES, type = LimitType.IP, message = "获取验证码操作太频繁,请稍后再试")}) public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") String email, CaptchaVO captchaReq) throws MessagingException { // 行为验证码校验 ResponseModel verificationRes = behaviorCaptchaService.verification(captchaReq); ValidationUtils.throwIfNotEqual(verificationRes.getRepCode(), RepCodeEnum.SUCCESS.getCode(), verificationRes - .getRepMsg()); + .getRepMsg()); // 生成验证码 CaptchaProperties.CaptchaMail captchaMail = captchaProperties.getMail(); String captcha = RandomUtil.randomNumbers(captchaMail.getLength()); @@ -154,11 +163,11 @@ public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regex Long expirationInMinutes = captchaMail.getExpirationInMinutes(); Map siteConfig = optionService.getByCategory(OptionCategoryEnum.SITE); String content = TemplateUtils.render(captchaMail.getTemplatePath(), Dict.create() - .set("siteUrl", projectProperties.getUrl()) - .set("siteTitle", siteConfig.get("SITE_TITLE")) - .set("siteCopyright", siteConfig.get("SITE_COPYRIGHT")) - .set("captcha", captcha) - .set("expiration", expirationInMinutes)); + .set("siteUrl", projectProperties.getUrl()) + .set("siteTitle", siteConfig.get("SITE_TITLE")) + .set("siteCopyright", siteConfig.get("SITE_COPYRIGHT")) + .set("captcha", captcha) + .set("expiration", expirationInMinutes)); MailUtils.sendHtml(email, "【%s】邮箱验证码".formatted(projectProperties.getName()), content); // 保存验证码 String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; @@ -175,7 +184,7 @@ public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regex * 2、同一号码所有模板 24 小时 100 条
* 3、同一 IP 每分钟限制发送 30 条 *

- * + * * @param phone 手机号 * @param captchaReq 行为验证码信息 * @return / @@ -183,17 +192,17 @@ public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regex @Operation(summary = "获取短信验证码", description = "发送验证码到指定手机号") @GetMapping("/sms") @RateLimiters({ - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "MIN", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 2, interval = 1, unit = RateIntervalUnit.MINUTES, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "HOUR", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 8, interval = 1, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "DAY'", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 20, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#phone", rate = 100, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), - @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#phone", rate = 30, interval = 1, unit = RateIntervalUnit.MINUTES, type = LimitType.IP, message = "获取验证码操作太频繁,请稍后再试")}) + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "MIN", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 2, interval = 1, unit = RateIntervalUnit.MINUTES, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "HOUR", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 8, interval = 1, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX + "DAY'", key = "#phone + ':' + T(cn.hutool.extra.spring.SpringUtil).getProperty('captcha.sms.templateId')", rate = 20, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#phone", rate = 100, interval = 24, unit = RateIntervalUnit.HOURS, message = "获取验证码操作太频繁,请稍后再试"), + @RateLimiter(name = CacheConstants.CAPTCHA_KEY_PREFIX, key = "#phone", rate = 30, interval = 1, unit = RateIntervalUnit.MINUTES, type = LimitType.IP, message = "获取验证码操作太频繁,请稍后再试")}) public R getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误") String phone, CaptchaVO captchaReq) { // 行为验证码校验 ResponseModel verificationRes = behaviorCaptchaService.verification(captchaReq); ValidationUtils.throwIfNotEqual(verificationRes.getRepCode(), RepCodeEnum.SUCCESS.getCode(), verificationRes - .getRepMsg()); + .getRepMsg()); CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms(); // 生成验证码 String captcha = RandomUtil.randomNumbers(captchaSms.getLength()); @@ -204,7 +213,7 @@ public R getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Pattern(reg messageMap.put("captcha", captcha); messageMap.put("expirationInMinutes", String.valueOf(expirationInMinutes)); SmsResponse smsResponse = smsBlend.sendMessage(phone, captchaSms - .getTemplateId(), (LinkedHashMap)messageMap); + .getTemplateId(), (LinkedHashMap) messageMap); CheckUtils.throwIf(!smsResponse.isSuccess(), "验证码发送失败"); // 保存验证码 String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone; diff --git a/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql b/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql index 0cc5ecebd..b1bf5c0e7 100644 --- a/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql +++ b/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql @@ -143,6 +143,8 @@ VALUES (19, 'MAIL', '密码', 'MAIL_PASSWORD', NULL, NULL, NULL, NULL, NULL), (20, 'MAIL', '是否启用SSL', 'MAIL_SSL_ENABLED', NULL, '1', NULL, NULL, NULL), (21, 'MAIL', 'SSL端口', 'MAIL_SSL_PORT', NULL, '465', NULL, NULL, NULL); +(22, 'CAPTCHA', '是否开启验证码', 'NEED_CAPTCHA', '1', '1', '是否开启验证码(1:开启 0:关闭)', 1, '2024-12-04 16:19:58'); + -- 初始化默认字典 INSERT INTO `sys_dict` diff --git a/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql b/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql index 47db58349..58ebc9d7a 100644 --- a/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql +++ b/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql @@ -143,6 +143,7 @@ VALUES (19, 'MAIL', '密码', 'MAIL_PASSWORD', NULL, NULL, NULL, NULL, NULL), (20, 'MAIL', '是否启用SSL', 'MAIL_SSL_ENABLED', NULL, '1', NULL, NULL, NULL), (21, 'MAIL', 'SSL端口', 'MAIL_SSL_PORT', NULL, '465', NULL, NULL, NULL); +(22, 'CAPTCHA', '是否开启验证码', 'NEED_CAPTCHA', '1', '1', '是否开启验证码(1:开启 0:关闭)', 1, '2024-12-04 16:19:58'); -- 初始化默认字典 INSERT INTO "sys_dict"