diff --git a/continew-admin-common/pom.xml b/continew-admin-common/pom.xml index 90b1f112..90ff6d58 100644 --- a/continew-admin-common/pom.xml +++ b/continew-admin-common/pom.xml @@ -112,6 +112,12 @@ continew-starter-file-excel + + + top.continew + continew-starter-security-limiter + + top.continew diff --git a/continew-admin-common/src/main/java/top/continew/admin/common/config/properties/CaptchaProperties.java b/continew-admin-common/src/main/java/top/continew/admin/common/config/properties/CaptchaProperties.java index 2b8fc526..51870c56 100644 --- a/continew-admin-common/src/main/java/top/continew/admin/common/config/properties/CaptchaProperties.java +++ b/continew-admin-common/src/main/java/top/continew/admin/common/config/properties/CaptchaProperties.java @@ -63,11 +63,6 @@ public static class CaptchaMail { */ private long expirationInMinutes; - /** - * 限制时间 - */ - private long limitInSeconds; - /** * 模板路径 */ diff --git a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/common/CaptchaController.java b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/common/CaptchaController.java index a1de216b..24ea7c21 100644 --- a/continew-admin-webapi/src/main/java/top/continew/admin/webapi/common/CaptchaController.java +++ b/continew-admin-webapi/src/main/java/top/continew/admin/webapi/common/CaptchaController.java @@ -41,7 +41,7 @@ import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.comm.constant.SupplierConstant; import org.dromara.sms4j.core.factory.SmsFactory; -import org.redisson.api.RateType; +import org.redisson.api.RateIntervalUnit; import org.springframework.http.HttpHeaders; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -57,6 +57,9 @@ import top.continew.starter.core.util.validate.ValidationUtils; import top.continew.starter.log.core.annotation.Log; import top.continew.starter.messaging.mail.util.MailUtils; +import top.continew.starter.security.limiter.annotation.RateLimiter; +import top.continew.starter.security.limiter.annotation.RateLimiters; +import top.continew.starter.security.limiter.enums.LimitType; import top.continew.starter.web.model.R; import java.time.Duration; @@ -116,14 +119,28 @@ public R getImageCaptcha() { return R.ok(CaptchaResp.builder().uuid(uuid).img(captcha.toBase64()).expireTime(expireTime).build()); } + /** + * 获取邮箱验证码 + * + *

+ * 限流规则:
+ * 1.同一邮箱同一模板,1分钟2条,1小时8条,24小时20条
+ * 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 = "获取验证码操作太频繁,请稍后再试")}) public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern(regexp = RegexPool.EMAIL, message = "邮箱格式错误") String email) throws MessagingException { - String limitKeyPrefix = CacheConstants.LIMIT_KEY_PREFIX; - String captchaKeyPrefix = CacheConstants.CAPTCHA_KEY_PREFIX; - String limitCaptchaKey = limitKeyPrefix + captchaKeyPrefix + email; - long limitTimeInMillisecond = RedisUtils.getTimeToLive(limitCaptchaKey); - CheckUtils.throwIf(limitTimeInMillisecond > 0, "发送验证码过于频繁,请您 {}s 后再试", limitTimeInMillisecond / 1000); // 生成验证码 CaptchaProperties.CaptchaMail captchaMail = captchaProperties.getMail(); String captcha = RandomUtil.randomNumbers(captchaMail.getLength()); @@ -138,42 +155,40 @@ public R getMailCaptcha(@NotBlank(message = "邮箱不能为空") @Pattern .set("expiration", expirationInMinutes)); MailUtils.sendHtml(email, "【%s】邮箱验证码".formatted(projectProperties.getName()), content); // 保存验证码 - String captchaKey = captchaKeyPrefix + email; + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + email; RedisUtils.set(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes)); - RedisUtils.set(limitCaptchaKey, captcha, Duration.ofSeconds(captchaMail.getLimitInSeconds())); return R.ok("发送成功,验证码有效期 %s 分钟".formatted(expirationInMinutes)); } + /** + * 获取短信验证码 + * + *

+ * 限流规则:
+ * 1.同一号码同一模板,1分钟2条,1小时8条,24小时20条
+ * 2、同一号码所有模板 24 小时 100 条
+ * 3、同一 IP 每分钟限制发送 30 条 + *

+ * + * @param phone 手机号 + * @param captchaReq 行为验证码信息 + * @return / + */ @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 = "获取验证码操作太频繁,请稍后再试")}) public R getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Pattern(regexp = RegexPool.MOBILE, message = "手机号格式错误") String phone, - CaptchaVO captchaReq, - HttpServletRequest request) { + CaptchaVO captchaReq) { // 行为验证码校验 ResponseModel verificationRes = behaviorCaptchaService.verification(captchaReq); ValidationUtils.throwIfNotEqual(verificationRes.getRepCode(), RepCodeEnum.SUCCESS.getCode(), verificationRes .getRepMsg()); CaptchaProperties.CaptchaSms captchaSms = captchaProperties.getSms(); - String templateId = captchaSms.getTemplateId(); - String limitKeyPrefix = CacheConstants.LIMIT_KEY_PREFIX; - String captchaKeyPrefix = CacheConstants.CAPTCHA_KEY_PREFIX; - String limitTemplateKeyPrefix = limitKeyPrefix + captchaKeyPrefix; - // 限制短信发送频率 - // 1.同一号码同一短信模板,1分钟2条,1小时8条,24小时20条,e.g. LIMIT:CAPTCHA:XXX:188xxxxx:1 - final String errorMsg = "获取验证码操作太频繁,请稍后再试"; - CheckUtils.throwIf(!RedisUtils.rateLimit(RedisUtils - .formatKey(limitTemplateKeyPrefix + "MIN", phone, templateId), RateType.OVERALL, 2, 60), errorMsg); - CheckUtils.throwIf(!RedisUtils.rateLimit(RedisUtils - .formatKey(limitTemplateKeyPrefix + "HOUR", phone, templateId), RateType.OVERALL, 8, 60 * 60), errorMsg); - CheckUtils.throwIf(!RedisUtils.rateLimit(RedisUtils - .formatKey(limitTemplateKeyPrefix + "DAY", phone, templateId), RateType.OVERALL, 20, 60 * 60 * 24), errorMsg); - // 2.同一号码所有短信模板 24 小时 100 条,e.g. LIMIT:CAPTCHA:188xxxxx - String limitPhoneKey = limitKeyPrefix + captchaKeyPrefix + phone; - CheckUtils.throwIf(!RedisUtils.rateLimit(limitPhoneKey, RateType.OVERALL, 100, 60 * 60 * 24), errorMsg); - // 3.同一 IP 每分钟限制发送 30 条,e.g. LIMIT:CAPTCHA:PHONE:1xx.1xx.1xx.1xx - String limitIpKey = RedisUtils.formatKey(limitKeyPrefix + captchaKeyPrefix + "PHONE", JakartaServletUtil - .getClientIP(request)); - CheckUtils.throwIf(!RedisUtils.rateLimit(limitIpKey, RateType.OVERALL, 30, 60), errorMsg); // 生成验证码 String captcha = RandomUtil.randomNumbers(captchaSms.getLength()); // 发送验证码 @@ -186,7 +201,7 @@ public R getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Patte .getTemplateId(), (LinkedHashMap)messageMap); CheckUtils.throwIf(!smsResponse.isSuccess(), "验证码发送失败"); // 保存验证码 - String captchaKey = captchaKeyPrefix + phone; + String captchaKey = CacheConstants.CAPTCHA_KEY_PREFIX + phone; RedisUtils.set(captchaKey, captcha, Duration.ofMinutes(expirationInMinutes)); return R.ok("发送成功,验证码有效期 %s 分钟".formatted(expirationInMinutes)); } diff --git a/continew-admin-webapi/src/main/resources/config/application-dev.yml b/continew-admin-webapi/src/main/resources/config/application-dev.yml index 2df78e50..02da2755 100644 --- a/continew-admin-webapi/src/main/resources/config/application-dev.yml +++ b/continew-admin-webapi/src/main/resources/config/application-dev.yml @@ -143,8 +143,6 @@ captcha: length: 6 # 过期时间 expirationInMinutes: 5 - # 限制时间 - limitInSeconds: 60 # 模板路径 templatePath: mail/captcha.ftl ## 短信验证码配置 @@ -257,8 +255,9 @@ sa-token.extension: # 本地存储资源 - /file/** ---- ### 字段加/解密配置 +--- ### 安全配置 continew-starter.security: + ## 字段加/解密配置 crypto: enabled: true # 对称加密算法密钥 @@ -266,13 +265,15 @@ continew-starter.security: # 非对称加密算法密钥(在线生成 RSA 密钥对:http://web.chacuo.net/netrsakeypair) public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9uaUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ== private-key: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV - ---- ### 密码编码器配置 -continew-starter.security: + ## 密码编码器配置 password: enabled: true # BCryptPasswordEncoder encoding-id: bcrypt + ## 限流器配置 + limiter: + enabled: true + key-prefix: RateLimiter --- ### 文件上传配置 spring.servlet: diff --git a/continew-admin-webapi/src/main/resources/config/application-prod.yml b/continew-admin-webapi/src/main/resources/config/application-prod.yml index 2f9ae4ca..50b1d9fc 100644 --- a/continew-admin-webapi/src/main/resources/config/application-prod.yml +++ b/continew-admin-webapi/src/main/resources/config/application-prod.yml @@ -145,8 +145,6 @@ captcha: length: 6 # 过期时间 expirationInMinutes: 5 - # 限制时间 - limitInSeconds: 60 # 模板路径 templatePath: mail/captcha.ftl ## 短信验证码配置 @@ -254,8 +252,9 @@ sa-token.extension: # 本地存储资源 - /file/** ---- ### 字段加/解密配置 +--- ### 安全配置 continew-starter.security: + ## 字段加/解密配置 crypto: enabled: true # 对称加密算法密钥 @@ -263,13 +262,15 @@ continew-starter.security: # 非对称加密算法密钥(在线生成 RSA 密钥对:http://web.chacuo.net/netrsakeypair) public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9uaUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ== private-key: MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAznV2Bi0zIX61NC3zSx8U6lJXbtru325pRV4Wt0aJXGxy6LMTsfxIye1ip+f2WnxrkYfk/X8YZ6FWNQPaAX/iRwIDAQABAkEAk/VcAusrpIqA5Ac2P5Tj0VX3cOuXmyouaVcXonr7f+6y2YTjLQuAnkcfKKocQI/juIRQBFQIqqW/m1nmz1wGeQIhAO8XaA/KxzOIgU0l/4lm0A2Wne6RokJ9HLs1YpOzIUmVAiEA3Q9DQrpAlIuiT1yWAGSxA9RxcjUM/1kdVLTkv0avXWsCIE0X8woEjK7lOSwzMG6RpEx9YHdopjViOj1zPVH61KTxAiBmv/dlhqkJ4rV46fIXELZur0pj6WC3N7a4brR8a+CLLQIhAMQyerWl2cPNVtE/8tkziHKbwW3ZUiBXU24wFxedT9iV - ---- ### 密码编码器配置 -continew-starter.security: + ## 密码编码器配置 password: enabled: true # BCryptPasswordEncoder encoding-id: bcrypt + ## 限流器配置 + limiter: + enabled: true + key-prefix: RateLimiter --- ### 文件上传配置 spring.servlet: