【问题标题】:How can I implement incremental wait time for django axes?如何为 django 轴实现增量等待时间?
【发布时间】:2020-12-02 16:58:36
【问题描述】:

我希望为我的应用程序实现 django 轴以进行强力保护。我想实现类似“5次尝试失败后锁定5分钟,再尝试5次后锁定10分钟” 似乎文档中的示例更简单,等待时间固定,所以我想知道是否有可能实现这样的东西。

【问题讨论】:

    标签: django security axes brute-force


    【解决方案1】:

    是的,这是可能的,但需要设置几个项目。

    models.py

    from axes.models import AccessAttempt
    from django.db import models
    from django.utils.translation import gettext_lazy as _
    
    
    class AccessAttemptAddons(models.Model):
        accessattempt = models.OneToOneField(AccessAttempt, on_delete=models.CASCADE)
        expiration_date = models.DateTimeField(_("Expiration Time"), auto_now_add=False)
    

    这个 models.py 文件位于我的登录应用程序中(目录:project_name/login/models.py)。这将创建一个名为 login_accessattemptaddons 的新表,并与 Django Axes 的 AccessAttempt 表建立“一对一”关系。这是必要的,以便 AccessAttempt 表中的每个条目都与 AccessAttemptAddons 表中的每个条目相关联;因此,使 AccessAttemptAddons 中的 expiration_date 列成为 AccessAttempt 表的扩展。

    settings.py

    AXES_FAILURE_LIMIT = 5
    AXES_LOCK_OUT_AT_FAILURE = True
    AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True
    AXES_LOCKOUT_CALLABLE = "login.views.timeout"
    

    在 settings.py 中,AXES_FAILURE_LIMIT 设置为 5,这意味着在第 5 次登录失败后,AXES_LOCKOUT_CALLABLE 将执行。 AXES_LOCKOUT_CALLABLE 设置为“login.views.timeout”。我不确定这是否是最佳实践,但失败后运行的功能位于我的项目登录应用程序、views.py 文件和我的views.py 文件中,一个名为“超时”的方法。

    views.py

    import datetime
    
    from axes.models import AccessAttempt
    from django.contrib.auth import login, authenticate
    from django.http import HttpResponseRedirect
    from django.shortcuts import render
    from django.urls import reverse
    from django.views import View
    
    from .models import AccessAttemptAddons
    from .forms import LoginForm
    
    
    class LoginView(View):
        form = LoginForm
        context = {"form": form}
        base_page = "login/index.html"
        auth_base_page = "login/index.auth.html"
    
        def get(self, request):
            """ returns the login app index page """
            if(request.user.is_authenticated):
                return render(request, self.auth_base_page, self.context)
            else:
                return render(request, self.base_page, self.context)
    
        def post(self, request):
            """ Handles logging the user in """
    
            # passing the request to the form to access in our forms.py file
            form_post = self.form(request.POST, request=request)
            if(form_post.is_valid()):
                email = form_post.cleaned_data["email"]
                password = form_post.cleaned_data["password"]
                user = authenticate(username=email, password=password)
                login(request, user, backend="django.contrib.auth.backends.ModelBackend") 
                return HttpResponseRedirect(reverse("account:index"))
            else:
                errors = form_post.non_field_errors()
                context = {"form": form_post, "errors": errors}
                return render(request, self.base_page, context) 
    
    def timeout(request):
        try:
            loginview = LoginView()
            username = request.POST.get("email")
            ip_address = request.axes_ip_address
            account = AccessAttempt.objects.filter(username=username).filter(ip_address=ip_address)
            
            current_time = datetime.datetime.now()
            number_of_attempts = account.failures_since_start
            threshold = (number_of_attempts / 5) * 5
           
            form = LoginForm(request.POST)
            error = f"Access attempts exceeded. Please wait {threshold} minutes"
            base_page = "login/index.html"
            context = {"form": form, "errors": error}
            
            result = AccessAttempt.objects.raw(
                '''
                SELECT axes_accessattempt.id, login_accessattemptaddons.expiration_date
                FROM axes_accessattempt
                INNER JOIN login_accessattemptaddons
                ON axes_accessattempt.id = login_accessattemptaddons.accessattempt_id
                WHERE axes_accessattempt.username = %s and axes_accessattempt.ip_address = %s
                ''', [username, ip_address]
            )[0]
    
            if(current_time < result.expiration_date):
                return render(request, base_page, context)
            else:
                account.delete()
                account_page = loginview.post(request)
                return account_page
    
        except IndexError:
            expiration_date = current_time + datetime.timedelta(minutes=threshold)
            id = AccessAttempt.objects.filter(username=username, ip_address=ip_address)[0].id
            addons = AccessAttemptAddons(expiration_date=expiration_date, accessattempt_id=id)
            addons.save()
            return render(request, base_page, context)
    

    这是我的views.py(目录:project_name/login/views.py)的副本。从超时方法开始,请注意阈值公式是尝试次数除以 5,然后结果乘以 5。示例:(10 次失败尝试/5)* 5min = 10min 超时。对于不能被 5(11, 12, .. 14) 整除的尝试,它们将向下舍入到 10 分钟锁定。对于 15 次失败到 19 次失败的尝试,锁定时间为 15 分钟。

    继续代码,AccessAttempt 表和 AccessAttemptAddons 表在一对一字段“accessattempt_id”上连接。这将创建一个结果表,其中可以获得每个相关 AccessAttempt id 的 expiration_date 信息。如果当前失败的尝试没有条目(全新帐户锁定),则会发生索引错误。错误被捕获并使用上面的公式在 AccessAttemptAddons 表中创建一个新的过期条目。在任何后续尝试中,原始 sql 将通过而不会出现索引失败,因为现在有一个现有的 expiration_date 条目。

    这会导致 if 语句块检查帐户是否已准备好解锁或将错误页面返回给用户。如果当前时间小于结果表中的 expire_date 条目,则返回带有错误消息的页面。如果当前时间更大,则意味着已经有足够的时间重试登录帐户。请求被移交给登录视图“post”方法。在此之后的任何失败都会重复超时方法步骤。

    结束语

    views.py 中的 timeout 方法是必须的;锁定到期后如何处理请求取决于开发人员。我只展示了我如何使用基于类的登录视图来处理它。

    【讨论】:

      猜你喜欢
      • 2018-10-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多