并发下接口防刷

实现原理:

根据请求入参(兼容没有入参的或者不能转换成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;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值