RateLimiter 限流 —— 通过切面对单个用户进行限流和黑名单处理

简介: RateLimiter 限流 —— 通过切面对单个用户进行限流和黑名单处理

关于登录的安全性管理有较多的手段,包括;设备信息、IP信息、绑定的信息、验证码登各类方式。不过在一些网页版的登录中,如果有人想办法把你的验证码给我,我就可以登录你的账户,查看你的数据。对于一些不法分子通过让你进入某些应用的录屏会议后(XXX退货返现),就能拿到你的验证码,并做登录操作。还有一些是完全流氓式做法,就玩命的一些快递📦手机号+验证码频繁的撞接口,也是有概率成功登录的。因此,为了避免这种情况,我们还需要思考如何防范。

我们可以考虑在登录的阶段必须加一些恶心的图片比对码,或者滑块验证码。这也是一种方式,能尽可能降低登录的撞接口操作。之后再考虑添加一个指纹ID,对于验证码的生成与用户从浏览器设备过来的指纹做绑定。这样即使对方通过录屏拿到你的验证码,也仍然没有做登录操作。

<script>
  // Initialize the agent at application startup.
  const fpPromise = import('https://openfpcdn.io/fingerprintjs/v4')
    .then(FingerprintJS => FingerprintJS.load())
 
  // Get the visitor identifier when you need it.
  fpPromise
    .then(fp => fp.get())
    .then(result => {
      // This is the visitor identifier:
      const visitorId = result.visitorId
      console.log(visitorId)
    })
</script>

有了上面这个方案,我们至少可以做一些安全的管控了。但还有臭不要脸的,一直刷你接口。这既有安全风险,又有对服务器的压力。所以我们要考虑对于这样的恶意用户进行限流和自动化黑名单处理。

浏览器指纹的方案只需要做一个验证码绑定即可,之后限流和自动化黑名单,则需要做一些代码的开发。通过配置的方式为每一个需要做此类功能的接口添加上服务治理。通常我们把对应用的熔断、降级、限流、切量、黑白名单、人群等,都称为服务治理

工程结构

限流拦截

切面定义

public @interface AccessInterceptor {
 
    /** 用哪个字段作为拦截标识,未配置则默认走全部 */
    String key() default "all";
 
    /** 限制频次(每秒请求次数) */
    double permitsPerSecond();
 
    /** 黑名单拦截(多少次限制后加入黑名单)0 不限制 */
    double blacklistCount() default 0;
 
    /** 黑名单持续时间(秒) */
    long blacklistDurationSeconds() default 24 * 3600; // 默认为24小时
 
    /** 拦截后的执行方法 */
    String fallbackMethod();
 
 
}

  • 自定义切面注解,提供了拦截的key、限制频次、黑名单处理、黑名单持续时间、拦截后的回调方法。再通过 @Pointcut 切入配置了自定义注解的接口方法

切面拦截

@Slf4j
@Aspect
public class RateLimiterAOP {
 
    @Resource
    private RedissonClient redissonClient;
 
 
    @Pointcut("@annotation(cn.bugstack.chatgpt.data.types.annotation.AccessInterceptor)")
    public void aopPoint() {
    }
 
 
    @Around("aopPoint() && @annotation(accessInterceptor)")
    public Object doRouter(ProceedingJoinPoint jp, AccessInterceptor accessInterceptor) throws Throwable {
        String key = accessInterceptor.key();
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("RateLimiter key is null or empty!");
        }
 
        // 获取拦截字段
        String keyAttr = getAttrValue(key, jp.getArgs());
        log.info("aop attr {}", keyAttr);
 
        // 黑名单拦截
        if (!"all".equals(keyAttr) && accessInterceptor.blacklistCount() != 0 && isBlacklisted(keyAttr, accessInterceptor)) {
            log.info("限流-黑名单拦截(24h):{}", keyAttr);
            return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
        }
 
        // 速率限制
        if (!isRateLimited(keyAttr, accessInterceptor)) {
            log.info("限流-超频次拦截:{}", keyAttr);
            return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
        }
 
        // 返回结果
        return jp.proceed();
    }
 
    /**
     * 黑名单
     * @param keyAttr
     * @param accessInterceptor
     * @return
     */
    private boolean isBlacklisted(String keyAttr, AccessInterceptor accessInterceptor) {
        String blacklistKey = "blacklist:" + keyAttr;
        long count = redissonClient.getAtomicLong(blacklistKey).incrementAndGet();
        redissonClient.getAtomicLong(blacklistKey).expire(accessInterceptor.blacklistDurationSeconds(), TimeUnit.SECONDS);
        return count > accessInterceptor.blacklistCount();
    }
 
    /**
     * 限流
     * @param keyAttr
     * @param accessInterceptor
     * @return
     */
    private boolean isRateLimited(String keyAttr, AccessInterceptor accessInterceptor) {
        String rateLimitKey = "ratelimit:" + keyAttr;
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(rateLimitKey);
        // 设置速率
        rateLimiter.trySetRate(RateType.OVERALL, (long) accessInterceptor.permitsPerSecond(), 1, RateIntervalUnit.SECONDS);
        // 尝试获取许可
        return rateLimiter.tryAcquire();
    }
 
 
    /**
     * 调用用户配置的回调方法,当拦截后,返回回调结果。
     */
 
    private Object fallbackMethodResult(ProceedingJoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) jp.getSignature();
        Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
        return method.invoke(jp.getThis(), jp.getArgs());
    }
 
 
    /**
     * 实际根据自身业务调整,主要是为了获取通过某个值做拦截
     */
    private String getAttrValue(String attr, Object[] args) {
        if (args[0] instanceof String) {
            return (String) args[0];
        }
        String fieldValue = null;
        for (Object arg : args) {
            try {
                if (fieldValue != null) {
                    break;
                }
                fieldValue = String.valueOf(getValueByName(arg, attr));
            } catch (Exception e) {
                log.error("获取属性值失败 attr:{}", attr, e);
            }
        }
        return fieldValue;
    }
 
 
    /**
     * 获取对象的特定属性值
     *
     * @param item 对象
     * @param name 属性名
     * @return 属性值
     * @author tang
     */
 
    private Object getValueByName(Object item, String name) {
        try {
            Field field = getFieldByName(item, name);
            if (field == null) {
                return null;
            }
            field.setAccessible(true);
            Object value = field.get(item);
            field.setAccessible(false);
            return value;
        } catch (IllegalAccessException e) {
            return null;
        }
    }
 
    /**
     * 根据名称获取方法,该方法同时兼顾继承类获取父类的属性
     *
     * @param item 对象
     * @param name 属性名
     * @return 该属性对应方法
     * @author tang
     */
 
    private Field getFieldByName(Object item, String name) {
        try {
            Field field;
            try {
                field = item.getClass().getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                field = item.getClass().getSuperclass().getDeclaredField(name);
            }
            return field;
        } catch (NoSuchFieldException e) {
            return null;
        }
    }
 
 
}
  • 通过自定义注解中配置的拦截字段,获取对应的值。这里的值作为用户的标识使用,只对这个用户进行拦截。【也可以是一些列的信息确认,包括用户IP、设备等。】
  • 这段代码流程中会根据自定义注解中的配置,对访问的用户进行限流拦截,当拦击次数达到加入黑名单的次数后,则直接存起来(Redis)在24h内直接走黑名单。—— 实际的场景中还会有风控的手段介入,以及人工来操作黑名单。

 

@Configuration
public class RateLimiterAOPConfig {
 
    @Bean
    public RateLimiterAOP rateLimiter(){
        return new RateLimiterAOP();
    }
 
}

最后在需要拦截的方法上添加自定义注解即可

  • key: 以用户ID作为拦截,这个用户访问次数限制
  • fallbackMethod:失败后的回调方法,方法出入参保持一样
  • permitsPerSecond:每秒的访问频次限制。1秒1次
  • blacklistCount:超过10次都被限制了,还访问的,扔到黑名单里24小时


目录
相关文章
|
监控 算法
探秘Guava的RateLimiter:单机流量控制的黄金法宝
探秘Guava的RateLimiter:单机流量控制的黄金法宝
870 0
|
开发工具
成功解决 zsh: command not found
成功解决 zsh: command not found
3862 0
|
8月前
|
存储 人工智能 安全
2026年OpenClaw部署使用从入门到精通:3步部署+3大必装Skill+几个实战案例
2026年初,一只名为Molty的小龙虾图标席卷全球科技圈——OpenClaw(曾用名Clawd、Moltbot)以72小时狂揽6万+GitHub Stars的速度引爆市场,如今星标数已突破18万,成为现象级AI工具。它颠覆了传统AI助手“只聊天不干活”的局限,能通过Telegram、飞书等10+平台远程操控设备,完成网站重建、文件管理、代码开发等复杂任务,被用户誉为“住在手机里的全能管家”。
4639 2
|
负载均衡 容灾 应用服务中间件
使用Docker-compose搭建nginx-keepalived双机热备来实现高可用nginx集群
最近同学出去面试经常会被问到一个问题。 面试官:你说你们公司使用nginx反向代理tornado,部署了多少多少台机器,好像很牛逼的样子,但是我问你,如果主机也就是部署了nginx那台机器并发过大导致宕机了怎么办? 答曰:不考虑带宽峰值的话,比较新的 CPU 跑 nginx 单核每秒能接近 2 万请求,而且nginx如果纯做 HTTP 转发的话,Nginx 的性能高到恐怖,会挂掉很不科学。。。。 面试官:好吧,算你会忽悠,那如果插头被扫地大妈踢掉了怎么办,因为没电导致服务器宕机 答曰:阿里云机房会断电。。。你特么在逗我吧。。
使用Docker-compose搭建nginx-keepalived双机热备来实现高可用nginx集群
|
Ubuntu Linux 网络安全
如何在Linux中更改主机名?修改主机名最新方法
本期教程将指导您如何在Linux系统中更改主机名。主机名是Linux系统的常用功能,用于识别服务器,帮助区分不同服务器,并与网络进程和其他应用程序协同工作。教程涵盖显示当前主机名的方法(通过`hostname`命令),以及在CentOS 7、Debian 9和Ubuntu 16.04及以上版本中更改主机名的步骤(使用`hostnamectl set-hostname`命令)。对于其他Linux版本,可编辑`/etc/hostname`文件实现更改。记得重启相关服务或服务器以使更改生效!
3295 12
|
设计模式 缓存 Java
重学Java基础篇—Java对象创建的7种核心方式详解
本文全面解析了Java中对象的创建方式,涵盖基础到高级技术。包括`new关键字`直接实例化、反射机制动态创建、克隆与反序列化复用对象,以及工厂方法和建造者模式等设计模式的应用。同时探讨了Spring IOC容器等框架级创建方式,并对比各类方法的适用场景与优缺点。此外,还深入分析了动态代理、Unsafe类等扩展知识及注意事项。最后总结最佳实践,建议根据业务需求选择合适方式,在灵活性与性能间取得平衡。
957 3
|
并行计算 调度
多线程的并发和并行
多线程的并发和并行
|
运维 监控 安全
堡垒机是用来干什么的?堡垒机的好处有什么?
堡垒机能保障网络和数据不受来自外部和内部用户的入侵和破坏,运用各种技术手段实时收集和监控网络环境。
2642 1
|
Ubuntu Linux Shell
10-11|linux如何修改时区
10-11|linux如何修改时区
|
消息中间件 前端开发 Java
java高并发场景RabbitMQ的使用
java高并发场景RabbitMQ的使用
606 0