如何在 Django 中渐进式登录速率限制?

Tro*_*roy 5 authentication django throttling django-rest-framework

我正在开发一个 Django/DRF 应用程序,并且正在尝试实现一个 API 限制,该限制对于失败的登录尝试会产生越来越长的延迟。

例如。在 3 次失败尝试后将用户锁定 1 分钟,在 6 次失败后锁定用户 10 分钟,在 9 次失败后锁定用户 30 分钟等,类似于手机的操作以及一般登录页面的常见操作。我惊讶地发现,考虑到登录场景的普遍性,Django 或 DRF 中似乎没有内置渐进式节流阀......

DRF 油门选项:

Django Rest FrameworkAPIView提供了一个throttle_classes字段和一个get_throttles()方法,并且它有一些用于执行固定速率油门延迟的通用油门。我可以通过添加限制列表来模拟渐进速率,如下所示:

def get_throttles(self):
    return [
        MyCustomThrottle('3/m'),
        MyCustomThrottle('6/10m'),
        MyCustomThrottle('9/30m'),
    ]
Run Code Online (Sandbox Code Playgroud)

然后添加一个自定义get_cache_key()方法,MyCustomThrottle该方法返回一个不会与列表中其他节流阀冲突的唯一键。

这几乎是有效的——它可以阻止刚刚踩油门的机器人——但是,它有几个问题:

  1. 如果/当用户成功登录时,DRF 限制没有一种简单的方法可以清除限制列表。我通过手动修改 DRF 节流使用的缓存来解决这个问题,但这并不理想......

  2. DRF 节流阀在请求循环中的某个点触发,并且该点可能会也可能不会发生身份验证 - 因此节流阀可能不知道传入的凭据是否良好:

    答:如果通过APIView.authentication_classes现场进行身份验证,则身份验证发生在节流阀之前,然后节流阀可以知道身份验证是否成功并可以采取相应的行动。这样做的缺点是每个机器人请求都会导致数据库命中。

    B. 如果在视图代码中进行身份验证,则身份验证会在触发限制后发生。缺点是节流器不知道传入的信用是否良好,但优点是机器人在数据库受到攻击之前被阻止。

我们的应用程序正在执行选项 B,因为我们也在实施 2FA(也许有一种方法可以通过 2FA 进行authentication_classes,但这就是今天的情况......)并且因为我们希望以最少的 DB 攻击来阻止机器人。 选项 B 有点排除了 DRF 限制,因为边缘情况会导致糟糕/令人困惑的用户体验。

其他选项:

我开始将django-axes视为 DRF 节流阀的替代方案。

优点:

  1. 它似乎更注重身份验证,并且从头开始构建时就考虑到了身份验证。它通过中间件实现限制,因此无论何时何地进行身份验证,它都是身份验证友好的。
  2. 它提供了一种简单的方法来清除对用户、IP 或一般基础的限制。

缺点:

  1. 它似乎没有办法为我正在拍摄的渐进/增加油门延迟提供多个油门。
  2. 它主要被设计为“将用户锁定在手动清除之前”的库。它确实有一个选项来提供“冷却”时间以自动解锁,但它不会向用户报告还剩下多少冷却时间(就像 DRF 那样),也没有为我提供简单的方法向用户提供该信息(也许确实如此 - 我对这个库还是有点陌生​​)。
  3. 整个应用程序似乎只有一组配置选项 - 没有特定于视图的配置。我们为 2 种类型的用户提供 2 个登录页面。每个都有不同的节流需求。

也许有一种方法可以让 django-axes 工作,但无论如何,我觉得我在这里重新发明了轮子......渐进式节流在登录世界中很常见,但在 Django 中似乎完全不存在世界...

我缺少一些简单的东西吗? 似乎这样的东西应该已经内置到 Django/DRF 的某个地方......

nik*_*ami 1

BotBouncer.py - 停止那些无头机器

这是我的个人库,用于与框架无关的速率限制,适用于 Flask、FastAPI、Django。计划发布到pip install botbouncer. 感谢反馈。


设置

pip install expiringdict


代码

from expiringdict import ExpiringDict

BOUNCERS = {}
THROTTLE_CONDITIONS = [
    "3/m",
    "6/10m",
    "10/30m"
]


def init_bouncers(defs: list):
    # input[list]: "3/m", "6/10m" , "10/30m"
    # format: n/t -> (n attempts)/(t window)
    global BOUNCERS
    tmap = {
        'm': 60,
        'h': 3600,
        'd': 86400,
        'w': 604800,
        'y': 31536000
    }

    for d in defs:
        n, t = d.split('/')

        if t in tmap:
            t = str("1" + t)

        # print(f"Initializing BOUNCERS for {n} attempts in {t} seconds")
        BOUNCERS[d] = {
            "memory": ExpiringDict(max_len=10000, max_age_seconds=int(t[:-1]) * tmap[t[-1]]),
            "loginlimit": int(n),
        }


def throttled_login(username):
    # query BOUNCERS for user if user is not in BOUNCERS, add user to BOUNCERS
    # check if user has exceeded attempts return False
    LIMIT_REACHED = False
    for k, v in BOUNCERS.items():
        if username not in v["memory"]:
            v["memory"][username] = 1
            print(f"{username} attempted login {v['memory'][username]} times in {k}")

        else:
            print(f"{username} attempted login {v['memory'][username]} times in {k}")
            if v["memory"][username] > v["loginlimit"]:
                LIMIT_REACHED = True
            v["memory"][username] += 1

    if LIMIT_REACHED:
        return {"status": 429, "message": "Too many requests, retry after some time"}
    else:
        return simulate_login(username)


def simulate_login(username):
    # Simulate a successful login for demonstration purposes
    return "Login Success"


if __name__ == "__main__":
    init_bouncers(THROTTLE_CONDITIONS)

    for i in range(1000):
        print(throttled_login("user123"))
Run Code Online (Sandbox Code Playgroud)

输出

user123 attempted login 1 times in 3/m
user123 attempted login 1 times in 6/10m
user123 attempted login 1 times in 10/30m
Login Success
user123 attempted login 1 times in 3/m
user123 attempted login 1 times in 6/10m
user123 attempted login 1 times in 10/30m
Login Success
user123 attempted login 2 times in 3/m
user123 attempted login 2 times in 6/10m
user123 attempted login 2 times in 10/30m
Login Success
user123 attempted login 3 times in 3/m
user123 attempted login 3 times in 6/10m
user123 attempted login 3 times in 10/30m
Login Success
user123 attempted login 4 times in 3/m
user123 attempted login 4 times in 6/10m
user123 attempted login 4 times in 10/30m
{'status': 429, 'message': 'Too many requests, retry after some time'}
user123 attempted login 5 times in 3/m
user123 attempted login 5 times in 6/10m
user123 attempted login 5 times in 10/30m
{'status': 429, 'message': 'Too many requests, retry after some time'}
Run Code Online (Sandbox Code Playgroud)