zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

通过redis和注解实现ip访问频次限制

2023-04-18 16:11:08 时间

浙江的八月下旬还是有点热

实现目标:在接口上面加一个注解。限制单个ip在指定时间范围内可以访问的次数。

实现的逻辑是,将访问的ip和要访问的url作为key存放在reids中。

设定其数据类型为list,value的值为每次访问的时间戳。

redis中的数据如图:

验证方法:

当list的长度达到了设定的访问最大次数,

就和用当前的时间戳和最早存放的时间戳做对比。

若相差时间小于设定的时间范围,则说明此ip访问此接口达到了上限。

开始实现

新建自定义注解用在controller中需要限制的接口上面

import java.lang.annotation.*;
/**
 * ip 最大 访问次数
 * time 时间范围
 * (@interface 注解类)
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IpMax {

    /**
     * 允许访问的最大次数
     */
    int count() default Integer.MAX_VALUE;

    /**
     * 时间段,单位为秒,默认值三十秒
     */
    int time() default 30;


}

使用如下, 直接在原有接口上面添加刚刚定义好的注解。其中 count 为最大访问次数,time为时间范围(本处时间单位采用的是秒)

@IpMax(count = 3, time = 10)
@ApiOperation("查询用户的数量")
@PostMapping("/getUserCount")
public long getUserCount() {
    return userService.getUserCount();
}

在拦截器中添加ip是否超频的验证逻辑

import com.batata.continuing.config.changeable.OpenOrClose;
import com.batata.continuing.config.count.ip.IpCount;
import com.batata.continuing.config.count.ip.IpMax;
import com.batata.continuing.config.moreNote.token.NotNeedToken;
import com.batata.continuing.utils.JwtUtil;
import org.nutz.lang.Lang;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private OpenOrClose openOrClose;

    /**
     * 用来验证ip访问次数的逻辑层
     */
    @Autowired
    private IpCount ipCount;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) {

        // 从 http 请求头中取出 token
        String token = request.getHeader("token");
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();


        // ip超频访问方面的验证   start
        if (method.isAnnotationPresent(IpMax.class)) { // 判断访问的接口是否有此注解
            String requestURI = request.getRequestURI(); // 请求的url
            String ip = Lang.getIP(request); // 获得请求者的ip(可根据自己的方法,我这边用的是nutz的工具类,引入请参考历史文章)
            IpMax ipMax = method.getAnnotation(IpMax.class); // 获得注解中的内容
            int count = ipMax.count(); // 访问次数
            long time = ipMax.time(); //  时间范围
            // 通过封装的方法,判断ip是否可以通过验证
            boolean ipIsOk = ipCount.ipIsOk(requestURI, ip, count, time);
            if (!ipIsOk) { // 超过访问次数
                // 时间范围内超出最大访问次数   注:超过访问次数的处理方式可自行根据具体需求
                throw new RuntimeException("本接口" + time + "秒内可以请求" + count + "次,您已超出最大访问次数!!!");
            }
        }
        // ip超频访问方面的验证   end

     

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }


}

ip验证的逻辑处理

import com.batata.continuing.config.RedisKeyConfig;
import com.batata.continuing.service.common.RedisService;
import com.batata.continuing.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;


/**
 * ip 相关的统计
 */
@Component
public class IpCount {


    /**
     * 封装的redis方法
     */
    @Autowired
    private RedisService redisService;


    @Autowired
    public RedisTemplate redisTemplate;


    /**
     * ip 是否可以继续访问
     *
     * @param url      访问的url
     * @param ip       访问的ip
     * @param maxValue 最大的访问次数
     * @param time     时间范围 单位为秒
     * @return true 可以继续访问  false 超出限制,不可以继续访问
     */
    public boolean ipIsOk(String url, String ip, int maxValue, long time) {
        boolean ipIsOk = true;

        String key = RedisKeyConfig.ipCount + ip + url; // 根据指定规则拼接生成此次访问的key
        List<Long> timeList = redisService.getCacheList(key); // 查询redis中已有的数据


        if (
                timeList.size() == 0 || timeList.size() < maxValue
        ) { // 没有记录或者没有达到最大访问次数不做超时验证
            addNewTime(key, time); // 添加当前的时间到list中
            return ipIsOk;

        } // 若不满足此条件,则证明list中的值达到了最大数量(即访问的次数)

        // 判断达到规定的访问次数的用时是否小于规定的时间
        // (当前时间戳-最旧记录的时间戳)< 限定的时间转毫秒
        if (
                (DateUtils.getNowTimeLong() - timeList.get(0)) < (time * 1000)
        ) {
            // 未达到,证明指定范围时间内访问数量超过的定义数量
            ipIsOk = false;
        }

        // 删除第一个值(就是时间最旧的那个值,我这边是下标为0的,手动在redis客户端测试的为row最大的值。这个根据自己的具体情况)
        redisTemplate.opsForList().remove(key, 1, timeList.get(0));
        addNewTime(key, time); // 添加当前的时间到list中

        return ipIsOk;
    }

    /**
     * 往redis中添加新的数据,注:新增的值row在后
     *
     * @param key  key
     * @param time 有效时间,单位为秒
     */
    public void addNewTime(String key, long time) {
        List<Long> nowTime = new ArrayList<>();
        nowTime.add(DateUtils.getNowTimeLong()); // 当前时间戳
        redisService.setCacheList(key, nowTime); // 追加值或新缓存值
        redisService.expire(key, time + 1); // 设置有效时间 
    }


}

// 用到的工具类 start

redis操作封装的service (仅含使用到的方法)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisService {


    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间  注:此处时间单位为秒
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    
    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

}

时间util(仅含使用到的方法)

import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.lang3.time.DateFormatUtils;

public class DateUtils {
      
    /**
     * 获得当前时间的时间戳
     * @return 当前时间戳
     */
    public static long getNowTimeLong() {
        return System.currentTimeMillis();
    }


}

// 用到的工具类 end

启动项目访问加了注解的接口测试

第四次访问出现请求不通过,(此处的处理方式为抛出了一个异常)配置成功

查看redis中的数据为

另注:也可以配合超频做一个黑名单的机制