赞
踩
前段时间测试提出了一个bug,因为前端没有做防抖,导致APP按钮可以无限次点击提交,后端请求一直在不断触发。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的或者是对一个接口的重复操作
解决方案有很多中,这里以java 代码实现,用redis存储,对接口限制访问次数与访问时间.
1.自定义接口
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Prevent {
/**
* 限制的时间值(秒)默认60s
*/
long value() default 60;
/**
* 限制规定时间内访问次数,默认只能访问一次
*/
long times() default 1;
/**
* 禁用时长
*
* @return
*/
long forbiddenTime() default 60L;
/**
* 提示
*/
String message() default "";
/**
* 策略
*/
PreventStrategy strategy() default PreventStrategy.DEFAULT;
}
2.实现切面编程
@Aspect
@Component
@Slf4j
public class PreventAop implements Serializable {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 切入点
*/
@Pointcut("@annotation(com.health.boot.prevent.Prevent)")
public void pointcut() {
}
/**
* 处理前
*/
@Before("pointcut()")
public void joinPoint(JoinPoint joinPoint) throws Exception {
// 获取调用者ip
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
String userIP = IpUtils.getIpAddr(httpServletRequest);
// 获取调用接口方法名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = joinPoint.getTarget().getClass().getMethod(
methodSignature.getName(),
methodSignature.getParameterTypes()); // 获取该接口方法
String methodFullName = method.getDeclaringClass().getName() + method.getName(); // 获取到方法名
Prevent preventAnnotation = method.getAnnotation(Prevent.class); // 获取该接口上的prevent注解(为了使用该注解内的参数)
// 执行对应策略
entrance(preventAnnotation, userIP, methodFullName);
}
/**
* 通过prevent注册判断执行策略
*
* @param prevent 该接口的prevent注解对象
* @param userIP 访问该接口的用户ip
* @param methodFullName 该接口方法名
*/
private void entrance(Prevent prevent, String userIP, String methodFullName) throws Exception {
PreventStrategy strategy = prevent.strategy(); // 获取校验策略
if (Objects.requireNonNull(strategy) == PreventStrategy.DEFAULT) { // 默认就是default策略,执行default策略方法
defaultHandle(userIP, prevent, methodFullName);
} else {
throw new MessageException("无效的策略");
}
}
/**
* Default测试执行方法
*
* @param userIP 访问该接口的用户ip
* @param prevent 该接口的prevent注解对象
* @param methodFullName 该接口方法名
*/
private void defaultHandle(String userIP, Prevent prevent, String methodFullName) throws Exception {
String loginUserId = SysUserUtil.getLoginUserId();
String base64StrIP = toBase64String(userIP); // 加密用户ip(避免ip存在一些特殊字符作为redis的key不合法)
long expire = prevent.value(); // 获取访问限制时间
long times = prevent.times(); // 获取访问限制次数
String redisKey =RedisKey.PREVENT_METHOD_NAME+ String.format("%s:%s:%s", base64StrIP,methodFullName,loginUserId);
Object count = redisTemplate.opsForValue().get(redisKey);
if(Objects.isNull(count)){
log.info("首次访问");
redisTemplate.opsForValue().set(redisKey,1,expire,TimeUnit.SECONDS);
}else {
if((Integer)count<times){
log.info("总请求次数+1:{}", count);
redisTemplate.opsForValue().increment(redisKey);
}else {
if((Integer)count>(times*3)){
//访问次数异常超出
redisTemplate.opsForValue().set(redisKey,1,prevent.forbiddenTime(),TimeUnit.SECONDS);
throw new MessageException("疑似违规操作,我们已经记录!");
}
log.info("请求频繁:{}", count);
// 如果有限制信息则使用限制信息,没有则使用默认限制信息
String errorMessage = prevent.message();
if (StrUtil.isEmpty(errorMessage)) {
errorMessage =
!StrUtil.isEmpty(prevent.message()) ? prevent.message() : expire + "秒内不允许重复请求";
}
throw new MessageException(errorMessage);
}
}
}
public Long checkRequest(String key, Prevent prevent){
StringBuffer redisScript=new StringBuffer();
redisScript.append("if tonumber(redis.call('get', KEYS[1]) or '0') + 1 > tonumber(ARGV[1]) then ");
redisScript.append(" return 0");
redisScript.append("else ");
redisScript.append(" value + 1 ");
redisScript.append(" redis.call('INCRBY', KEYS[1], 1) ");
redisScript.append(" redis.call('EXPIRE', KEYS[1], ARGV[2]) ");
redisScript.append(" return 1");
redisScript.append("end ");
DefaultRedisScript<Long> defaultRedisScript= new DefaultRedisScript<>();
defaultRedisScript.setResultType(Long.class);
defaultRedisScript.setScriptText(redisScript.toString());
Long execute = redisTemplate.execute(defaultRedisScript, Collections.singletonList(key), prevent.value(), prevent.times());
return execute;
};
/**
* 对象转换为base64字符串
*
* @param obj 对象值
* @return base64字符串
*/
private String toBase64String(String obj) throws Exception {
if (StrUtil.isEmpty(obj)) {
return null;
}
Base64.Encoder encoder = Base64.getEncoder();
byte[] bytes = obj.getBytes(StandardCharsets.UTF_8);
return encoder.encodeToString(bytes);
}
}
@Getter
public enum PreventStrategy {
/**
* 默认(60s内不允许再次请求)
*/
DEFAULT
}
@PostMapping(value = "saveUserAnswer")
@Prevent(value = 5, times = 1, message = "请勿重复提交,休息一下吧!")
public String saveUserAnswer(@Valid @RequestBody ProblemLibraryUserAnswer problemLibraryUserAnswer, HttpServletRequest request) {
final String id = UserInfo.id(request, sign);
problemLibraryUserAnswer.setUserId(id);
return problemLibraryService.saveUserAnswer(problemLibraryUserAnswer) ? ResultUtil.success(ResultEnum.SUCCESS, "答题成功")
: ResultUtil.error(ResultEnum.MSG_NOT_FOUND, "请重新答题");
}
这里表示每5秒请求一次
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。