API限流-利用Redis限制API在X分钟内的调用次数

8 篇文章 0 订阅
该博客介绍了一种使用Redis进行API调用频率限制的策略,通过结合IP和API地址创建key,并利用Redis的分布式锁和过期时间功能,防止爬虫或恶意请求耗尽服务器资源。在Java环境中,通过自定义注解和拦截器实现限制,对每个用户的API访问次数和时间间隔进行控制,当达到预设阈值时,会锁定该用户对该API的访问一段时间。
摘要由CSDN通过智能技术生成
背景

在对外开放我们的API的时候,有时候API调用不一定是来自于我们的APP或者网站。若是一个资源网站,极有可能会遇到爬虫来盗刷我们的数据。导致短时间内API的调用猛增,耗费服务器资源。因此需要在某些需要查数据库、文件等的API处,增加API调用频率限制。

我的思路

某API 1分钟内调用次数限制思路 ---- 利用redis进行限流。
若是负载均衡的api,则在进行判断的时候,要用redis提供的分布式锁setnx进行互斥执行以下步骤
假设API的地址为: /api/resource/list

redis中的数据结构
keyvalue过期时间备注
10.200.120.3:/api/resource/list2021062315:40:23#153min-----正常记录1分钟内api调用次数及上次调用的时间
10.200.120.3:/api/resource/listlocked3minapi访问频率达到限制时,限制IP对api的访问,限制时间为3min

key: 由IP+API地址构成,表明哪个IP正在访问哪个api
value: 由上次访问时间+累计访问次数构成

操作流程说明
  1. IP为10.200.120.3的客户端每次访问/api/resource/list时,首先从redis中取出key= 10.200.120.3:/api/resource/list 的值value
  2. 判断value是否等于locked,如果value=locked,跳转到第3步,如果value为空,则跳转到第4步,否则跳转到第5步
  3. 返回错误代码,提示1分钟内访问api次数超过60次,请稍后再试
  4. 在redis中新增key=10.200.120.3:/api/resource/list value= 2021062315:40:23#1,设置过期时间为3min,并调用api返回结果到调用者
  5. 判断当前api调用时间与value中的时间差值diff是否小于1分钟,若大于等于1分钟,则跳转到第6步,否则跳转到第7步
  6. 更新redis中,key=10.200.120.3:/api/resource/list value= 2021062315:42:23#1,设置超时时间为3min,并调用API返回结果到调用者
  7. 若调用时间差小于1分钟,则判断value中的调用次数是否小于60次,若大于等于60次,则跳转到第8步,否则跳转到第9步
  8. 更新redis中,key=10.200.120.3:/api/resource/list value= locked,并且更新过期时间为3min返回错误代码,提示1分钟内访问api次数超过60次,请稍后再试
  9. 若1分钟内调用次数少于60次,则更新redis中,key=10.200.120.3:/api/resource/list value= 2021062315:42:58#13,设置过期时间为3min,并调用api返回结果到调用者

redis 设置某个key的超时时间后,若后续key的值发生改变,重新set key value时,会删除之前的key value 以及连带的超时时间。所以,如果更新key的值后,还需要有超时时间,最好重新设置一次。

Java代码实现思路
  1. 自定义一个annotation。属性有count:调用次数 durationTime: 时间间隔,比如1分钟内,lockTime: API调用达到限制时,限制多少时长后可以访问其中关于time的,单位可自行定义为秒,或分钟
  2. 在需要做调用限制的方法上面,添加你自定义的注解。并注入对应的属性值
  3. 编写拦截器,拦截 ”/“ 开头的或者你自定义的路由前缀,例如拦截: /rest/rsource/**等。
  4. 进入拦截方法后,判断当前拦截的方法上面是否有自定义的注解,若没有,则步进行后续处理
  5. 若方法定义了注解,则取出注解中定义的各个属性,count、durationTime、lockTime,按照上面的限制思路进行判断,即可。
目前实现的效果
自定义注解
/**
 * @author CoreCmd
 * @date 2021/7/5
 * @apiNote 在durationTime时间内,对某api的访问次数达到maxCount时,将被禁止访问lockTime时间,时间单位为:timeUnit
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
@Inherited
public @interface AccessLimitAnnotation {
    //请求次数限制数量
    int maxCount() default 60;
    //请求间隔时间
    int durationTime() default 1;
    //锁定访问时间
    int lockTime() default 3;
    TimeUnit timeUnit() default TimeUnit.MINUTES;
}

使用方式:在需要访问限制的接口上,加上注解: @AccessLimitAnnotation

 @ApiOperation(value = "获取定时任务类列表")
    @GetMapping(value = "/list/job-beans")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header",name = "authToken", value = "登录token", required = true, dataType = "String")
    })
    @AccessLimitAnnotation
    public CustomResponse listJobBeans(@RequestHeader(value = "authToken")String authToken){
        CustomResponse customResponse = null;
        try {
            customResponse = CustomResponse.ok();
            customResponse.put("jobBeans",myApplicationListener.getJobDefinitions());
        } catch (Exception e){
            log.info("获取定时任务类列表异常",e);
            customResponse = CustomResponse.error("获取定时任务类列表异常,请联系管理员");
        }
        return customResponse;
    }
自定义拦截器进行api拦截

在实际使用中,key的组成,我换成了token+api的方式,针对某个用户进行限制,不针对IP限制,后续有需要再自行改动。

//校验权限
        if(handler instanceof HandlerMethod) {
            AccessLimitAnnotation accessLimitAnnotation = ((HandlerMethod) handler).getMethodAnnotation(AccessLimitAnnotation.class);
            if (null != accessLimitAnnotation){
                int maxCount = accessLimitAnnotation.maxCount();
                int durationTime = accessLimitAnnotation.durationTime();
                int lockTime = accessLimitAnnotation.lockTime();
                TimeUnit timeUnit = accessLimitAnnotation.timeUnit();
                String reqPath = request.getRequestURI();
                String accessLimitCheckKey = authToken + ":"+ URLEncoder.encode(reqPath,"utf-8");
                String accessInfo = redisTemplate.opsForValue().get(accessLimitCheckKey);
                if (null != accessInfo){
                    if ("locked".equalsIgnoreCase(accessInfo)){
                        isAccessAllowed = false;
                    } else {
                        String []accessTimeAndCount =  accessInfo.split("#");
                        Date lastAccessTime = sdf.parse(accessTimeAndCount[0]);
                        Date nowDate = new Date();
                        int accessCount = Integer.parseInt(accessTimeAndCount[1]);
                        long diff = nowDate.getTime() - lastAccessTime.getTime();
                        long nd = 1000 * 24 * 60 * 60;
                        long nh = 1000 * 60 * 60;
                        long nm = 1000 * 60;
                        long min = diff % nd % nh / nm;
                        if (min >= durationTime){
                            redisTemplate.opsForValue().set(accessLimitCheckKey,sdf.format(nowDate)+"#"+1,lockTime,timeUnit);
                        } else if (accessCount >= maxCount){
                            redisTemplate.opsForValue().set(accessLimitCheckKey,"locked",lockTime,timeUnit);
                            isAccessAllowed = false;
                        } else {
                            redisTemplate.opsForValue().set(accessLimitCheckKey,sdf.format(nowDate) + "#"+(accessCount + 1),lockTime,timeUnit);
                        }
                    }
                } else {
                    redisTemplate.opsForValue().set(accessLimitCheckKey,sdf.format(new Date())+"#"+1,lockTime,timeUnit);
                }
            }
访问效果:

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值