实现原理:
根据请求入参(兼容没有入参的或者不能转换成json的请求)+ip地址+接口路径拼接起来,生成md5加密字符串,利用redis setNx判断在单位时间内是否请求过
话不多说上代码
注解类
package com.annto.dc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 防重复提交注解
* 实现原理:根据请求入参(兼容没有入参的或者不能转换成json的请求)+ip地址+接口路径拼接起来,生成md5加密字符串,利用redis setNx判断在单位时间内是否请求过
* 注意点:
* 1.当前仅支持无参、第一个请求参数的防重
* 2.不能解析成json的入参需要加上concatParam=false,防止转换成json时报错
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Resubmit {
//超时时间,单位s,不指定默认1s
long expireTime() default 1L;
//需要剔除的字段,默认空(英文单引号间隔,例如:createTime,updateTime)
String excludeKeys() default "";
//是否要拼接入参,默认true(有些请求不能解析成json,例如:MultipartFile)
boolean concatParam() default true;
}
切面
package com.annto.dc.imports.aop;
import cn.hutool.core.util.ArrayUtil;
import com.alibaba.fastjson.JSON;
import com.annto.dc.annotation.Resubmit;
import com.annto.dc.common.helper.Md5Helper;
import com.annto.dc.constants.CommonConstants;
import com.annto.dc.vo.BusinessException;
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.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
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;
@Aspect
@Component
@Slf4j
public class ResubmitAspect {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.annto.dc.annotation.Resubmit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();//获取方法
Resubmit annotation = method.getAnnotation(Resubmit.class);//获取注解信息
Object[] args = joinPoint.getArgs();//获取入参
String paramJson;
if (ArrayUtil.isEmpty(args) || !annotation.concatParam()) {
paramJson = CommonConstants.EMPTY_STRING;
} else {
paramJson = JSON.toJSONString(args[0]);//获取第一个入参
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//获取httpRequest
boolean firstSubmit = isFirstSubmit(paramJson, request.getRemoteAddr(), request.getRequestURI(), annotation.excludeKeys(), annotation.expireTime());
if (!firstSubmit) throw BusinessException.fail(String.format("接口【%s】在【%s s】内不允许重复请求,请稍后再试!", request.getRequestURI(), annotation.expireTime()));
return joinPoint.proceed();//放行
}
/**
* 是否第一次提交
*
* @param json 请求参数json字符串
* @param ip 请求方ip
* @param url 请求地址
* @param excludeKeys 需要剔除的字段
* @param expireTime 过期时间
* @return
*/
private boolean isFirstSubmit(String json, String ip, String url, String excludeKeys, long expireTime) {
//根据入参+ip+url生成MD5加密字符串
String md5Str = Md5Helper.buildParamMD5(json, ip, url, excludeKeys);
long expireAt = System.currentTimeMillis() + expireTime;
String value = "expireAt_" + expireAt;
boolean firstSubmit = false;
try {
firstSubmit = stringRedisTemplate.opsForValue().setIfAbsent(md5Str, value, expireTime, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("redis setIfAbsent异常:", e);
}
return firstSubmit;
}
}
MD5加密工具类
package com.annto.dc.common.helper;
import com.alibaba.fastjson.JSON;
import com.annto.dc.constants.CommonConstants;
import deps.cn.hutool.core.collection.CollectionUtil;
import deps.cn.hutool.core.util.ObjUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.xml.bind.DatatypeConverter;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
@Slf4j
@Component
public class Md5Helper {
/**
* @param excludeKeys
* @param json
* @param ip
* @param url
* @return
*/
public static String buildParamMD5(String json, String ip, String url, String excludeKeys) {
//将入参转成TreeMap,保证有序性
TreeMap paramTreeMap = JSON.parseObject(json, TreeMap.class);
//剔除需要排除的参数
excludeParams(paramTreeMap, excludeKeys);
//对字符串进行md5加密
String finalJson = String.format("%s_%s_%s", ip, url, JSON.toJSONString(paramTreeMap));
String md5Str = jdkMD5(finalJson);
log.debug("加密后的md5字符串:{},需要剔除的字段:{}", md5Str, StringUtils.join(excludeKeys, CommonConstants.COMMA));
return md5Str;
}
/**
* 剔除需要排除的参数
*
* @param paramTreeMap
* @param excludeKeys
*/
public static void excludeParams(TreeMap paramTreeMap, String excludeKeys) {
if (ObjUtil.isNull(paramTreeMap)) return;
if (StringUtils.isBlank(excludeKeys)) return;
List<String> excludeKeyList = Arrays.asList(excludeKeys.split(CommonConstants.COMMA));
if (CollectionUtil.isEmpty(excludeKeyList)) return;
for (String excludeKey : excludeKeyList) {
paramTreeMap.remove(excludeKey);
}
}
/**
* 对字符串进行md5加密
*
* @param str
* @return
*/
public static String jdkMD5(String str) {
String res = CommonConstants.EMPTY_STRING;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] mdBytes = messageDigest.digest(str.getBytes());
res = DatatypeConverter.printHexBinary(mdBytes);
} catch (Exception e) {
log.error("jdkMD5加密异常:", e);
}
return res;
}
}