Springboot 接口限流Reids,限制ip防止暴力登录Aop切面

高并发下漏洞桶限流设计方案 - Redis

背景

在我们做社区的时候,经常会出现发水帖的同学。对于这种恶意刷帖的,我们的运营同学很是头疼,而且这种还不能在网关进行ip之类的过滤,只能基于单个单个用户进行处理,我们经常策略就是:每分钟发帖次数不能超过2个,超过后就关小黑屋10分钟。

出现场景:

  1. 上面讲的发帖的防刷机制。
  2. 广告流量的防刷。
  3. 接口请求失败进行熔断机制处理。
  4. ......

解决方案

对于这种“黑恶”请求,我们必须要做到是关小黑屋,当然有的系统架构比较大的,在网关层面就已经进行关了,我们这里是会在业务层来做,因为咱业务不是很大,当然同学们也可以把这个移植到网关层,这样不用穿透到我们业务侧,最少能够减少我们机房内部网络流量。

流程说明

  1. 接口发起请求,服务端获取这个接口用户唯一标识(用户id,电话号码...).
  2. 判断该用户是否被锁住,如果锁住就直接返回错误码。
  3. 未锁住就将该请求标记,亦或者叠加(叠加有坑,往下面看)。
  4. 进行计算当前用户在一定时间内是否超过我们设置的阈值。如果未超过直接返回。
  5. 如果超过,那么就进行锁定,再返回,下次请求的时候再进行判断。

具体方案

以我们场景为例子,使用redis和切面来做分布式锁和原子计数器,时间内叠加,判断叠加值是否超过阈值。

这个方案,在很多人设计的时候,都会考虑,看起来也没有太大问题,主要流程是:

一、pom文件引入aop切面,redis

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

二、创建RateLimiter,RateLimiterAspect配置文件,工具类

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;
}
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.stereotype.Component;

import java.lang.reflect.Method;

@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();
    }
}

三、创建ip获取工具类,和限流方法枚举

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;
    }
}
import io.swagger.annotations.ApiModel;
import lombok.Getter;

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

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

    /** ip限流 */
    IP,

    /**  用户id限流 */
    USER
}

四、接口注解限流

@RateLimiter(time = 60,count = 20 , limitType = LimitType.IP) //代表一分钟限制访问20次同一ip
  @PostMapping("materialLogin")
    @ApiOperation("登录")
    @RateLimiter(time = 60,count = 20 , limitType = LimitType.IP)
    public HttpResult<LoginVO> materialLogin(@Validated @RequestBody BaseLoginDto dto) {
      
        return HttpResult.ok();
    }

总结

  1. 在开始的时候,我一直在想第一个方案的问题所在,后来在讨论方案时候,总是发现时间移动,数值应该是会更改,可在第一个方案内,我们的请求量是不会更改,我们时间段已经固化成数值了。
  2. 整体的方案设计我们使用到的Redis的有序集合来做,当然有更好的方案欢迎大家来推荐哈,这个对于redis的读写压力很大的,但是作为临时的数据存储,这个场景还是比较符合。
  3. 我们redis的所有操作建议使用原子化来进行,这个可以使用官方提供的lua脚本来将多个语句合并成一个语句,并且lua执行速率也是很高。
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值