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

问题描述 投票:0回答:1

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

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

DRF 油门选项:

Django Rest Framework

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

def get_throttles(self):
    return [
        MyCustomThrottle('3/m'),
        MyCustomThrottle('6/10m'),
        MyCustomThrottle('9/30m'),
    ]

然后向

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

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

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

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

    A.如果通过

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

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

我们的应用程序正在执行选项 B,因为我们也在实施 2FA(也许有一种方法可以通过

authentication_classes

 进行 2FA,但这就是今天的情况......)并且因为我们希望通过最少的数据库攻击来阻止机器人。 
选项 B 有点排除了 DRF 限制,因为边缘情况会导致糟糕/令人困惑的用户体验。

其他选项:

我已经开始将

django-axes 作为 DRF 节流阀的替代方案。

优点:

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

缺点:

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

我觉得我在这里重新发明了轮子......渐进式节流在登录世界中很常见,但它似乎完全是 Django 世界中缺席...

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

django authentication django-rest-framework throttling
1个回答
0
投票

Bouncer.PY - 机器人打击者

这是我的个人库,用于与框架无关的速率限制,适用于 Flask、FastAPI、Django。计划发布到

pip install botbouncer

 的官方 pypy 存储库。感谢反馈。


设置

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"))

输出

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'}

来源:图标@Flaticons,参考:ChatGPT(在 ofc 的指导下)

如果我给了你你正在尝试重新发明的轮子,请标记回答,这个微型库是为了你的问题而创建的,谢谢你,我太懒了,我以前的许多应用程序都没有速率限制

© www.soinside.com 2019 - 2024. All rights reserved.