基于redis实现IP访问频次控制(超简单)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 基于redis实现IP访问频次控制(超简单)

一、背景描述



最近做了一个项目,通过调用短信网关实现手机号和验证码登录。但是如果有人恶意调用,会发出很多无谓的短信,而且会增加短信服务的费用。于是对接口做了请求频率限制 Rate limiting。例如限制一个用户1分钟内最多可以范围100次。这里简要的复现一下实现思路。


思路:以类名+调用方法名+ip作为key


  1. 当用户调用接口的时候,先查询redis中是否有存在该key,获取该key所对应的value,比较value和frequency,如果小于frequency,则在原来的基础上value++;如果大于则返回访问频率过于频繁。
  2. 如果不存在,则将该key存入redis,value为1,设置过期时间。


二、代码演示



1. pom文件


<!--引入web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring boot 测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--分布式锁-->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>2.3.0</version>
</dependency>
<!--连接池-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.0</version>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!-- swagger -->
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>swagger-bootstrap-ui</artifactId>
    <version>1.9.6</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<!--aop-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>


2、redis的配置


  1. 配置文件
#redis
redis.host=192.168.1.6
redis.password=
redis.port=6379
redis.taskScheduler.poolSize=100
redis.taskScheduler.defaultLockMaxDurationMinutes=10
redis.default.timeout=10
redisCache.expireTimeInMilliseconds=1200000


  1. 配置类
package com.example.redis_demo_limit.redis;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;
import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;
@Configuration
public class RedisConfig {
    @Value("${redis.host}")
    private String redisHost;
    @Value("${redis.port}")
    private int redisPort;
    @Value("${redis.password}")
    private String password;
    @Value("${redis.taskScheduler.poolSize}")
    private int tasksPoolSize;
    @Value("${redis.taskScheduler.defaultLockMaxDurationMinutes}")
    private int lockMaxDuration;
    @Bean(destroyMethod = "shutdown")
    ClientResources clientResources() {
        return DefaultClientResources.create();
    }
    @Bean
    public RedisStandaloneConfiguration redisStandaloneConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration(redisHost, redisPort);
        if (password != null && !password.trim().equals("")) {
            RedisPassword redisPassword = RedisPassword.of(password);
            redisStandaloneConfiguration.setPassword(redisPassword);
        }
        return redisStandaloneConfiguration;
    }
    @Bean
    public ClientOptions clientOptions() {
        return ClientOptions.builder()
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
                .autoReconnect(true).build();
    }
    @Bean
    LettucePoolingClientConfiguration lettucePoolConfig(ClientOptions options, ClientResources dcr) {
        return LettucePoolingClientConfiguration.builder().poolConfig(new GenericObjectPoolConfig())
                .clientOptions(options).clientResources(dcr).build();
    }
    @Bean
    public RedisConnectionFactory connectionFactory(
            RedisStandaloneConfiguration redisStandaloneConfiguration,
            LettucePoolingClientConfiguration lettucePoolConfig) {
        return new LettuceConnectionFactory(redisStandaloneConfiguration, lettucePoolConfig);
    }
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    @Primary
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    @Bean
    public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
        return new RedisLockProvider(connectionFactory);
    }
    @Bean
    public ScheduledLockConfiguration taskSchedulerLocker(LockProvider lockProvider) {
        return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider)
                .withPoolSize(tasksPoolSize).withDefaultLockAtMostFor(Duration.ofMinutes(lockMaxDuration))
                .build();
    }
}


3. redis操作工具类


  1. 接口类
package com.example.redis_demo_limit.redis;
public interface DataCacheRepository<T> {
  boolean add(String collection, String hkey, T object, Long timeout);
  boolean delete(String collection, String hkey);
  T find(String collection, String hkey, Class<T> tClass);
  Boolean isAvailable();
  /**
   * redis 加锁
   * 
   * @param key
   * @param second
   * @return
   */
  Boolean lock(String key, String value, Long second);
  Object getValue(String key);
  /**
   * redis 解锁
   * 
   * @param key
   * @return
   */
  void unLock(String key);
  void setIfAbsent(String key, long value, long ttl);
  void increment(String key);
  Long get(String key);
  void set(String key, long value, long ttl);
  void set(Object key, Object value, long ttl);
  Object getByKey(String key);
  void getLock(String key, String clientID) throws Exception;
  void releaseLock(String key, String clientID);
}


  1. 实现类
package com.example.redis_demo_limit.redis;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
@Slf4j
@Repository
public class CacheRepository<T> implements com.example.redis_demo_limit.redis.DataCacheRepository<T> {
  private static final ObjectMapper OBJECT_MAPPER;
  private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC");
  static {
    OBJECT_MAPPER = new ObjectMapper();
    OBJECT_MAPPER.setTimeZone(DEFAULT_TIMEZONE);
  }
  Logger logger = LoggerFactory.getLogger(CacheRepository.class);
  @Autowired
  RedisTemplate template; // and we're in business
  @Value("${redis.default.timeout}00")
  Long defaultTimeOut;
  public boolean addPermentValue(String collection, String hkey, T object) {
    try {
      String jsonObject = OBJECT_MAPPER.writeValueAsString(object);
      template.opsForHash().put(collection, hkey, jsonObject);
      return true;
    } catch (Exception e) {
      logger.error("Unable to add object of key {} to cache collection '{}': {}", hkey, collection,
          e.getMessage());
      return false;
    }
  }
  @Override
  public boolean add(String collection, String hkey, T object, Long timeout) {
    Long localTimeout;
    if (timeout == null) {
      localTimeout = defaultTimeOut;
    } else {
      localTimeout = timeout;
    }
    try {
      String jsonObject = OBJECT_MAPPER.writeValueAsString(object);
      template.opsForHash().put(collection, hkey, jsonObject);
      template.expire(collection, localTimeout, TimeUnit.SECONDS);
      return true;
    } catch (Exception e) {
      logger.error("Unable to add object of key {} to cache collection '{}': {}", hkey, collection,
          e.getMessage());
      return false;
    }
  }
  @Override
  public boolean delete(String collection, String hkey) {
    try {
      template.opsForHash().delete(collection, hkey);
      return true;
    } catch (Exception e) {
      logger.error("Unable to delete entry {} from cache collection '{}': {}", hkey, collection,
          e.getMessage());
      return false;
    }
  }
  @Override
  public T find(String collection, String hkey, Class<T> tClass) {
    try {
      String jsonObj = String.valueOf(template.opsForHash().get(collection, hkey));
      return OBJECT_MAPPER.readValue(jsonObj, tClass);
    } catch (Exception e) {
      if (e.getMessage() == null) {
        logger.error("Entry '{}' does not exist in cache", hkey);
      } else {
        logger.error("Unable to find entry '{}' in cache collection '{}': {}", hkey, collection,
            e.getMessage());
      }
      return null;
    }
  }
  @Override
  public Boolean isAvailable() {
    try {
      return template.getConnectionFactory().getConnection().ping() != null;
    } catch (Exception e) {
      logger.warn("Redis server is not available at the moment.");
    }
    return false;
  }
  @Override
  public Boolean lock(String key, String value, Long second) {
    Boolean absent = template.opsForValue().setIfAbsent(key, value, second, TimeUnit.SECONDS);
    return absent;
  }
  @Override
  public Object getValue(String key) {
    return template.opsForValue().get(key);
  }
  @Override
  public void unLock(String key) {
    template.delete(key);
  }
  @Override
  public void increment(String key) {
    RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
    counter.incrementAndGet();
  }
  @Override
  public void setIfAbsent(String key, long value, long ttl) {
    ValueOperations<String, Object> ops = template.opsForValue();
    ops.setIfAbsent(key, value, Duration.ofSeconds(ttl));
  }
  @Override
  public Long get(String key) {
    RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
    return counter.get();
  }
  @Override
  public void set(String key, long value, long ttl) {
    RedisAtomicLong counter = new RedisAtomicLong(key, template.getConnectionFactory());
    counter.set(value);
    counter.expire(ttl, TimeUnit.SECONDS);
  }
  @Override
  public void set(Object key, Object value, long ttl) {
    template.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
  }
  @Override
  public Object getByKey(String key) {
    return template.opsForValue().get(key);
  }
  @Override
  public void getLock(String key, String clientID) throws Exception {
    Boolean lock = false;
    // 重试3次,每间隔1秒重试1次
    for (int j = 0; j <= 3; j++) {
      lock = lock(key, clientID, 10L);
      if (lock) {
        log.info("获得锁》》》" + key);
        break;
      }
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        log.error("线程休眠异常", e);
        break;
      }
    }
    // 重试3次依然没有获取到锁,那么返回服务器繁忙,请稍后重试
    if (!lock) {
      throw new Exception("服务繁忙");
    }
  }
  @Override
  public void releaseLock(String key, String clientID) {
    if (clientID.equals(getByKey(key))) {
      unLock(key);
    }
  }
}


4. 访问频次实现核心逻辑


  1. 注解
package com.example.redis_demo_limit.annotation;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitedAccess {
  /**
   * 从第一次访问接口的时间到周期时间内,最大访问频率次,默认60次
   * @return
   */
  long frequency() default 60;
  /**
   * 周期时间,默认30分钟内
   * @return
   */
  long second() default 30*60;
}


  1. 切面类
package com.example.redis_demo_limit.annotation;
import com.example.redis_demo_limit.redis.DataCacheRepository;
import lombok.extern.log4j.Log4j2;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
@Log4j2
//@Order
public class LimitedAccessAspect {
    public static String LIMITED_ACCESS_ASPECT_COLLECTION = "LIMITED_ACCESS_ASPECT_COLLECTION";
    @Autowired
    private DataCacheRepository redisCacheService;
    @Pointcut("@annotation(limitedAccess)")
    public void limitAccessPointCut(LimitedAccess  limitedAccess) {
        // 限制接口调用切面类
    }
    @Around(value = "limitAccessPointCut(limitedAccess)", argNames = "point,limitedAccess")
    public Object doAround(ProceedingJoinPoint point, LimitedAccess limitedAccess) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null != attributes) {
            String className = point.getTarget().getClass().getName();
            String methodName = point.getSignature().getName();
            HttpServletRequest request = attributes.getRequest();
            String remoteAddr = request.getRemoteAddr();
            log.info("remoteAddr地址:" + remoteAddr);
            //String realRequestIps = request.getHeader("X-Forwarded-For");
            String key = LIMITED_ACCESS_ASPECT_COLLECTION + className + "." + methodName + "#" + remoteAddr;
            try {
                long limit = redisCacheService.get(key);
                if (limit > 0) {
                    // 时间段内超过访问频次上限 - 阻断
                    if (limit >= limitedAccess.frequency()) {
                        log.info("接口调用过于频繁 {}", key);
//
                        return "接口调用过于频繁!!!";
                    }
                    redisCacheService.increment(key);
                } else {
                    redisCacheService.set(key, 1, limitedAccess.second());
                }
            } catch (Exception e) {
                log.debug(e.getStackTrace());
            }
        }
        return point.proceed();
    }
}


三、调用方法



package com.example.redis_demo_limit.controller;
import com.example.redis_demo_limit.annotation.LimitedAccess;
import com.example.redis_demo_limit.redis.DataCacheRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/redis")
public class RedisController {
    @Resource
    private DataCacheRepository dataCacheRepository;
    //这个设置为1秒1次,方便测试
    @LimitedAccess(frequency = 1,second = 1)
    @PostMapping("/add")
    public String add(String str){
        dataCacheRepository.set("str","add success",200L);
        return "success";
    }
}


四、测试结果



测试


2020-09-22 00:24:21.786  INFO 30552 --- [nio-8080-exec-3] c.e.r.annotation.LimitedAccessAspect     : remoteAddr地址:0:0:0:0:0:0:0:1
2020-09-22 00:24:22.417  INFO 30552 --- [nio-8080-exec-9] c.e.r.annotation.LimitedAccessAspect     : remoteAddr地址:0:0:0:0:0:0:0:1
2020-09-22 00:24:22.419  INFO 30552 --- [nio-8080-exec-9] c.e.r.annotation.LimitedAccessAspect     : 接口调用过于频繁 LIMITED_ACCESS_ASPECT_COLLECTIONcom.example.redis_demo_limit.controller.RedisController.add#0:0:0:0:0:0:0:1


从测试结果来看,它第二次访问的时候,直接返回了接口访问过于频繁。所以实现了接口访问频次的控制。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3月前
|
缓存 NoSQL 网络安全
【Azure Redis 缓存】Azure Redis服务开启了SSL(6380端口), PHP如何访问缓存呢?
【Azure Redis 缓存】Azure Redis服务开启了SSL(6380端口), PHP如何访问缓存呢?
|
7天前
|
NoSQL 编译器 Linux
【赵渝强老师】Redis的安装与访问
本文基于Redis 6.2版本,详细介绍了在CentOS 7 64位虚拟机环境中部署Redis的步骤。内容包括安装GCC编译器、创建安装目录、解压安装包、编译安装、配置文件修改、启动服务及验证等操作。视频讲解和相关图片帮助理解每一步骤。
|
1月前
|
安全 NoSQL 网络安全
漏洞检测与防御:Redis未授权访问漏洞复现
漏洞检测与防御:Redis未授权访问漏洞复现
|
3月前
|
缓存 负载均衡 NoSQL
【Azure Redis】Azure Redis添加了内部虚拟网络后,其他区域的主机通过虚拟网络对等互连访问失败
【Azure Redis】Azure Redis添加了内部虚拟网络后,其他区域的主机通过虚拟网络对等互连访问失败
|
3月前
|
缓存 NoSQL 网络安全
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
|
3月前
|
缓存 NoSQL Redis
【Azure Redis 缓存】C#程序是否有对应的方式来优化并缩短由于 Redis 维护造成的不可访问的时间
【Azure Redis 缓存】C#程序是否有对应的方式来优化并缩短由于 Redis 维护造成的不可访问的时间
|
3月前
|
缓存 NoSQL Redis
【Azure Redis 缓存】Azure Redis加入VNET后,在另一个区域(如中国东部二区)的VNET无法访问Redis服务(注:两个VNET已经结对,相互之间可以互ping)
【Azure Redis 缓存】Azure Redis加入VNET后,在另一个区域(如中国东部二区)的VNET无法访问Redis服务(注:两个VNET已经结对,相互之间可以互ping)
|
3月前
|
缓存 NoSQL 网络协议
【Azure Redis 缓存】如何使得Azure Redis可以仅从内网访问? Config 及 Timeout参数配置
【Azure Redis 缓存】如何使得Azure Redis可以仅从内网访问? Config 及 Timeout参数配置
|
3月前
|
网络协议 NoSQL 网络安全
【Azure 应用服务】由Web App“无法连接数据库”而逐步分析到解析内网地址的办法(SQL和Redis开启private endpoint,只能通过内网访问,无法从公网访问的情况下)
【Azure 应用服务】由Web App“无法连接数据库”而逐步分析到解析内网地址的办法(SQL和Redis开启private endpoint,只能通过内网访问,无法从公网访问的情况下)
|
3月前
|
缓存 NoSQL 网络协议
【Azure Redis 缓存 Azure Cache For Redis】在创建高级层Redis(P1)集成虚拟网络(VNET)后,如何测试VNET中资源如何成功访问及配置白名单的效果
【Azure Redis 缓存 Azure Cache For Redis】在创建高级层Redis(P1)集成虚拟网络(VNET)后,如何测试VNET中资源如何成功访问及配置白名单的效果