[Java] Teach you how to implement interface anti-brush

foreword

When we browse the background of the website, if we request frequently, the website will prompt "Do not submit repeatedly", so what is the use of this function, and how is it realized?

In fact, this is a processing method of interface anti-brush. By limiting the number of requests made by the same user to the same interface within a certain period of time, the purpose is to prevent malicious access from increasing the pressure on the server and database, and to prevent users from submitting repeatedly. .

Idea analysis

There are many implementation ideas for interface anti-brush, such as: interceptor/AOP+Redis, interceptor/AOP+local cache, front-end restrictions, etc. Here we talk about the implementation of interceptor+Redis.

The principle is to intercept the interface request by the interceptor, and then go to redis to check whether the request already exists. If it does not exist, the request will be cached, and if it already exists, an exception will be returned. For details, please refer to the following figure
insert image description here

Implementation

Note: The AjaxResult in the following code is a unified return object, so I won’t post the code here, and you can write it according to your own business scenarios.

Write RedisUtils

import com.apply.core.exception.MyRedidsException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Redis工具类
 */
@Component
public class RedisUtils {
    
    

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /****************** common start ****************/
    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
    
    
        try {
    
    
            if (time > 0) {
    
    
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
    
    
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
    
    
        try {
    
    
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
    
    
        if (key != null && key.length > 0) {
    
    
            if (key.length == 1) {
    
    
                redisTemplate.delete(key[0]);
            } else {
    
    
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
    /****************** common end ****************/


    /****************** String start ****************/

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
    
    
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
    
    
        try {
    
    
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
    
    
        try {
    
    
            if (time > 0) {
    
    
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
    
    
                set(key, value);
            }
            return true;
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
    
    
        if (delta < 0) {
    
    
            throw new MyRedidsException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
    
    
        if (delta < 0) {
    
    
            throw new MyRedidsException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
    /****************** String end ****************/
}

Define Interceptor¶

import com.alibaba.fastjson.JSON;
import com.apply.common.utils.redis.RedisUtils;
import com.apply.common.validator.annotation.AccessLimit;
import com.apply.core.http.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * @description 重复请求拦截
 * @date 2023-08-13 14:14
 */
@Component
public class RepeatRequestIntercept extends HandlerInterceptorAdapter {
    
    

    @Autowired
    private RedisUtils redisUtils;

    /**
     * 限定时间 单位:秒
     */
    private final int seconds = 1;

    /**
     * 限定请求次数
     */
    private final int max = 1;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //判断请求是否为方法的请求
        if (handler instanceof HandlerMethod) {
    
    
            String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
            Object requestCountObj = redisUtils.get(key);
            if (Objects.isNull(requestCountObj)) {
    
    
                //若为空则为第一次请求
                redisUtils.set(key, 1, seconds);
            } else {
    
    
                response.setContentType("application/json;charset=utf-8");
                ServletOutputStream os = response.getOutputStream();
                AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
                String jsonString = JSON.toJSONString(result);
                os.write(jsonString.getBytes());
                os.flush();
                os.close();
                return false;
            }
        }
        return true;
    }

}

Then we register the interceptor into the container

import com.apply.common.validator.intercept.RepeatRequestIntercept;
import com.apply.core.base.entity.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @description 
 * @date 2023-08-13 14:17
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    

    @Autowired
    private RepeatRequestIntercept repeatRequestIntercept;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(repeatRequestIntercept);
    }
}

Let's write another interface for testing

import com.apply.common.validator.annotation.AccessLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description
 * @date 2023-08-13 14:35
 */
@RestController
public class TestController {
    
    

    @GetMapping("/test")
    public String test(){
    
    
        return "SUCCESS";
    }

}

Finally, let's see if the result meets our expectations:
the first request within 1 second:
insert image description here

Second request within 1 second:
insert image description here

It has indeed met our expectations, but if we intercept a specific interface, or if the limited interception time and times for different interfaces are different, this implementation method cannot meet our needs, so we have to propose improvements.

Improve

We can write a custom annotation, and set seconds and max as the attributes of the annotation, and then judge whether the request method contains the annotation in the interceptor. If it does, execute the interception method, and return directly if it does not.
Custom annotation RequestLimit

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @description 幂等性注解
 * @date 2023-08-13 15:10
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestLimit {
    
    

    /**
     * 限定时间
     */
    int seconds() default 1;

    /**
     * 限定请求次数
     */
    int max() default 1;

}

Improved RepeatRequestIntercept

/**
 * @description 重复请求拦截
 * @date 2023-08-13 15:14
 */
@Component
public class RepeatRequestIntercept extends HandlerInterceptorAdapter {
    
    

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //判断请求是否为方法的请求
        if (handler instanceof HandlerMethod) {
    
    
            HandlerMethod hm = (HandlerMethod) handler;
            //获取方法中是否有幂等性注解
            RequestLimit anno = hm.getMethodAnnotation(RequestLimit.class);
            //若注解为空则直接返回
            if (Objects.isNull(anno)) {
    
    
                return true;
            }
            int seconds = anno.seconds();
            int max = anno.max();
            String key = request.getRemoteAddr() + "-" + request.getMethod() + "-" + request.getRequestURL();
            Object requestCountObj = redisUtils.get(key);
            if (Objects.isNull(requestCountObj)) {
    
    
                //若为空则为第一次请求
                redisUtils.set(key, 1, seconds);
            } else {
    
    
                //限定时间内的第n次请求
                int requestCount = Integer.parseInt(requestCountObj.toString());
                //判断是否超过最大限定请求次数
                if (requestCount < max) {
    
    
                    //未超过则请求次数+1
                    redisUtils.incr(key, 1);
                } else {
    
    
                    //否则拒绝请求并返回信息
                    refuse(response);
                    return false;
                }
            }
        }
        return true;
    }
    /**
     * @date 2023-08-14 15:25
     * @author Bummon
     * @description 拒绝请求并返回结果
     */
    private void refuse(HttpServletResponse response) throws IOException {
    
    
        response.setContentType("application/json;charset=utf-8");
        ServletOutputStream os = response.getOutputStream();
        AjaxResult<Void> result = AjaxResult.error(100, "请求已提交,请勿重复请求");
        String jsonString = JSON.toJSONString(result);
        os.write(jsonString.getBytes());
        os.flush();
        os.close();
    }

}

In this way we can achieve our needs.

Guess you like

Origin blog.csdn.net/u011397981/article/details/132268872