spring boot 通过aop防止api连续重复访问

自用记录,欢迎大佬指正!

环境:

jdk1.8

spring boot 2.7.11

redis

话不多说,上代码。

pom.xml


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

<!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <!-- 过滤lettuce,使用jedis作为redis客户端 lettuce会自动断开,不知道怎么处理-->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
<!--        解决Springboot Redis command timed out 问题-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

        <!--        数字签名算法SHA1-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>

        <!--json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.25</version>
        </dependency>

application.yml


spring:
  redis:
    database: 9
    host: 127.0.0.1
    password: password
    port: 6379
    timeout: 5000
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1
        min-idle: 0
全局异常处理处理器:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理
 */
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {

    /**
     * 指定拦截那一中类型
     * @param ex 类型
     * @return m
     */

    @ExceptionHandler(RuntimeException.class)
    public String exceptionHandler(RuntimeException ex){
        return new Exception().getStackTrace()[1].getMethodName()+"方法出错,"+ex.getMessage();
    }


}

自定义注解,加上这个注解的接口,才会限制重复访问。


import java.lang.annotation.*;

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 防重复操作限时标记数值(存储redis限时标记数值)
     */
    String value() default "value" ;

    /**
     * 防重复操作过期时间(借助redis实现限时控制)
     */
    long expireSeconds() default 10;
}

自定义拦截器

根据redisKey进行判断,是否拦截。

redisKey组成:

自己随便定义的前缀:PREVENT_DUPLICATION_PREFIX

请求的URL:request.getRequestURI()

接口请求的所有参数:request.getParameterMap()

SHA1加密的方法信息:getMethodSign()

重复提交判断依据:

通过redisTemplate.opsForValue().set()添加请求的信息到redis。设置key的失效时间,失效时间在注解上设置。annotation.expireSeconds()就是获取的@RepeatSubmit(expireSeconds = 2)这里,代表这个key2秒后自动删除。2秒后就可以访问了。


import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
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;

@Component
@Aspect
public class NoRepeatSubmitAspect  {


    @Resource
    private RedisTemplate<String,Object> redisTemplate;
    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.example.repeat1.repeatSub.RepeatSubmit)")
    public void preventDuplication() {}

    @Around("preventDuplication()")
    public Object around(ProceedingJoinPoint joinPoint){
        /*
         * 获取请求信息
         */
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        // 获取执行方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取防重复提交注解
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
        // 获取token以及方法标记,生成redisKey和redisValue
        String token = JSONObject.toJSONString(request.getParameterMap());
        String url = request.getRequestURI();

//        redisKey的前缀
        String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
        /*
         *  通过前缀 + url + token + 函数参数签名 来生成redis上的 key
         */
        String redisKey = PREVENT_DUPLICATION_PREFIX
                .concat(url)
                .concat(token)
                .concat(getMethodSign(method, joinPoint.getArgs()));
        // 这个值只是为了标记,不重要
        String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");
        if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
            // 设置防重复操作限时标记(前置通知)
            redisTemplate.opsForValue().set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
            try {
                //正常执行方法并返回
                //ProceedingJoinPoint类型参数可以决定是否执行目标方法,
                // 且环绕通知必须要有返回值,返回值即为目标方法的返回值
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                //确保方法执行异常实时释放限时标记(异常后置通知)
                redisTemplate.delete(redisKey);
                throw new RuntimeException(throwable);
            }
        } else {
            // 重复提交了抛出异常,如果是在项目中,根据具体情况处理。
            throw new RuntimeException("请勿重复提交");
        }
    }

    /**
     * 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
     *
     * @param method 1
     * @param args 1
     * @return 1
     */
    private String getMethodSign(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(JSONObject.toJSONString(arg));
        }
        return DigestUtils.sha1Hex(sb.toString());
    }

}
TestController

import com.example.repeat1.repeatSub.RepeatSubmit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {

    @GetMapping
    @RepeatSubmit(expireSeconds = 2)
    public String get1(){
        return "get1";
    }

}

最后附上目录:

请求效果:

正常访问:

 2秒内再次访问:

上面说的aop需要用到包有点多,现在更新一个依赖少的,功能差不多,可能效率比redis要低点:

package com.sky.common.submit;

import com.alibaba.fastjson.JSONObject;
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.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Aspect
public class NoRepeatSubmitAspect {

    // 本地缓存,用于存储请求防重复提交的标记
    private final Map<String, String> preventDuplicationCache = new ConcurrentHashMap<>();
    //    这个用于记录过期时间的
    private final Map<String, Long> expirationMap = new ConcurrentHashMap<>();

    @Around("@annotation(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) {
        /*
         * 获取请求信息
         */
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        // 获取执行方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 获取防重复提交注解
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
        // 获取token以及方法标记,生成缓存的 key
        String token = JSONObject.toJSONString(request.getParameterMap());
        String url = request.getRequestURI();

        /*
         * 通过前缀 + url + token + 函数参数签名来生成缓存上的 key
         */
        String cacheKey = getCacheKey(url, token, method, joinPoint.getArgs());
        // 这个值只是为了标记,不重要
        String cacheValue = cacheKey.concat(annotation.value()).concat("submit duplication");

        synchronized (preventDuplicationCache) {
//            判断过期就删除key 如果使用redis就不需要这步,直接set的时候就可以设置过期时间。
            if ((expirationMap.containsKey(cacheKey)
                    && expirationMap.get(cacheKey) <= System.currentTimeMillis())) {
                preventDuplicationCache.remove(cacheKey);
            }
            if (!preventDuplicationCache.containsKey(cacheKey)) {
                // 设置防重复操作限时标记和超时时间
                putWithExpiration(cacheKey, cacheValue, Long.parseLong(annotation.value()));
                try {
                    // 正常执行方法并返回
                    // ProceedingJoinPoint类型参数可以决定是否执行目标方法,
                    // 且环绕通知必须要有返回值,返回值即为目标方法的返回值
                    return joinPoint.proceed();
                } catch (Throwable throwable) {
                    // 确保方法执行异常实时释放限时标记(异常后置通知)
                    preventDuplicationCache.remove(cacheKey);
                    throw new RuntimeException(throwable);
                }
            }
            throw new RuntimeException("请勿重复提交");
        }
    }

    /**
     * 生成缓存的 key:采用数字签名算法SHA1对方法签名字符串加签
     *
     * @param url    请求的 URL
     * @param token  请求的 Token
     * @param method 方法
     * @param args   方法参数
     * @return 缓存的 key
     */
    private String getCacheKey(String url, String token, Method method, Object... args) {
        StringBuilder sb = new StringBuilder(url).append(token).append(method.toString());
        for (Object arg : args) {
            sb.append(JSONObject.toJSONString(arg));
        }
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
    }

    /**
     * 设置判断重复访问的key和过期时间
     *
     * @param key        1
     * @param value      1
     * @param expiration 过期时间 秒
     */
    public void putWithExpiration(String key, String value, long expiration) {
        preventDuplicationCache.put(key, value);
        long expireTime = System.currentTimeMillis() + expiration * 1000;
        expirationMap.put(key, expireTime);
    }

}

本人是面向百度编程,有什么不足欢迎大佬指正。

 最后如果对你有一点点帮助,麻烦支持一下。

全国寄快递5元起,电影票8.8折。更多优惠微信关注公众号:【折价寄件】

感谢阅读!!!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值