前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >有个死鬼一直刷咱们接口,用`手机号+验证码`在那乱撞!

有个死鬼一直刷咱们接口,用`手机号+验证码`在那乱撞!

作者头像
小傅哥
发布2023-12-13 08:38:11
1830
发布2023-12-13 08:38:11
举报

持续坚持原创输出,点击蓝字关注我吧

作者:小傅哥 博客:https://bugstack.cn

❝沉淀、分享、成长,让自己和他人都能有所收获!😜 ❞

本文的宗旨在于通过对实际场景的案例进行抽复现,教会读者如何对应用的接口以浏览器指纹ID为维度的限流操作,同时对于频繁限流拦截的ID加入黑名单,不需要限流计算就禁止对应用接口访问。通过这样的方式来保护应用的可用性。

本文涉及的工程:

  • xfg-dev-tech-ratelimiter:https://gitcode.net/KnowledgePlanet/road-map/xfg-dev-tech-ratelimiter

一、场景说明

关于登录的安全性管理有较多的手段,包括;设备信息、IP信息、绑定的信息、验证码登各类方式,不过在一些网页版的登录中,手机验证码方式都会有一个对应的提醒:"请勿向他人泄露验证码信息"

也就是说,如果你把你的验证码给我,我就可以登录你的账户,查看你的数据。对于一些不法分子通过让你进入某些应用的录屏会议后(XXX退货返现),就能拿到你的验证码,并做登录操作。还有一些是完全流氓式做法,就玩命的一些快递📦手机号+验证码频繁的撞接口,也是有概率成功登录的。

所以,本节的案例我们来考虑下该如何做这样的防护处理。

二、方案设计

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

代码语言:javascript
复制
<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>

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

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

三、功能实现

1. 工程结构

  • 工程中,提供了一个 AOP 切面专门用于处理使用了自定义注解 AccessInterceptor 接口方法。
  • 这里的自定义注解,在 DDD 分层架构中,要放到 Types 层中,这样其他层才能引入使用。

2. 限流拦截

2.1 切面定义
代码语言:javascript
复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AccessInterceptor {

    /** 用哪个字段作为拦截标识,未配置则默认走全部 */
    String key() default "all";

    /** 限制频次(每秒请求次数) */
    double permitsPerSecond();

    /** 黑名单拦截(多少次限制后加入黑名单)0 不限制 */
    double blacklistCount() default 0;

    /** 拦截后的执行方法 */
    String fallbackMethod();

}

@Pointcut("@annotation(cn.bugstack.xfg.dev.tech.annotation.AccessInterceptor)")
public void aopPoint() {
}
  • 自定义切面注解,提供了拦截的key、限制频次、黑名单处理、拦截后的回调方法。再通过 @Pointcut 切入配置了自定义注解的接口方法

2.2 切面拦截

代码语言:javascript
复制
// 个人限频记录1分钟
private final Cache<String, RateLimiter> loginRecord = CacheBuilder.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .build();

// 个人限频黑名单24h - 自身的分布式业务场景,可以记录到 Redis 中
private final Cache<String, Long> blacklist = CacheBuilder.newBuilder()
        .expireAfterWrite(24, TimeUnit.HOURS)
        .build();

@Around("aopPoint() && @annotation(accessInterceptor)")
public Object doRouter(ProceedingJoinPoint jp, AccessInterceptor accessInterceptor) throws Throwable {
    String key = accessInterceptor.key();
    if (StringUtils.isBlank(key)) {
        throw new RuntimeException("annotation RateLimiter uId is null!");
    }
    
    // 获取拦截字段
    String keyAttr = getAttrValue(key, jp.getArgs());
    log.info("aop attr {}", keyAttr);
   
    // 黑名单拦截
    if (!"all".equals(keyAttr) && accessInterceptor.blacklistCount() != 0 && null != blacklist.getIfPresent(keyAttr) && blacklist.getIfPresent(keyAttr) > accessInterceptor.blacklistCount()) {
        log.info("限流-黑名单拦截(24h):{}", keyAttr);
        return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
    }
   
    // 获取限流 -> Guava 缓存1分钟
    RateLimiter rateLimiter = loginRecord.getIfPresent(keyAttr);
    if (null == rateLimiter) {
        rateLimiter = RateLimiter.create(accessInterceptor.permitsPerSecond());
        loginRecord.put(keyAttr, rateLimiter);
    }
    
    // 限流拦截
    if (!rateLimiter.tryAcquire()) {
        if (accessInterceptor.blacklistCount() != 0) {
            if (null == blacklist.getIfPresent(keyAttr)) {
                blacklist.put(keyAttr, 1L);
            } else {
                blacklist.put(keyAttr, blacklist.getIfPresent(keyAttr) + 1L);
            }
        }
        log.info("限流-超频次拦截:{}", keyAttr);
        return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());
    }
    // 返回结果
    return jp.proceed();
}
  • 通过自定义注解中配置的拦截字段,获取对应的值。这里的值作为用户的标识使用,只对这个用户进行拦截。【也可以是一些列的信息确认,包括用户IP、设备等。】
  • 这段代码流程中会根据自定义注解中的配置,对访问的用户进行限流拦截,当拦击次数达到加入黑名单的次数后,则直接存起来(Guava/Redis)在24h内直接走黑名单。—— 实际的场景中还会有风控的手段介入,以及人工来操作黑名单。
2.3 回调处理
代码语言:javascript
复制
/**
 * 调用用户配置的回调方法,当拦截后,返回回调结果。
 */
private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Signature sig = jp.getSignature();
    MethodSignature methodSignature = (MethodSignature) sig;
    Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
    return method.invoke(jp.getThis(), jp.getArgs());
}
  • 最终如果判定为拦截,则会走用户配置的回调方法。如 login 配置一个 loginErr,出入参都一样,只是名字不一样。这样才方便反射调用。

四、测试验证

1. 接口配置
代码语言:javascript
复制
@AccessInterceptor(key = "fingerprint", fallbackMethod = "loginErr", permitsPerSecond = 1.0d, blacklistCount = 10)
@RequestMapping(value = "login", method = RequestMethod.GET)
public String login(String fingerprint, String uId, String token) {
    log.info("模拟登录 fingerprint:{}", fingerprint);
    return "模拟登录:登录成功 " + uId;
}

public String loginErr(String fingerprint, String uId, String token) {
    return "频次限制,请勿恶意访问!";
}

给你需要拦截的方法,添加上自定义注解。

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

访问:http://localhost:8091/api/ratelimiter/login?fingerprint=uljpplllll01009&uId=1000&token=8790

代码语言:javascript
复制
22:34:47.518 [http-nio-8091-exec-6] INFO  RateLimiterAOP - 限流-超频次拦截:uljpplllll01009
22:34:47.669 [http-nio-8091-exec-7] INFO  RateLimiterAOP - aop attr uljpplllll01009
22:34:49.121 [http-nio-8091-exec-6] INFO  RateLimiterAOP - aop attr uljpplllll01009
22:34:49.122 [http-nio-8091-exec-6] INFO  RateLimiterAOP - 限流-黑名单拦截(24h):uljpplllll01009
22:34:57.647 [http-nio-8091-exec-8] INFO  RateLimiterAOP - aop attr uljpplllll01009
22:34:57.650 [http-nio-8091-exec-8] INFO  RateLimiterAOP - 限流-黑名单拦截(24h):uljpplllll01009
  • 好啦,到这,我们就可以看到,用户的访问已经被拦截了。
  • 赶紧到自己的应用加一下吧,一个指纹ID,一个用户维护限流访问。让自己的应用更加可靠!
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 bugstack虫洞栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 持续坚持原创输出,点击蓝字关注我吧
  • 一、场景说明
  • 二、方案设计
  • 三、功能实现
    • 1. 工程结构
      • 2. 限流拦截
        • 2.1 切面定义
      • 2.2 切面拦截
        • 2.3 回调处理
      • 四、测试验证
        • 1. 接口配置
        • 2. 测试验证
    相关产品与服务
    验证码
    腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档