有個死鬼一直刷咱們接口,用`手機號+驗證碼`在那亂撞!—— 小傅哥技術分享

作者:小傅哥

博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!😄

本文的宗旨在於通過對實際場景的案例進行抽復現,教會讀者如何對應用的接口以瀏覽器指紋ID爲維度的限流操作,同時對於頻繁限流攔截的ID加入黑名單,不需要限流計算就🈲禁止對應用接口訪問。通過這樣的方式來保護應用的可用性。

本文涉及的工程:

一、場景說明

關於登錄的安全性管理有較多的手段,包括;設備信息、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>

有了上面這個方案,我們至少可以做一些安全的管控了。但還有臭不要臉的,一直刷你接口。這既有安全風險,又有對服務器的壓力。所以我們要考慮對於這樣的惡意用戶進行限流和自動化黑名單處理。

瀏覽器指紋的方案只需要做一個驗證碼綁定即可,之後限流和自動化黑名單,則需要做一些代碼的開發。通過配置的方式爲每一個需要做此類功能的接口添加上服務治理通常我們把對應用的熔斷、降級、限流、切量、黑白名單、人羣等,都稱爲服務治理

三、功能實現

1. 工程結構

  • 工程中,提供了一個 AOP 切面專門用於處理使用了自定義註解 AccessInterceptor 接口方法。
  • 這裏的自定義註解,在 DDD 分層架構中,要放到 Types 層中,這樣其他層才能引入使用。

2. 限流攔截

2.1 切面定義

@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 切面攔截

// 個人限頻記錄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 回調處理

/**
 * 調用用戶配置的回調方法,當攔截後,返回回調結果。
 */
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. 接口配置

@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

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,一個用戶維護限流訪問。讓自己的應用更加可靠!

這些各項實際場景的內容,在小傅哥的博客有7個完結的項目和1個進行的項目,都有大量的實踐運用。可以掃碼加入,項目體驗地址;https://gaga.plus

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章