前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过redis和注解实现ip访问频次限制

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

作者头像
烤红薯
发布2021-12-23 15:05:46
2K0
发布2021-12-23 15:05:46
举报
文章被收录于专栏:烤红薯的学习笔记

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

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

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

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

redis中的数据如图:

验证方法:

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

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

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

开始实现

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

代码语言:javascript
复制
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为时间范围(本处时间单位采用的是秒)

代码语言:javascript
复制
@IpMax(count = 3, time = 10)
@ApiOperation("查询用户的数量")
@PostMapping("/getUserCount")
public long getUserCount() {
    return userService.getUserCount();
}

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

代码语言:javascript
复制
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验证的逻辑处理

代码语言:javascript
复制
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 (仅含使用到的方法)

代码语言:javascript
复制
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(仅含使用到的方法)

代码语言:javascript
复制
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中的数据为

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 jackHarbor 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis®
腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档