Skip to content

Commit

Permalink
refactor: 获取短信、邮箱验证码接口适配 ContiNew Starter 限流器
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles7c committed Jun 30, 2024
1 parent 5604fe9 commit 44811fc
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 48 deletions.
6 changes: 6 additions & 0 deletions continew-admin-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@
<artifactId>continew-starter-file-excel</artifactId>
</dependency>

<!-- ContiNew Starter 安全模块 - 限流器 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-security-limiter</artifactId>
</dependency>

<!-- ContiNew Starter 安全模块 - 加密 -->
<dependency>
<groupId>top.continew</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ public static class CaptchaMail {
*/
private long expirationInMinutes;

/**
* 限制时间
*/
private long limitInSeconds;

/**
* 模板路径
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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;
Expand Down Expand Up @@ -116,14 +119,28 @@ public R<CaptchaResp> getImageCaptcha() {
return R.ok(CaptchaResp.builder().uuid(uuid).img(captcha.toBase64()).expireTime(expireTime).build());
}

/**
* 获取邮箱验证码
*
* <p>
* 限流规则:<br>
* 1.同一邮箱同一模板,1分钟2条,1小时8条,24小时20条 <br>
* 2、同一邮箱所有模板 24 小时 100 条 <br>
* 3、同一 IP 每分钟限制发送 30 条
* </p>
*
* @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<Void> 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());
Expand All @@ -138,42 +155,40 @@ public R<Void> 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));
}

/**
* 获取短信验证码
*
* <p>
* 限流规则:<br>
* 1.同一号码同一模板,1分钟2条,1小时8条,24小时20条 <br>
* 2、同一号码所有模板 24 小时 100 条 <br>
* 3、同一 IP 每分钟限制发送 30 条
* </p>
*
* @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<Void> 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());
// 发送验证码
Expand All @@ -186,7 +201,7 @@ public R<Void> getSmsCaptcha(@NotBlank(message = "手机号不能为空") @Patte
.getTemplateId(), (LinkedHashMap<String, String>)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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,6 @@ captcha:
length: 6
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
## 短信验证码配置
Expand Down Expand Up @@ -257,22 +255,25 @@ sa-token.extension:
# 本地存储资源
- /file/**

--- ### 字段加/解密配置
--- ### 安全配置
continew-starter.security:
## 字段加/解密配置
crypto:
enabled: true
# 对称加密算法密钥
password: abcdefghijklmnop
# 非对称加密算法密钥(在线生成 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ captcha:
length: 6
# 过期时间
expirationInMinutes: 5
# 限制时间
limitInSeconds: 60
# 模板路径
templatePath: mail/captcha.ftl
## 短信验证码配置
Expand Down Expand Up @@ -254,22 +252,25 @@ sa-token.extension:
# 本地存储资源
- /file/**

--- ### 字段加/解密配置
--- ### 安全配置
continew-starter.security:
## 字段加/解密配置
crypto:
enabled: true
# 对称加密算法密钥
password: abcdefghijklmnop
# 非对称加密算法密钥(在线生成 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:
Expand Down

0 comments on commit 44811fc

Please sign in to comment.