SpringBoot begrenzt die Häufigkeit des Schnittstellenzugriffs – diese Fehler dürfen nicht gemacht werden

Vor kurzem baue ich ein System für normale Benutzer auf Basis von SpringBoot. Um die Stabilität des Systems sicherzustellen und böswillige Angriffe zu verhindern, möchte ich die Häufigkeit des Benutzerzugriffs auf jede Schnittstelle steuern. Um diese Funktion zu realisieren, können Sie eine Annotation entwerfen und dann mit AOP die Zugriffshäufigkeit der aktuellen IP überprüfen, bevor Sie die Methode aufrufen. Wenn die Häufigkeit die eingestellte Häufigkeit überschreitet, wird direkt eine Fehlermeldung zurückgegeben.

häufige Designfehler

Bevor ich mit der Einführung der konkreten Implementierung beginne, werde ich einige häufige falsche Designs auflisten, die ich im Internet gefunden habe.

1. Festes Fenster

Jemand hat ein aktuelles Begrenzungsschema entworfen, das nur 1000 Besuche pro Minute zulässt, wie in der Abbildung unten gezeigt, nur 1000 Besuche sind zwischen 01:00 und 02:00 Uhr erlaubt. Das größte Problem bei diesem Entwurf besteht darin, dass die Anfrage zwischen 01 und 01 liegen kann :59s-02:00s 1.000 Anfragen wurden zwischen 02:00s und 1.000 Anfragen zwischen 02:00s und 02:01s gestellt. In diesem Fall wurden 2.000 Anfragen zwischen 01:59s und 02:01s mit einem Intervall von 0,02s gestellt Offensichtlich ist das Design falsch.

jm36ts

2. Fehler bei der Aktualisierung der Cache-Zeit

Als ich dieses Problem untersuchte, stellte ich fest, dass es eine sehr gängige Methode gibt, den Datenverkehr im Internet zu begrenzen. Die Idee basiert auf Redis. Jedes Mal, wenn eine Benutzeranfrage eingeht, werden die IP-Adresse und die Anforderungs-URL des Benutzers als Schlüssel verwendet um den Zugriff zu beurteilen. Ob die Anzahl der Male das Limit überschreitet, wenn ja, einen Fehler zurückgeben, andernfalls 1 zum Wert hinzufügen, der dem Schlüssel in Redis entspricht, und die Ablaufzeit des Schlüssels auf den durch den angegebenen Zugriffszeitraum zurücksetzen Benutzer. Der Kerncode lautet wie folgt:

// core logic
int limit = accessLimit.limit();
long sec = accessLimit.sec();
String key = IPUtils.getIpAddr(request) + request.getRequestURI();
Integer maxLimit =null;
Object value =redisService.get(key);
if(value!=null && !value.equals("")) {
    maxLimit = Integer.valueOf(String.valueOf(value));
}
if (maxLimit == null) {
    redisService.set(key, "1", sec);
} else if (maxLimit < limit) {
    Integer i = maxLimit+1;
    redisService.set(key, i.toString(), sec);
} else {
	throw new BusinessException(500,"请求太频繁!");
}

// redis related
    public boolean set(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

Das große Problem hierbei besteht darin, dass die Cache-Ablaufzeit des Schlüssels jedes Mal aktualisiert wird, was einer verdeckten Verlängerung jedes Zählzyklus gleichkommt. Vielleicht möchten wir den Benutzer so steuern, dass er nur 5 Mal in einer Minute besucht, aber wenn der Benutzer Wird in der vorherigen Minute nur dreimal besucht und in der nächsten Minute dreimal besucht. In der obigen Implementierung wird wahrscheinlich beim sechsten Besuch ein Fehler zurückgegeben, dies ist jedoch problematisch, da der Benutzer den entsprechenden Besuch nicht überschreitet innerhalb von zwei Minuten Frequenzschwelle.

Informationen zur Schlüsselaktualisierung finden Sie im offiziellen Redis-Dokument . Bei jeder Aktualisierung wird die Schlüsselablaufzeit aktualisiert.EEB8ry

Richtiges Design basierend auf Schiebefenster

指定时间T内,只允许发生N次。我们可以将这个指定时间T,看成一个滑动时间窗口(定宽)。我们采用Redis的zset基本数据类型的score来圈出这个滑动时间窗口。在实际操作zset的过程中,我们只需要保留在这个滑动时间窗口以内的数据,其他的数据不处理即可。

WKdTZ9

比如在上面的例子里面,假设用户的要求是60s内访问频率控制为3次。那么我永远只会统计当前时间往前倒数60s之内的访问次数,随着时间的推移,整个窗口会不断向前移动,窗口外的请求不会计算在内,保证了永远只统计当前60s内的request。

为什么选择Redis zset ?

为了统计固定时间区间内的访问频率,如果是单机程序,可能采用concurrentHashMap就够了,但是如果是分布式的程序,我们需要引入相应的分布式组件来进行计数统计,而Redis zset刚好能够满足我们的需求。

Redis zset(有序集合)中的成员是有序排列的,它和 set 集合的相同之处在于,集合中的每一个成员都是字符串类型,并且不允许重复;而它们最大区别是,有序集合是有序的,set 是无序的,这是因为有序集合中每个成员都会关联一个 double(双精度浮点数)类型的 score (分数值),Redis 正是通过 score 实现了对集合成员的排序。

Redis 使用以下命令创建一个有序集合:

ZADD key score member [score member ...]

这里面有三个重要参数,

  • key:指定一个键名;
  • score:分数值,用来描述  member,它是实现排序的关键;
  • member:要添加的成员(元素)。

当 key 不存在时,将会创建一个新的有序集合,并把分数/成员(score/member)添加到有序集合中;当 key 存在时,但 key 并非 zset 类型,此时就不能完成添加成员的操作,同时会返回一个错误提示。

在我们这个场景里面,key就是用户ip+request uri,score直接用当前时间的毫秒数表示,至于member不重要,可以也采用和score一样的数值即可。

限流过程是怎么样的?

整个流程如下:

  1. Zuerst geht die Anfrage des Benutzers ein, die IP und die URI des Benutzers bilden den Schlüssel, der Zeitstempel ist der Wert und wird in zset eingefügt
  2. Aktualisieren Sie die Cache-Ablaufzeit des aktuellen Schlüssels. Dieser Schritt dient hauptsächlich der regelmäßigen Bereinigung kalter Daten, was sich von dem oben erwähnten häufigen Fehlerdesign 2 unterscheidet.
  3. Datensätze außerhalb des Fensters löschen.
  4. Zählen Sie die Gesamtzahl der Datensätze im aktuellen Fenster.
  5. Wenn die Anzahl der Datensätze den Schwellenwert überschreitet, wird direkt ein Fehler zurückgegeben, andernfalls wird die Benutzeranforderung normal verarbeitet.

e0tcMj

Strombegrenzung basierend auf SpringBoot und AOP

In diesem Teil wird hauptsächlich die spezifische Implementierungslogik vorgestellt.

Definieren Sie Anmerkungen und Verarbeitungslogik

Die erste besteht darin, eine Anmerkung zu definieren, um die spätere Verwendung verschiedener Grenzfrequenzen für verschiedene Schnittstellen zu erleichtern.

/**  
 * 接口访问频率注解,默认一分钟只能访问5次  
 */  
@Documented  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface RequestLimit {  
  
    // 限制时间 单位:秒(默认值:一分钟)  
    long period() default 60;  
  
    // 允许请求的次数(默认值:5次)  
    long count() default 5;  
  
}

Bei der Implementierung der Logik definieren wir eine Aspektfunktion zum Abfangen von Benutzeranforderungen. Der spezifische Implementierungsprozess stimmt mit dem oben beschriebenen aktuellen Begrenzungsprozess überein und umfasst hauptsächlich den Betrieb von redis zset.


@Aspect
@Component
@Log4j2
public class RequestLimitAspect {

    @Autowired
    RedisTemplate redisTemplate;

    // 切点
    @Pointcut("@annotation(requestLimit)")
    public void controllerAspect(RequestLimit requestLimit) {}

    @Around("controllerAspect(requestLimit)")
    public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
        // get parameter from annotation
        long period = requestLimit.period();
        long limitCount = requestLimit.count();

        // request info
        String ip = RequestUtil.getClientIpAddress();
        String uri = RequestUtil.getRequestUri();
        String key = "req_limit_".concat(uri).concat(ip);

        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        // add current timestamp
        long currentMs = System.currentTimeMillis();
        zSetOperations.add(key, currentMs, currentMs);

        // set the expiration time for the code user
        redisTemplate.expire(key, period, TimeUnit.SECONDS);

        // remove the value that out of current window
        zSetOperations.removeRangeByScore(key, 0, currentMs - period * 1000);

        // check all available count
        Long count = zSetOperations.zCard(key);

        if (count > limitCount) {
            log.error("接口拦截:{} 请求超过限制频率【{}次/{}s】,IP为{}", uri, limitCount, period, ip);
            throw new AuroraRuntimeException(ResponseCode.TOO_FREQUENT_VISIT);
        }

        // execute the user request
        return  joinPoint.proceed();
    }

}

Verwenden Sie Anmerkungen zur Strombegrenzungssteuerung

Hier definiere ich eine Schnittstellenklasse zum Testen, verwende die obige Anmerkung, um das aktuelle Limit zu vervollständigen, und erlaube Benutzern den Besuch dreimal pro Minute.

@Log4j2  
@RestController  
@RequestMapping("/user")  
public class UserController {    

    @GetMapping("/test")  
    @RequestLimit(count = 3)  
    public GenericResponse<String> testRequestLimit() {  
        log.info("current time: " + new Date());  
        return new GenericResponse<>(ResponseCode.SUCCESS);  
    }  
  
}

Dann greife ich auf verschiedenen Maschinen auf die Schnittstelle zu und sehe, dass die aktuelle Grenze verschiedener Maschinen isoliert ist und jede Maschine innerhalb eines Zeitraums nur dreimal darauf zugreifen kann. Erwarteter Effekt.

2023-05-21 11:23:15.733  INFO 99636 --- [nio-8080-exec-1] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:23:15 CST 2023
2023-05-21 11:23:21.848  INFO 99636 --- [nio-8080-exec-3] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:23:21 CST 2023
2023-05-21 11:23:23.044  INFO 99636 --- [nio-8080-exec-4] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:23:23 CST 2023
2023-05-21 11:23:25.920 ERROR 99636 --- [nio-8080-exec-5] c.v.c.a.annotation.RequestLimitAspect    : 接口拦截:/user/test 请求超过限制频率【3次/60s】,IP为0:0:0:0:0:0:0:1
2023-05-21 11:23:28.761 ERROR 99636 --- [nio-8080-exec-6] c.v.c.a.annotation.RequestLimitAspect    : 接口拦截:/user/test 请求超过限制频率【3次/60s】,IP为0:0:0:0:0:0:0:1
2023-05-21 11:24:12.207  INFO 99636 --- [io-8080-exec-10] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:24:12 CST 2023
2023-05-21 11:24:19.100  INFO 99636 --- [nio-8080-exec-2] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:24:19 CST 2023
2023-05-21 11:24:20.117  INFO 99636 --- [nio-8080-exec-1] c.v.c.a.api.controller.UserController    : current time: Sun May 21 11:24:20 CST 2023
2023-05-21 11:24:21.146 ERROR 99636 --- [nio-8080-exec-3] c.v.c.a.annotation.RequestLimitAspect    : 接口拦截:/user/test 请求超过限制频率【3次/60s】,IP为192.168.31.114
2023-05-21 11:24:26.779 ERROR 99636 --- [nio-8080-exec-4] c.v.c.a.annotation.RequestLimitAspect    : 接口拦截:/user/test 请求超过限制频率【3次/60s】,IP为192.168.31.114
2023-05-21 11:24:29.344 ERROR 99636 --- [nio-8080-exec-5] c.v.c.a.annotation.RequestLimitAspect    : 接口拦截:/user/test 请求超过限制频率【3次/60s】,IP为192.168.31.114

Achten Sie gerne auf den offiziellen Account [MALAOSI] und sprechen Sie nur über die am einfachsten zu verstehenden originalen technischen Trockenwaren.

Ich denke du magst

Origin juejin.im/post/7235484890018562106
Empfohlen
Rangfolge