springboot接口限流,防连点攻击

概要

通过aop切面+redis实现接口限流,ip限制,防重复提交

整体架构流程

提示:通过aop切面+redis实现接口限流,ip限制,防重复提交

接口 文件

package com.bzfar.config;


import com.bzfar.enums.LimitType;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter{

    /**
     * 限流key
     */
    String key() default "rate_limit:";

    /**
     * 限流时间,单位秒
     */
    int time() default 60;

    /**
     * 限流次数
     */
    int count() default 100;

    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
}
package com.bzfar.config;



import com.aspose.words.net.System.Data.DataException;
import com.bzfar.HeadContext;
import com.bzfar.enums.LimitType;
import com.bzfar.util.RedisUtil;
import com.bzfar.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

@Aspect
@Component
@Slf4j
public class RateLimiterAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisUtil redisUtil;


    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = rateLimiter.key();
        Long time = new Long(rateLimiter.time());
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        String keyCode = key + combineKey.hashCode();
        int number = 1;
        if(redisUtil.hasKey(keyCode)){
            number = (Integer)redisUtil.get(keyCode);
            ++number;
        }
        redisUtil.set(keyCode , number , time);
        if(number > count){
            throw new DataException("访问过于频繁,请稍候再试");
        }
    }

    @After("@annotation(rateLimiter)")
    public void doAfter(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String combineKey = getCombineKey(rateLimiter, point);
        redisTemplate.delete(combineKey);
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtil.getIp()).append("-");
        }
        if(rateLimiter.limitType() == LimitType.USER){
            stringBuffer.append(HeadContext.getToken()).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}

type文件

package com.bzfar.enums;

import io.swagger.annotations.ApiModel;
import lombok.Getter;

@ApiModel("限流类型")
@Getter
public enum LimitType {

    /** 默认策略全局限流 */
    DEFAULT,

    /** ip限流 */
    IP,

    /**  用户id限流 */
    USER
}
package com.bzfar.submit;

import java.lang.annotation.*;

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{

}
package com.bzfar.submit;

import com.bzfar.util.AssertUtil;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null)
            {
                if (this.isRepeatSubmit(request))
                {
                    AssertUtil.assertNull("", "不允许重复提交,请稍后再试");
                    return false;
                }
            }
            return true;
        }
        else
        {
            return super.preHandle(request, response, handler);
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request) throws Exception;
}

ip工具类

package com.bzfar.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Objects;

@Slf4j
public class IpUtil {

    // 多次反向代理后会有多个ip值 的分割符
    private static final String IP_UTILS_FLAG = ",";
    // 未知IP
    private static final String UNKNOWN = "unknown";
    // 本地 IP
    private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";
    private static final String LOCALHOST_IP1 = "127.0.0.1";

    public static String getIp(){
        // 根据 HttpHeaders 获取 请求 IP地址
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("x-forwarded-for");
            if (ip != null && ip.length() != 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
                // 多次反向代理后会有多个ip值,第一个ip才是真实ip
                if (ip.contains(IP_UTILS_FLAG)) {
                    ip = ip.split(IP_UTILS_FLAG)[0];
                }
            }
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        //兼容k8s集群获取ip
        if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = Objects.requireNonNull(request.getRemoteAddr());
            if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
                //根据网卡取本机配置的IP
                InetAddress iNet = null;
                try {
                    iNet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    log.error("getClientIp error: ", e);
                }
                assert iNet != null;
                ip = iNet.getHostAddress();
            }
        }
        return ip;
    }
}

controller层注解

    @PostMapping("addOrUpdateUser")
    @ApiOperation("新增或修改用户信息及人脸")
    @RateLimiter(time = 2,count = 1 , limitType = LimitType.IP)
    public HttpResult addOrUpdateUserFace(@Validated @RequestBody AddUserDto dto){
  
        return HttpResult.ok( userService.addUserFace(dto));

    }

小结

提示:

@RateLimiter(time = 2,count = 1 , limitType = LimitType.IP) 注解开启会根据固定的ip没2秒允许访问 1次
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 中,我们可以使用 AOP(面向切面编程)和拦截器的方式来实现接口限流。以下是一个简单的实现方式: 1. 引入 Guava 库,它包含了令牌桶算法和漏斗算法的实现。 2. 定义一个注解 `@RateLimit`,用来标记需要进行限流接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { int value() default 100; // 默认每秒限制100个请求 } ``` 3. 定义一个切面 `RateLimitAspect`,在接口被调用时进行限流检查。 ```java @Aspect @Component public class RateLimitAspect { private final RateLimiter rateLimiter = RateLimiter.create(100.0); // 每秒100个请求 @Around("@annotation(rateLimit)") public Object limit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { if (rateLimiter.tryAcquire(rateLimit.value(), TimeUnit.MILLISECONDS)) { return joinPoint.proceed(); } else { throw new RuntimeException("接口限流,请稍后再试!"); } } } ``` 4. 在接口方法上添加 `@RateLimit` 注解,指定每秒允许的请求个数。 ```java @RestController public class DemoController { @GetMapping("/demo") @RateLimit(10) // 每秒限制10个请求 public String demo() { return "Hello World!"; } } ``` 以上代码实现了一个简单的接口限流功能,可以根据实际需求进行调整。需要注意的是,这种方式只适用于单机部署的场景,如果是分布式部署的系统,需要考虑使用分布式限流方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值