利用注解实现规定时间内限制接口访问次数

引言

在用户访问的时候,不免因为各种原因导致用户会在短时间内频繁的访问一个接口,而为了限制这种情况带来的危害,通常会约定用户在一段时间内具有固定的访问次数,已达到解决用户短时间内频繁访问接口的问题。

实现方案

这里选择的是 注解 + AOP  的形式去实现,实现过程会使用到,注解、AOP、Redis、锁相关的知识。

代码流程图

实现代码与代码截图

1.注解代码

具体代码图与代码如下:

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface APICountLimit {

    // 设置访问次数
    String limitNumber() default "3";

    // 设置限制时间
    long limitTime() default 1L;

    // 设置限制的时间单位
    TimeUnit timeUnit() default TimeUnit.MINUTES;

}

2.切面代码

具体切面与切面截图如下:

@Component
@Aspect
public class APICountLimitAspect {



    public static final String  LIMIT_REQUIRES = "limit:require:";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(apiCountLimit)")
    public Object around(ProceedingJoinPoint joinPoint, APICountLimit apiCountLimit) throws Throwable {
        // 获取对应的真实 ip,用于限制对应 ip 的范围时间内的访问次数
        String ipAddress = getIpAddress(request);
        // 获取拦截的注解中的限制次数参数
        String limitNumber = apiCountLimit.limitNumber();
        TimeUnit timeUnit = apiCountLimit.timeUnit();
        long limitTime = apiCountLimit.limitTime();
        // 在 redis 中以 IP 组成 key 的存储信息
        String key = LIMIT_REQUIRES + ipAddress;

        // 根据 IPKey 获取 redis 中的值
        String getValueByIp = (String)  stringRedisTemplate.opsForHash().get(key,"visitsTime");

        // 获取到的值为空值,为对应 IP 设置初始值信息并通过
        if( getValueByIp == null){
            // 第一次访问的时候的时间戳
            long zeroTime = System.currentTimeMillis();
            // 第一次访问时初始化 redis 中的 map 信息
            HashMap<Object, Object> map = new HashMap<>();
            map.put("numberOfVisits","1");
            map.put("visitsTime",String.valueOf(zeroTime));
            // 存储 map 类型,并设置过期时间。
            stringRedisTemplate.opsForHash().putAll(key ,map);
            stringRedisTemplate.expire(key,limitTime,timeUnit);
        }
        // 当数据存在但是访问次数大于限制次数时返回不合法值
        else if (Integer.parseInt(stringRedisTemplate.opsForHash().get(key,"numberOfVisits").toString()) >= Integer.parseInt(limitNumber)) {
            return "当前访问频繁请稍等";
        }
        // 当数据存在并且访问次数小于限制次数 限制次数 +1 并通过
        else {
             // 避免程序在运行时候恰巧 redis 数据过期
             if(stringRedisTemplate.opsForHash().get(key,"visitsTime") == null) around(joinPoint,apiCountLimit) ;
             // 获取当前 IP 的已访问次数
            synchronized (this) {
                int currentNumber = (Integer.parseInt(stringRedisTemplate.opsForHash().get(key,"numberOfVisits").toString()));
                currentNumber++;
                // redis 中重写当前 IP 的访问次数
                stringRedisTemplate.opsForHash().put(key,"numberOfVisits",String.valueOf(currentNumber));
            }


            // 获取 redis 中记录的访问时间戳
             String reportRedisTimestamp = stringRedisTemplate.opsForHash().get(key, "visitsTime").toString();
             // 本次访问时间的时间戳
             long currentTimestamp = System.currentTimeMillis();
             // 计算本次的访问时间距离上次的访问时间的时间间隔
             long timeInterval = currentTimestamp - Long.parseLong(reportRedisTimestamp);
             // redis 中重写当前 IP 近一次访问的时间戳
             stringRedisTemplate.opsForHash().put(key,"visitsTime",String.valueOf(currentTimestamp));
             // 重新设置 redis 中 对应 IP 的生存时间( IPKey 的剩余时间 = 当前所剩时间 - 访问的间隔时间)
             stringRedisTemplate.expire(key,(stringRedisTemplate.getExpire(key)*1000 - timeInterval)/1000 ,TimeUnit.SECONDS);

        }

        return joinPoint.proceed();
    }

代码思路与解释

代码的限制方案是根据访问者的 IP 去进行实现的,实际中可以根据真实的参数去进行限制,比如,token、cookie、JWT等。

在注解代码中,开发者可以自定义的配置实际的限制次数、限制时间、限制时间单位。

在切面代码中,当用户开始访问时,获取到对应的注解参数,并获取对应的请求 IP,并结合约定的 redis 中的 key 前缀组成完整的 key 参数,根据完整的 key 信息去获取 redis 中的数据,如果数据不存在的话,以 key 和 map 组成一条数据信息,在 map 中存储第一次的访问时间戳以及访问次数并为 key 设置注解中的生存时间,如果数据存在的话,则获取 map 中记录访问次数的信息,判断是否大于了注解中的约定次数,大于的话返回错误信息,代码结束;小于的话,获取 redis 中存储的访问次数并且 +1 存回 redis 中(切记这里要加锁,可以使用 Jmeter 测试一下不加锁的情况造成的问题)之后获取本次访问的时间戳并且从 redis 中获取上次访问时间的时间戳,两个时间戳的差作为两次访问的时间差并且将当前 key 的 生存时间 - 访问时间 作为 key 的新生存时间(注意这里的单位转换)并且将本次的访问时间戳记录到 redis 中 用于作为下次访问的上次时间戳。

总结

本次问题的解决就结束了,实现这个需求的方案有很多种,实现过程中有很些细节问题需要解决,也希望大佬能斧正提出更好的解决方案。

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以通过使用拦截器或者过滤器实现对指定接口访问次数限制。 以下是使用拦截器实现限制访问次数的示例代码: 1. 创建自定义注解 `@AccessLimit`,用于标记需要限制访问次数接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AccessLimit { // 默认访问次数限制为5次 int limit() default 5; // 时间段,单位为秒,默认为60秒 int seconds() default 60; } ``` 2. 创建拦截器 `AccessLimitInterceptor`,用于实现限制访问次数的逻辑。 ```java @Component public class AccessLimitInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断是否标注了@AccessLimit注解 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (!handlerMethod.hasMethodAnnotation(AccessLimit.class)) { return true; } // 获取@AccessLimit注解 AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class); // 获取接口访问限制次数时间段 int limit = accessLimit.limit(); int seconds = accessLimit.seconds(); // 获取请求的IP地址和接口地址 String ipAddr = getIpAddress(request); String requestUrl = request.getRequestURI(); // 设置Redis中的Key String redisKey = String.format("%s_%s", ipAddr, requestUrl); // 判断Redis中是否存在Key ValueOperations<String, Object> valueOps = redisTemplate.opsForValue(); if (!redisTemplate.hasKey(redisKey)) { // 第一次访问,设置初始值 valueOps.set(redisKey, 1, seconds, TimeUnit.SECONDS); } else { // 已经访问过,进行访问次数限制判断 int count = (int) valueOps.get(redisKey); if (count >= limit) { // 超出访问次数限制,返回错误信息 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("超出访问次数限制"); return false; } else { // 访问次数加1 valueOps.increment(redisKey, 1); } } } return true; } /** * 获取请求的IP地址 */ private String getIpAddress(HttpServletRequest request) { String ipAddr = request.getHeader("x-forwarded-for"); if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isBlank(ipAddr) || "unknown".equalsIgnoreCase(ipAddr)) { ipAddr = request.getRemoteAddr(); } return ipAddr; } } ``` 3. 在需要限制访问次数接口上添加 `@AccessLimit` 注解。 ```java @RestController public class DemoController { @GetMapping("/demo") @AccessLimit public String demo() { return "Hello World!"; } } ``` 这样,每个IP地址在60秒内最多只能访问 `/demo` 接口5次。 注意:以上代码仅供参考,实际应用中还需要考虑并发访问、线程安全等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值