場景一:控制表單重復(fù)提交
防重提交有很多方案,從前端的按鈕置灰,到后端synchronize鎖、Lock鎖、借助Redis語法實現(xiàn)簡單鎖、Redis+Lua分布式鎖、Redisson分布式鎖,再到DB的悲觀鎖、樂觀鎖、借助表唯一索引等等都可以實現(xiàn)防重提交,以保證數(shù)據(jù)的安全性。
這篇文章我們介紹其中一種方案–借助Redis語法實現(xiàn)簡單鎖,最終實現(xiàn)防重提交。
背景:
我們項目中,為了控制表單重復(fù)提交問題,會在點擊頁面按鈕(向后端發(fā)起業(yè)務(wù)請求)后就會置灰按鈕,直到后端響應(yīng)后解除按鈕置灰。通過按鈕置灰來防止重啟提交問題。但Postman、Jmeter和其他服務(wù)調(diào)用(繞過前端頁面)呢?所以后端接口也要根據(jù)控制表單重復(fù)提交的問題。
后端代碼可以在2個位置做控制:
一是放在gateway網(wǎng)關(guān)做
好處是只在一個地方加上控制代碼,就可以控制所有接口的重復(fù)提交問題。壞處是控制的范圍太廣(比如查詢接口無需控制,控制了反而多余)、定義重復(fù)提交的時間段不能靈活調(diào)整。
二是放在AOP切面做
好處是只有需要的地方才會被控制(哪里需要引用一下自定義注解即可),另外也能靈活調(diào)整定義重復(fù)提交的時間段(自定義注解里定義時間字段開放給使用者填寫)。壞處是每個需要控制的地方都要加注解,會有侵入性和一定的工作量。
實現(xiàn)代碼
1、添加自定義注解
package com.xxx.annotations;
import java.lang.annotation.*;
/**
* 自定義注解防止表單重復(fù)提交
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:11
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 過期時間,單位毫秒
*/
long expireTime() default 500L;
}
2、添加AOP切面
package com.xxx.aop;
import com.xxx.annotations.RepeatSubmit;
import com.xxx.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 防止表單重復(fù)提交切面
*
* @Author WANGLINGQIANG
* @Date 2023/9/6 10:13
*/
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
private static final String KEY_PREFIX = "repeat_submit:";
@Resource
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.xxx.annotations.RepeatSubmit)")
public void repeatSubmit() {}
@Around("repeatSubmit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//joinPoint獲取方法對象
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//獲取方法上的@RepeatSubmit注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
//獲取HttpServletRequest對象,以獲取請求uri
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
String uri = request.getRequestURI();
//拼接Redis的key,這里只是簡單根據(jù)uri來判斷是否重復(fù)提交??梢愿鶕?jù)自己業(yè)務(wù)調(diào)整,比如根據(jù)用戶id或者請求token等
String cacheKey = KEY_PREFIX.concat(uri);
Boolean flag = null;
try {
//借助setIfAbsent(),key不存在才能設(shè)值成功
flag = redisTemplate.opsForValue().setIfAbsent(cacheKey, "", annotation.expireTime(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
//如果Redis不可用,則打印日志記錄,但依然對請求放行
log.error("", e);
return joinPoint.proceed();
}
//Redis可用的情況,如果flag=true說明單位時間內(nèi)這是第一次請求,放行
if (flag) {
return joinPoint.proceed();
} else {
//進(jìn)入else說明單位時間內(nèi)進(jìn)行了多次請求,則攔截請求并提示稍后重試
throw new ServiceException("系統(tǒng)繁忙,請稍后重試");
}
}
}
這里利用redisTemplate的setIfAbsent()實現(xiàn)的,如果存在就不能set成功,set的同時設(shè)置過期時間,可以是用使用默認(rèn),也可以自己根據(jù)業(yè)務(wù)調(diào)整。
另外,cacheKey的定義,也可以根據(jù)自己的需要去調(diào)整,比如根據(jù)當(dāng)前登錄用戶的userId、當(dāng)前登錄的token等。
3、使用
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@RepeatSubmit
@PostMapping
public AjaxResult add(@Validated @RequestBody SysUser user) {
//....
}
場景二:控制接口調(diào)用頻率
背景:
忘記密碼后通過發(fā)送手機(jī)驗證碼找回密碼的場景。因為每發(fā)一條短信都需要收費,所以要控制發(fā)短信的頻率。比如,同一個手機(jī)號在3分鐘內(nèi)只能發(fā)送3次短信,超過3次后則提示用戶“短信發(fā)送過于頻繁,請10分鐘后再試”。文章來源:http://www.zghlxwxcb.cn/news/detail-697475.html
實現(xiàn)代碼
@Slf4j
@RestController
@RequestMapping("/sms")
public class SmsController {
@Resource
private ISmsService smsService;
@Resource
public RedisTemplate redisTemplate;
@PostMapping("/sendValidCode")
public Result sendValidCode(@RequestBody @Valid SmsDTO smsDTO) {
//驗證手機(jī)號格式
checkPhoneNumber(smsDTO.getPhoneNumber());
//...其他驗證
//拼接Redis的key(key為手機(jī)號,以控制一個手機(jī)號有限時間內(nèi)容發(fā)送的次數(shù))
String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber();
//驗證發(fā)送短信次數(shù),超過則攔截(閾值是3次,超時時間是3分鐘,重試時間是10分鐘)
checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME);
return smsService.sendMsg(smsDTO);
}
/**
* 驗證發(fā)送短信次數(shù),超過則攔截
* 該方法用lua腳本替換實現(xiàn)更好
*/
private void checkSendCount(String cacheKey, Long threshold, Long timeout, String retryTime) {
//首先進(jìn)方法就先+1
Long count = redisTemplate.opsForValue().increment(cacheKey);
//然后比較次數(shù),是否超過閾值
if (count > threshold) {
//超過則設(shè)置過期時間為10分鐘,并提示10分鐘后重試
redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES);
throw new ServiceException("短信發(fā)送過于頻繁,請" + retryTime + "分鐘后再試");
} else {
//沒超過3次,則累加上這一次
redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES);
}
}
}
本章完結(jié)。
文章來源地址http://www.zghlxwxcb.cn/news/detail-697475.html
到了這里,關(guān)于使用Redis控制表單重復(fù)提交控制接口訪問頻率的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!