AOP实现接口幂等
实现原理:针对控制器的入口进行切面拦截请求参数,对参数进行MD5加密,将密文存储在redis中,若短时间内有相同的参数进行请求时,进行拦截提示
1、切面注解
import java.lang.annotation.*;/*** 防重放操作校验*/
@Documented
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatOperationCheck {/*** 是否校验重放操作*/boolean isCheck() default true;}
2、切面
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** desc 表单操作防重放校验* Author WuYongQiang* Date 2025/3/13 14:53* Param * return **/
@Component
@Aspect
@Slf4j
public class RepeatOperationCheckAspect {@AutowiredHttpServletRequest httpServletRequest;@AutowiredRedisTemplate redisTemplate;/*** 声明切面* 只要Controller的方法中有@log注解就切入 重要*/@Pointcut("@annotation(***.RepeatOperationCheck)")public void logPointCut() {}@Before("logPointCut()")public void doBefore(JoinPoint joinPoint){//1、获得注解RepeatOperationCheck repeatOperationCheck = getAnnotationLog(joinPoint);if (repeatOperationCheck == null) {return;}if (!repeatOperationCheck.isCheck()) {return;}//2、获取请求参数String requestParams = getRequestParams(joinPoint);if(StringUtils.isBlank(requestParams)){return;}//3、将requestParams进行md5加密String value = encrypt(requestParams);//4、获取方法url+String requestUrl = httpServletRequest.getRequestURI();//5、获取redis的keyString key = requestUrl + value;//5、redis校验Boolean b = redisTemplate.opsForValue().setIfAbsent(key, value, 1, TimeUnit.SECONDS);if (!b){throw new RuntimeException("请勿重复操作");}}/*** desc 入参转md5* Author WuYongQiang* Date 2025/3/13 16:57* Param [joinPoint]* return java.lang.String**/private String encrypt(String requestParams){return MD5Util.md5(requestParams);}/*** 是否存在注解,如果存在就获取*/private RepeatOperationCheck getAnnotationLog(JoinPoint joinPoint){Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method method = methodSignature.getMethod();if (method != null) {return method.getAnnotation(RepeatOperationCheck.class);}return null;}/*** 获取请求的参数*/private String getRequestParams(JoinPoint joinPoint){String requestMethod = httpServletRequest.getMethod();String paramsStr = "";if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {paramsStr = argsArrayToString(joinPoint.getArgs());} else {Map<String, String[]> parameterMap = httpServletRequest.getParameterMap();paramsStr = JSON.toJSONString(parameterMap);}return paramsStr;}/*** 参数拼装*/private String argsArrayToString(Object[] paramsArray) {String params = "";if (paramsArray != null && paramsArray.length > 0) {for (int i = 0; i < paramsArray.length; i++) {if (!isFilterObject(paramsArray[i])) {Object jsonObj = JSON.toJSON(paramsArray[i]);params += jsonObj.toString() + " ";}}}return params.trim();}/*** 判断是否需要过滤的对象。** @param o 对象信息。* @return 如果是需要过滤的对象,则返回true;否则返回false。*/public boolean isFilterObject(final Object o) {return o instanceof MultipartFile || o instanceof HttpServletRequest|| o instanceof HttpServletResponse;}}
3、Md5加密工具类
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;import static java.nio.charset.StandardCharsets.UTF_8;public class MD5Util {/*** 加密算法-md5*/public static final String ALGORITHM = "md5-salt";/*** 对字符串进行MD5加密** @param input 需要加密的字符串* @return 加密后的32位小写MD5字符串*/public static String md5(String input) {try {// 获取MD5算法实例MessageDigest md = MessageDigest.getInstance("MD5");// 将输入字符串转换为字节数组并计算摘要byte[] messageDigest = md.digest(input.getBytes());// 将字节数组转换为十六进制字符串StringBuilder hexString = new StringBuilder();for (byte b : messageDigest) {String hex = Integer.toHexString(0xff & b);if (hex.length() == 1) hexString.append('0');hexString.append(hex);}return hexString.toString();} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5算法不可用", e);}}/*** 计算md5值** @param string 加密串* @return md5值* @throws NoSuchAlgorithmException*/public static String md5Hex(String string) throws NoSuchAlgorithmException {StringBuilder md5Hex = new StringBuilder();MessageDigest md5 = MessageDigest.getInstance("MD5");for (byte b : md5.digest(string.getBytes(UTF_8))) {md5Hex.append(String.format("%02X", b));}return md5Hex.toString();}
}
5、调用方式
@PostMapping("/operator")@RepeatOperationCheckpublic R<Object> test(@RequestBody Dto dto) {//........}