我正在开发一个 Django/DRF 应用程序,并且我正在尝试实现一个 API 限制,该限制对于失败的登录尝试会产生越来越长的延迟。
例如。在 3 次失败尝试后将用户锁定 1 分钟,在 6 次失败后锁定用户 10 分钟,在 9 次失败后锁定用户 30 分钟等,类似于手机的操作以及一般登录页面的常见操作。我很惊讶地发现,考虑到这种登录场景的普遍性,Django 或 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
方法,该方法返回一个不会与列表中其他节流阀发生冲突的唯一键。
这几乎有效 - 它可以阻止刚刚踩油门的机器人 - 但是,它有几个问题:
DRF 限制没有一种简单的方法来清除限制列表。我通过手动修改 DRF 节流使用的缓存来解决这个问题,但这并不理想......
DRF 节流阀在请求循环中的某个点触发,并且该点可能会或可能不会发生身份验证 - 因此节流阀可能不知道传入的凭据是否良好:
A.如果通过
APIView.authentication_classes
字段进行身份验证,则身份验证发生在节流阀之前,然后节流阀可以知道身份验证是否成功并可以采取相应的行动。这样做的缺点是每个机器人请求都会导致数据库命中。
B.如果在视图代码中进行身份验证,则身份验证会在触发节流阀之后之后发生。缺点是节流器不知道传入的信用是否良好,但优点是机器人在数据库受到攻击之前被阻止。
authentication_classes
进行 2FA,但这就是今天的情况......)并且因为我们希望通过最少的数据库攻击来阻止机器人。选项 B 有点排除了 DRF 限制,因为边缘情况会导致糟糕/令人困惑的用户体验。 其他选项:
django-axes 作为 DRF 节流阀的替代方案。
优点:
缺点:
我觉得我在这里重新发明了轮子......渐进式节流在登录世界中很常见,但它似乎完全是 Django 世界中缺席...
我缺少一些简单的东西吗?似乎这样的东西应该已经内置到 Django/DRF 的某个地方......
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 的指导下)
如果我给了你你正在尝试重新发明的轮子,请标记回答,这个微型库是为了你的问题而创建的,谢谢你,我太懒了,我以前的许多应用程序都没有速率限制