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次时,就显示请求过于频繁,请稍后再试。控制台打印结果:

posted @ 2022-06-08 19:04  钟小嘿  阅读(4838)  评论(1编辑  收藏  举报