zoukankan      html  css  js  c++  java
  • Springboot基于Guava+自定义注解实现IP或自定义key限流 升级版

    Springboot基于Guava+自定义注解实现IP或自定义key限流 升级版

    2020年5月17日 凌晨 有人恶意刷接口,刚喝完酒回来 大晚上的给我搞事情。。。。

    之前版本Springboot基于Guava+自定义注解实现限流功能是对访问这个接口所有人总的QPS限制,如果我们想对某一个用户或Ip地址访问接口的QPS限制,限制恶意请求接口的人而不影响正常的用户请求访问。

    实现步骤

    1、添加POM依赖
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!--AOP相关-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- guava -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>29.0-jre</version>
    </dependency>
    
    2、定义注解
    package com.example.guavalimit.limit;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @Description 自定义限流注解
     * @Author jie.zhao
     * @Date 2020/5/17 11:49
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LxRateLimit {
    
        //资源名称
        String name() default "默认资源";
    
        //限制每秒访问次数,默认为3次
        double perSecond() default 3;
    
        /**
         * 限流Key类型
         * 自定义根据业务唯一码来限制需要在请求参数中添加 String limitKeyValue
         */
        LimitKeyTypeEnum limitKeyType() default LimitKeyTypeEnum.IPADDR;
    
    }
    
    3、定义切面
    package com.example.guavalimit.limit;
    
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import com.google.common.util.concurrent.RateLimiter;
    import lombok.extern.slf4j.Slf4j;
    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.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Description 基于Guava cache缓存存储实现限流切面
     * @Author jie.zhao
     * @Date 2020/5/17 11:51
     */
    @Slf4j
    @Aspect
    @Component
    public class LxRateLimitAspect {
    
        /**
         * 缓存
         * maximumSize 设置缓存个数
         * expireAfterWrite 写入后过期时间
         */
        private static LoadingCache<String, RateLimiter> limitCaches = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(1, TimeUnit.DAYS)
                .build(new CacheLoader<String, RateLimiter>() {
                    @Override
                    public RateLimiter load(String key) throws Exception {
                        double perSecond = LxRateLimitUtil.getCacheKeyPerSecond(key);
                        return RateLimiter.create(perSecond);
                    }
                });
    
        /**
         * 切点
         * 通过扫包切入 @Pointcut("execution(public * com.ycn.springcloud.*.*(..))")
         * 带有指定注解切入 @Pointcut("@annotation(com.ycn.springcloud.annotation.LxRateLimit)")
         */
        @Pointcut("@annotation(com.example.guavalimit.limit.LxRateLimit)")
        public void pointcut() {
        }
    
        @Around("pointcut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            log.info("限流拦截到了{}方法...", point.getSignature().getName());
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            if (method.isAnnotationPresent(LxRateLimit.class)) {
                String cacheKey = LxRateLimitUtil.generateCacheKey(method, request);
                RateLimiter limiter = limitCaches.get(cacheKey);
                if (!limiter.tryAcquire()) {
                    throw new LimitAccessException("【限流】这位小同志的手速太快了");
                }
            }
            return point.proceed();
        }
    }
    
    
    4、枚举
    package com.example.guavalimit.limit;
    
    /**
     * @Description 限流key类型枚举
     * @Author jie.zhao
     * @Date 2020/5/17 14:28
     */
    public enum LimitKeyTypeEnum {
    
        IPADDR("IPADDR", "根据Ip地址来限制"),
        CUSTOM("CUSTOM", "自定义根据业务唯一码来限制,需要在请求参数中添加 String limitKeyValue");
    
        private String keyType;
        private String desc;
    
        LimitKeyTypeEnum(String keyType, String desc) {
            this.keyType = keyType;
            this.desc = desc;
        }
    
        public String getKeyType() {
            return keyType;
        }
    
        public String getDesc() {
            return desc;
        }
    }
    
    
    5、工具类
    package com.example.guavalimit.limit;
    
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    
    /**
     * @Description 限流工具类
     * @Author jie.zhao
     * @Date 2020/5/17 15:37
     */
    public class LxRateLimitUtil {
    
    
        /**
         * 获取唯一key根据注解类型
         * <p>
         * 规则 资源名:业务key:perSecond
         *
         * @param method
         * @param request
         * @return
         */
        public static String generateCacheKey(Method method, HttpServletRequest request) {
            //获取方法上的注解
            LxRateLimit lxRateLimit = method.getAnnotation(LxRateLimit.class);
            StringBuffer cacheKey = new StringBuffer(lxRateLimit.name() + ":");
            switch (lxRateLimit.limitKeyType()) {
                case IPADDR:
                    cacheKey.append(getIpAddr(request) + ":");
                    break;
                case CUSTOM:
                    String limitKeyValue = request.getParameter("limitKeyValue");
                    if (StringUtils.isEmpty(limitKeyValue)) {
                        throw new LimitAccessException("【" + method.getName() + "】自定义业务Key缺少参数String limitKeyValue,或者参数为空");
                    }
                    cacheKey.append(limitKeyValue + ":");
                    break;
            }
            cacheKey.append(lxRateLimit.perSecond());
            return cacheKey.toString();
        }
    
        /**
         * 获取缓存key的限制每秒访问次数
         * <p>
         * 规则 资源名:业务key:perSecond
         *
         * @param cacheKey
         * @return
         */
        public static double getCacheKeyPerSecond(String cacheKey) {
            String perSecond = cacheKey.split(":")[2];
            return Double.parseDouble(perSecond);
        }
    
        /**
         * 获取客户端IP地址
         *
         * @param request 请求
         * @return
         */
        public static String getIpAddr(HttpServletRequest request) {
            String ip = request.getHeader("x-forwarded-for");
            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.getRemoteAddr();
                if ("127.0.0.1".equals(ip)) {
                    //根据网卡取本机配置的IP
                    InetAddress inet = null;
                    try {
                        inet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        e.printStackTrace();
                    }
                    ip = inet.getHostAddress();
                }
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ip != null && ip.length() > 15) {
                if (ip.indexOf(",") > 0) {
                    ip = ip.substring(0, ip.indexOf(","));
                }
            }
            if ("0:0:0:0:0:0:0:1".equals(ip)) {
                ip = "127.0.0.1";
            }
            return ip;
        }
    }
    
    
    6、自定义异常
    package com.example.guavalimit.limit;
    
    /**
     * @Description 限流自定义异常
     * @Author jie.zhao
     * @Date 2019/8/7 16:01
     */
    public class LimitAccessException extends RuntimeException {
    
        private static final long serialVersionUID = -3608667856397125671L;
    
        public LimitAccessException(String message) {
            super(message);
        }
    }
    
    
    7、测试controller
    package com.example.guavalimit.controller;
    
    import com.example.guavalimit.limit.LimitKeyTypeEnum;
    import com.example.guavalimit.limit.LxRateLimit;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
    
        @GetMapping("/test1")
        @LxRateLimit(perSecond = 1, limitKeyType = LimitKeyTypeEnum.IPADDR)
        public String test1() {
            return "SUCCESS";
        }
    
        @GetMapping("/test2")
        @LxRateLimit(perSecond = 1, limitKeyType = LimitKeyTypeEnum.CUSTOM)
        public String test2(String limitKeyValue) {
            return "SUCCESS";
        }
    }
    
    
  • 相关阅读:
    个人永久性免费-Excel催化剂功能第31波-数量金额分组凑数功能,财务表哥表姐最爱
    个人永久性免费-Excel催化剂功能第30波-工作表快捷操作(批量创建、命名、排序、工作表目录)
    个人永久性免费-Excel催化剂功能第29波-追加中国特色的中文相关自定义函数
    发现用System.Net.Mail发邮件(代码附后),附件稍微大一点就会造成程序假死. 有没有什么简单的解决办法呢? 多谢!!
    上传文件 获取文件名 --360浏览器
    js中的json对象和字符串之间的转化
    chosen.jquery.js
    select 后台获取text 并根据text 设置value选中项
    iframe中有ajax,设置iframe自适应高度
    charme浏览器 jquery1.9.1min.js 报脚本错误 无jquery.min.map 文件
  • 原文地址:https://www.cnblogs.com/cnsyear/p/12905074.html
Copyright © 2011-2022 走看看