• SpringBoot对接口限制IP访问次数


    对于某些特定的接口,为了防止数据碰撞等问题,可限制接口对同一IP在一段时间内的访问次数。本文使用注解方式:

    1.导入需要的依赖

            <!-- redis依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!-- AOP依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <!-- Map依赖 -->
            <dependency>
                <groupId>net.jodah</groupId>
                <artifactId>expiringmap</artifactId>
                <version>0.5.8</version>
            </dependency>

    2.定义注解

    定义注解InterfaceLimit,用于接口拦截

    package com.zxh.example.anno;
    
    import java.lang.annotation.*;
    
    /**
     * 接口访问频率注解,默认一分钟只能访问5次
     */
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface InterfaceLimit {
        long time() default 60000; // 限制时间 单位:毫秒(默认值:一分钟)
    
        int value() default 5; // 允许请求的次数(默认值:5次)
    }

    3.在切面做限制

    在切面中对接口进行限制。

    1)服务为单节点

    package com.zxh.example.anno;
    
    import lombok.extern.slf4j.Slf4j;
    import net.jodah.expiringmap.ExpirationPolicy;
    import net.jodah.expiringmap.ExpiringMap;
    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.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestAttributes;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.TimeUnit;
    
    @Aspect
    @Component
    @Slf4j
    public class InterfaceLimitAspect {
        private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        /**
         * 层切点
         */
        @Pointcut("@annotation(interfaceLimit)")
        public void controllerAspect(InterfaceLimit interfaceLimit) {
        }
    
        @Around("controllerAspect(interfaceLimit)")
        public Object doAround(ProceedingJoinPoint pjp, InterfaceLimit interfaceLimit) throws Throwable {
            // 获得request对象
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();
    
            // 获取Map value对象, 如果没有则返回默认值
            // getOrDefault获取参数,获取不到则给默认值
            ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
            Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0);
            if (uCount >= interfaceLimit.value()) { // 超过次数,不执行目标方法
                log.error("接口拦截:{} 请求超过限制频率【{}次/{}ms】,IP为{}", request.getRequestURI(), interfaceLimit.value(), interfaceLimit.time(), request.getRemoteAddr());
                return "请求过于频繁,请稍后再试";
            } else if (uCount == 0) { // 第一次请求时,设置有效时间
                uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, interfaceLimit.time(), TimeUnit.MILLISECONDS);
            } else { // 未超过次数, 记录加一
                uc.put(request.getRemoteAddr(), uCount + 1);
            }
            book.put(request.getRequestURI(), uc);
    
            // result的值就是被拦截方法的返回值
            Object result = pjp.proceed();
    
            return result;
        }
    
    }

    使用ConcurrentHashMap和ExpiringMap来对请求路径和ip进行限制。其中ConcurrentHashMap可以处理并发情况的 HashMap,ExpiringMap为单个元素设置过期时间。

    2)服务为多节点(推荐)

    由于ConcurrentHashMap是基于线程的,当服务为多节点时,只能在当前服务有效,那么就会造成实际接口的限制大于规定的限制。

    这时可借助redis进行限制。验证的方法修改如下:

        @Around("controllerAspect(interfaceLimit)")
        public Object doAround(ProceedingJoinPoint pjp, InterfaceLimit interfaceLimit) throws Throwable {
            // 获得request对象
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();
            //redis这里推荐使用hash类型,url为外层key,ip作为内层key,访问次数作为value
            BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps("test:interfaceLimit:" + request.getRequestURI());
            String ipCnt = (String) ops.get(request.getRemoteAddr());
            Integer uCount = ipCnt == null ? 0 : "".equals(ipCnt) ? 0 : Integer.parseInt(ipCnt);
            if (uCount >= interfaceLimit.value()) { // 超过次数,不执行目标方法
                log.error("接口拦截:{} 请求超过限制频率【{}次/{}ms】,IP为{}", request.getRequestURI(), interfaceLimit.value(), interfaceLimit.time(), request.getRemoteAddr());
                return "请求过于频繁,请稍后再试";
            } else {
                //请求时,设置有效时间, 记录加一
                ops.increment(request.getRemoteAddr(), 1);
                ops.expire(interfaceLimit.time(), TimeUnit.MILLISECONDS);
            }
            // result的值就是被拦截方法的返回值
            Object result = pjp.proceed();
    
            return result;
        }

    4.在接口中使用注解

    package com.zxh.example.controller;
    
    import com.zxh.example.anno.InterfaceLimit;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/api")
    @Slf4j
    public class TestController {
    
        @InterfaceLimit
        @GetMapping("/test")
        public String test() {
            return "123";
        }
    
        @InterfaceLimit(value = 10)
        @GetMapping("/test2")
        public String test2() {
            return "1234";
        }
    
    }

    可使用默认的访问频率,也可自行传值。

    本地启动后在1分钟内连续访问/api/test接口(/api/test2同),当未超过5次时,访问正常,超过5次时,就显示请求过于频繁,请稍后再试。控制台打印结果:

  • 相关阅读:
    Hadoop入门
    Redis缓存分布式高可用架构原理
    ES分布式搜索引擎架构原理
    Java锁,多线程与高并发,JUC并发包
    图算法--拓扑序列
    数据结构--数组模拟队列
    数据结构--数组模拟栈
    数据结构--数组模拟双链表
    数据结构--数组模拟单链表
    基础算法--双指针
  • 原文地址:https://www.cnblogs.com/zys2019/p/16328053.html
Copyright © 2020-2023  润新知