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