.. image:: https://jazzband.co/static/img/badge.svg :target: https://jazzband.co/ :alt: Jazzband
.. image:: https://img.shields.io/pypi/pyversions/django-defender.svg :alt: 支持的 Python 版本 :target: https://pypi.org/project/django-defender/
.. image:: https://img.shields.io/pypi/djversions/django-defender.svg :target: https://pypi.org/project/django-defender/ :alt: 支持的 Django 版本
.. image:: https://github.com/jazzband/django-defender/workflows/Test/badge.svg :target: https://github.com/jazzband/django-defender/actions :alt: GitHub Actions
.. image:: https://codecov.io/gh/jazzband/django-defender/branch/master/graph/badge.svg :target: https://codecov.io/gh/jazzband/django-defender :alt: 代码覆盖率
.. image:: https://readthedocs.org/projects/django-defender/badge/?version=latest :alt: 文档状态 :target: https://django-defender.readthedocs.io/en/latest/?badge=latest
一个简单的 Django 可复用应用,用于阻止暴力破解登录尝试。目标是使其尽可能快,避免拖慢登录过程。
我们将使用缓存,这样每次登录尝试时无需查询数据库。第一个版本基于 Redis,但目标是使其可配置,以便用户根据需求选择最佳后端。
如果你正在自己的站点上使用 defender,请提交 PR 以将其加入列表。
文档可在 Read the Docs 上获取:
https://django-defender.readthedocs.io
将所有登录尝试记录到数据库
支持使用不同标头获取 IP 地址的反向代理
基于以下条件进行速率限制
使用 Redis 存储黑名单
配置
Redis 服务器
封锁时长
封锁前的错误尝试次数
代码覆盖率 95%
完善的文档
能够将登录尝试存储到数据库
用于清理登录尝试数据库表的管理命令
管理页面
可轻松适配自定义认证方法
封锁用户名或 IP 时发送信号
管理页面
.. image:: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :target: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :alt: 管理页面截图 1
.. image:: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :target: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :alt: 管理页面截图 2
下载代码,并根据方法选择以下一种方式运行 setup。
从 PyPI 安装生产就绪版本:
.. code-block:: bash
pip install django-defender
从源代码下载后安装开发版本:
.. code-block:: bash
python setup.py install
从 GitHub 仓库安装 master 分支开发版本:
.. code-block:: bash
pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev
首先,你需要将此项目添加到 settings.py 中的 INSTALLED_APPS 列表:
.. code-block:: python
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', # ... 'defender', # ... ]
接下来,安装 FailedLoginMiddleware 中间件:
.. code-block:: python
MIDDLEWARE_CLASSES = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'defender.middleware.FailedLoginMiddleware', ]
如果你想通过 Django 管理后台管理被封锁的用户,请将以下内容添加到 urls.py:
.. code-block:: python
urlpatterns = [ path('admin/defender/', include('defender.urls')), # defender 管理 path('admin/', admin.site.urls), # 常规管理 # 你自己的路由规则... ]
数据库迁移
你需要在数据库中创建运行所需的表。
.. code-block:: bash
python manage.py migrate defender
管理命令
cleanup_django_defender
如果你的网站流量很大,AccessAttempts 表会很快被填满。如果你不需要保留数据用于审计,有一个管理命令可以帮助你保持表整洁。
它会根据 DEFENDER_ACCESS_ATTEMPT_EXPIRATION 设置决定删除哪些记录。如果未指定,默认为 24 小时。
.. code-block:: bash
$ python manage.py cleanup_django_defender
你可以将其设置为每日或每周的 cron 任务,以控制表的大小。
.. code-block:: bash
24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log
性能
defender 的目标是尽可能快,避免拖慢登录过程。为确保达到目标,我们需要一种测试应用的方法,以确认我们走在正确的轨道上。最好的方法是比较使用 defender、django-axes 和普通 Django 登录的耗时。
普通 Django 登录作为基准,预计是三种方法中最快的,因为没有额外的检查。
带 defender 的登录可能会比普通 Django 登录慢,但希望比 django-axes 登录快。目标是使常规登录与 defender 之间的差异尽可能小。
django-axes 的登录速度可能是三者中最慢的,因为它进行更多检查并执行大量数据库查询。
确定登录速度的最佳方法是对每种配置的应用进行负载测试,并比较每种类型的登录时间。
负载测试
为了确保覆盖所有不同类型的登录,在负载测试中我们需要不止一项测试。
#. 全部成功:仅进行成功登录的负载测试。
#. 混合:部分成功、部分失败:进行包含成功和失败登录的负载测试,观察失败对性能的影响。
#. 全部失败:全部失败登录的负载测试,比较性能差异。
我们需要一个示例应用来进行负载测试,唯一区别在于配置:加载 defender、axes,或两者都不加载。
我们可以使用托管的负载测试服务,或类似 jmeter 的工具。无论哪种方式,所有测试必须保持一致。如果使用 jmeter,应提供 jmeter 配置,以便其他人自行运行测试。
负载测试结果
我们将在此处发布结果。我们会解释每项测试,并展示结果及图表。
django-axes 很优秀,但它将所有数据存储在数据库中,当数据量很大时会造成瓶颈。它会使认证请求延迟 200-300ms。这对某些站点可能不算什么,但对其他站点来说太长了。
该项目最初是 django-axes 的一个分支,尽可能多地使用其代码,同时移除不需要的部分,并加速查询以改进登录。
#. 当有人尝试登录时,首先检查他们当前是否被封锁。检查他们使用的用户名以及 IP 地址。如果被封锁,转至第 5 步。如果未被封锁,转至第 2 步。
#. 未被封锁,因此检查登录是否有效。如果有效,转至第 6 步。如果无效,转至第 3 步。
#. 登录尝试无效。为此尝试将用户名和 IP 地址添加到缓存。如果这导致他们超出限制,将其添加到封锁列表,然后转至第 5 步。如果未超出限制,转至第 4 步。
#. 登录无效,但未超出限制。将用户发回登录页面,让他们重试。
#. 用户被封锁:将用户发至封锁页面,告知他们已被封锁,并给出预计解封时间。
#. 登录有效。重置所有失败登录尝试,并将用户转到目标页面。
Defender 使用缓存保存失败的尝试。
缓存键
计数器:
布尔值(如果存在则表示已封锁):
有几个选项可以对 django-defender 进行自定义。这些应在你的 settings.py 文件中定义。
DEFENDER_LOGIN_FAILURE_LIMIT\ : 整数:在记录失败登录之前允许的登录尝试次数。 [默认值: 3\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME\ : 整数:在记录失败登录之前允许的用户名尝试次数。 [默认值: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_IP\ : 整数:在记录失败登录之前允许的 IP 尝试次数。 [默认值: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_BEHIND_REVERSE_PROXY\ : 布尔值:defender 是否位于反向代理后面? [默认值: False\ ]
DEFENDER_REVERSE_PROXY_HEADER\ : 字符串:包含你的反向代理 IP 地址的 HTTP 标头名称。 [默认值: HTTP_X_FORWARDED_FOR\ ]
DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : 布尔值:根据 IP 和用户名的组合锁定用户。这可以防止用户通过封锁 IP 地址来阻止来自同一 IP 的其他用户访问应用程序。 [默认值: False\ ]
关于使用 DEFENDER_ATTEMPT_COOLOFF_TIME 和 DEFENDER_LOCKOUT_COOLOFF_TIME 的理由
虽然仅使用 DEFENDER_COOLOFF_TIME 对大多数用例来说已足够,但在某些特定场景下(如高安全设置),开发者可能希望对在考虑封锁时“记住”无效登录尝试的时间有更细粒度的控制,相比封锁键实际被封锁的时间。 DEFENDER_ATTEMPT_COOLOFF_TIME 和 DEFENDER_LOCKOUT_COOLOFF_TIME 就提供了这种精确的细粒度配置。
我们还可以考虑一个低安全性和低规模的例子,比如高中的网站。这样的网站可能运行在学校的一些计算机上,由学校的 IT 人员和计算机科学老师(如果幸运的话有)管理。在这种情况下,我们可以想象网站的大部分内容无需认证即可访问,但登录后可以访问一些相对私密的信息,如学生的姓名、电子邮件、成绩和课程安排。最后,由于账户关联了电子邮件,我们假设有密码重置功能,完成后会解除账户封锁。在这种情况下,可以想象,不需要长时间记住失败的登录,因为应用程序只想防范潜在的拒绝服务攻击。这可以通过将 DEFENDER_ATTEMPT_COOLOFF_TIME 设置得较低(比如 30 秒),并将 DEFENDER_LOCKOUT_COOLOFF_TIME 设置得更高(比如 600 秒)来实现。通过保持 DEFENDER_ATTEMPT_COOLOFF_TIME 较低,并通过对 DEFENDER_LOCKOUT_COOLOFF_TIME 设置较高值长时间封锁恶意用户,可以抵御快速的暴力破解登录攻击,并且他们的小型服务器可以在缓存中留出更多空间用于其他数据。通过提供上述密码重置功能,这些假设的管理员可以限制他们手动解封真实用户的工作量,同时保持网站的可访问性。
虽然前面的例子有些牵强,但以下解释和示例展示了这些配置的全部威力。
当 DEFENDER_STORE_ACCESS_ATTEMPTS 为 True 时,DEFENDER_LOCKOUT_COOLOFF_TIME 也可以配置为整数列表。当配置为列表时,对于配置的封锁键,先前的失败登录尝试次数除以 DEFENDER_LOGIN_FAILURE_LIMIT,以产生一个故意高估的在 DEFENDER_ACCESS_ATTEMPT_EXPIRATION 定义的时间段内的失败登录次数。由于在计算时未考虑失败登录尝试之间的时间间隔,这最终会是一个高估值。虽然这可能看起来严厉,但在某些特定场景下,针对较慢攻击提供的额外保护值得对系统真实用户造成的潜在不便。
一个这样的例子是,一个面向公众的 Web 应用程序,它存储用户的敏感信息(比如个人财务记录)。应用程序及其数据应尽量不中断地访问,但安全性至关重要,因此可以容忍一定程度的延迟。在这种情况下,我们可能希望将 DEFENDER_COOLOFF_TIME 设置为一个非常大的整数甚至 0 以获得最大保护。但这意味着,如果真实用户被封锁,我们需要管理员手动解封,这当然很麻烦且成本高昂。通过将 DEFENDER_ATTEMPT_COOLOFF_TIME 设置为一个足够大的数字(比如 600),并将 DEFENDER_LOCKOUT_COOLOFF_TIME 设置为一个递增整数列表(例如 [60, 120, 300, 600, 0]),我们可以在保护理论应用程序方面与仅将 DEFENDER_COOLOFF_TIME 设置为 600 相当,同时显著减少对用户的干扰。
defender 可用于 Django 认证系统之外的认证。例如,如果需要保护 django-rest-framework 的认证免受暴力攻击,可以实现自定义认证方法。
以下是基于 djangorestframework.BasicAuthentication 的 BasicAuthenticationDefender 类示例:
.. code-block:: python
import base64 import binascii
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework.authentication import ( BasicAuthentication, get_authorization_header, )
from defender import utils from defender import config
class BasicAuthenticationDefender(BasicAuthentication):def get_username_from_request(self, request): auth = get_authorization_header(request).split() return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
if utils.is_already_locked(request, get_username=self.get_username_from_request):
detail = "You have attempted to login {failure_limit} times, with no success." \
"Your account is locked for {cooloff_time_seconds} seconds" \
"".format(
failure_limit=config.FAILURE_LIMIT,
cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[
defender_utils.get_lockout_cooloff_time(username=self.get_username_from_request(request))
]
)
raise exceptions.AuthenticationFailed(_(detail))
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
login_unsuccessful = False
login_exception = None
try:
response = self.authenticate_credentials(userid, password)
except exceptions.AuthenticationFailed as e:
login_unsuccessful = True
login_exception = e
utils.add_login_attempt_to_db(request,
login_valid=not login_unsuccessful,
get_username=self.get_username_from_request)
# add the failed attempt to Redis in case of a failed login or resets the attempt count in case of success
utils.check_request(request,
login_unsuccessful=login_unsuccessful,
get_username=self.get_username_from_request)
if login_unsuccessful:
raise login_exception
return response
要使其生效,请在 settings.py 中将 BasicAuthenticationDefender 添加到所有其他认证方法之上的 DEFAULT_AUTHENTICATION_CLASSES 中。
defender 可以与 django-rest-framework 和 django-rest-auth 结合使用,用于认证用户。
参考
以下是一个基于 rest_framework.authentication.TokenAuthentication 的示例 BasicAuthenticationDefender 类,它使用 django-rest-auth 库进行用户认证。
.. code-block:: python
import base64 import binascii
from django.conf import settings from django.contrib.auth import get_user_model, authenticate from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm from django.contrib.auth.tokens import default_token_generator from django.utils.http import urlsafe_base64_decode as uid_decoder from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str from rest_framework import serializers, exceptions, HTTP_HEADER_ENCODING from rest_framework.exceptions import ValidationError from defender import utils as defender_utils from defender import config from rest_framework.authentication import ( get_authorization_header, )
UserModel = get_user_model()
class BasicAuthenticationDefender(serializers.Serializer):
username = serializers.CharField(required=False, allow_blank=True)
email = serializers.EmailField(required=False, allow_blank=True)
password = serializers.CharField(style={'input_type': 'password'})
def authenticate(self, **kwargs):
request = self.context['request']
if hasattr(settings, 'ACCOUNT_AUTHENTICATION_METHOD'):
login_field = settings.ACCOUNT_AUTHENTICATION_METHOD
else:
login_field = 'username'
userid = self.username_from_request(request, login_field)
if defender_utils.is_already_locked(request, username=userid):
detail = "You have attempted to login {failure_limit} times with no success. "
.format(
failure_limit=config.FAILURE_LIMIT,
cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[defender_utils.get_lockout_cooloff_time(username=userid)]
)
raise exceptions.AuthenticationFailed(_(detail))
login_unsuccessful = False
login_exception = None
try:
response = authenticate(request, **kwargs)
if response == None:
login_unsuccessful = True
msg = _('Unable to log in with provided credentials.')
# raise exceptions.ValidationError(msg)
login_exception = exceptions.ValidationError(msg)
except exceptions.AuthenticationFailed as e:
login_unsuccessful = True
login_exception = e
defender_utils.add_login_attempt_to_db(request,
login_valid=not login_unsuccessful,
username=userid)
user_not_blocked = defender_utils.check_request(request,
login_unsuccessful=login_unsuccessful,
username=userid)
if user_not_blocked and not login_unsuccessful:
return response
raise login_exception
def _validate_email(self, email, password):
user = None
if email and password:
user = self.authenticate(email=email, password=password)
else:
msg = _('Must include "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username(self, username, password):
user = None
if username and password:
user = self.authenticate(username=username, password=password)
else:
msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username_email(self, username, email, password):
user = None
if email and password:
user = self.authenticate(email=email, password=password)
elif username and password:
user = self.authenticate(username=username, password=password)
else:
msg = _('Must include either "username" or "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def validate(self, attrs):
username = attrs.get('username')
email = attrs.get('email')
password = attrs.get('password')
user = None
if 'allauth' in settings.INSTALLED_APPS:
from allauth.account import app_settings
# Authentication through email
if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
user = self._validate_email(email, password)
# Authentication through username
elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:
user = self._validate_username(username, password)
# Authentication through either username or email
else:
user = self._validate_username_email(username, email, password)
else:
# Authentication without using allauth
if email:
try:
username = UserModel.objects.get(
email__iexact=email).username()
except UserModel.DoesNotExist:
pass
if username:
user = self._validate_username_email(username, '', password)
# Did we get back an active user?
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.ValidationError(msg)
else:
msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg)
# If required, is the email verified?
if 'rest_auth.registration' in settings.INSTALLED_APPS:
from allauth.account import app_settings
if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
email_address = user.emailaddress_set.get(email=user.email)
if not email_address.verified:
raise serializers.ValidationError(
_('E-mail is not verified.'))
attrs['user'] = user
return attrs
def username_from_request(self, request, login_field):
user_data = request._data
return user_data[login_field]
要使其生效,请将 BasicAuthenticationDefender 添加到 settings.py 的 REST_AUTH_SERIALIZERS 字典中的 LOGIN_SERIALIZER 键下。例如,在 settings.py 中添加以下行:
.. code-block:: python
REST_AUTH_SERIALIZERS = { 'LOGIN_SERIALIZER': '.BasicAuthenticationDefender', }
defender 可以适配 Django 的 PasswordResetView 以防止过多提交。
我们需要创建一些新的视图,这些视图继承自 Django 内置的 LoginView、PasswordResetView 和 PasswordResetConfirmView ——然后在我们的 urls.py 中使用这些视图替换 Django 的内置视图。
这些视图基于在密码重置视图上提交的电子邮件地址进行阻止。这与默认实现(使用用户名)不同,因此我们需要小心地在登录和完成密码重置后清理缓存。
.. code-block:: python
from defender import utils as def_utils
from django.contrib.auth import views as auth_views
class UserSignIn(auth_views.LoginView):
def form_valid(self, form):
"""Force clear all the cached Defender statues for the authenticated user’s email address."""
super_valid = super().form_valid(form)
def_utils.check_request(self.request, False, username=form.get_user().email)
return super_valid
class PasswordResetBruteForceProtectedView(auth_views.PasswordResetView):
def get(self, request, *args, **kwargs):
"""Confirm the user isn’t already blocked by IP before showing the password reset view."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Confirm the user isn’t already blocked by IP before allowing form POST.
Also, force log this form POST as a single entry in the Defender cache, against the submitted email address.
"""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
def_utils.check_request(
request, login_unsuccessful=True, username=request.POST.get("email")
)
return super().post(request, *args, **kwargs)
class PasswordResetConfirmBruceForceProtectedView(auth_views.PasswordResetConfirmView):
def get(self, request, *args, **kwargs):
"""Confirm the user isn’t already blocked by IP before showing the password confirm view."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""Confirm the user isn’t already blocked by IP before allowing form POST for the password change confirmation."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().post(request, *args, **kwargs)
def form_valid(self, form):
"""Force clear all the cached Defender statues for the user’s email address after successfully changing their password."""
super_valid = super().form_valid(form)
def_utils.check_request(
self.request, login_unsuccessful=False, username=self.user.email
)
return super_valid
django-defender 会在阻止用户名或 IP 地址时发送信号。要设置信号接收函数:
.. code-block:: python
from django.dispatch import receiver
from defender import signals
@receiver(signals.username_block) def username_blocked(username, **kwargs): print("%s was blocked!" % username)
@receiver(signals.ip_block) def ip_blocked(ip_address, **kwargs): print("%s was blocked!" % ip_address)
克隆仓库并安装 Django 后,可以运行测试,例如:
.. code-block:: bash
PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings
使用代码覆盖率:
.. code-block:: bash
PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin) test defender --settings=defender.test_settings
#. python setup.py sdist
#. twine upload dist/*
DEFENDER_DISABLE_IP_LOCKOUT\ : 布尔值:如果为 True,则不会封锁用户 IP 地址,只会封锁用户名。 [默认值: False]
DEFENDER_DISABLE_USERNAME_LOCKOUT\ : 布尔值:如果为 True,则不会封锁用户名,只会封锁 IP 地址。 [默认值: False]
DEFENDER_COOLOFF_TIME\ : 整数:如果设置,定义一段时间不活动后,旧的失败登录尝试和用户名/IP 封锁将被遗忘。整数,解释为秒数。如果为 0,则失败登录尝试和用户名/IP 封锁均不会过期。 [默认值: 300\ ]
DEFENDER_ATTEMPT_COOLOFF_TIME\ : 整数:如果设置,覆盖由 DEFENDER_COOLOFF_TIME 设置的失败登录尝试遗忘时间。整数,解释为秒数。如果为 0,则失败登录尝试不会过期。 [默认值: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_COOLOFF_TIME\ : 整数或列表:如果设置,覆盖由 DEFENDER_COOLOFF_TIME 设置的用户名/IP 封锁遗忘时间。整数,解释为秒数。列表中的整数,解释为秒数,索引表示在过去的 DEFENDER_ACCESS_ATTEMPT_EXPIRATION 小时内已发生的封锁次数(达到某个最大值之前)。如果设置为 0 或 [],则用户名/IP 封锁不会过期。 [默认值: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_TEMPLATE\ : 字符串: [默认值: None\ ] 如果设置,指定在用户被封锁时渲染的模板。模板接收以下上下文变量:
cooloff_time_seconds\ : 冷却时间(秒)cooloff_time_minutes\ : 冷却时间(分钟)failure_limit\ : 封锁前的失败次数。DEFENDER_USERNAME_FORM_FIELD\ : 字符串:包含用户名的表单字段名称。 [默认值: username\ ]
DEFENDER_CACHE_PREFIX\ : 字符串:defender 键的缓存前缀。 [默认值: defender\ ]
DEFENDER_LOCKOUT_URL\ : 字符串:如果某人被封锁,你想要重定向到的 URL。
DEFENDER_REDIS_URL\ : 字符串:defender 的 redis URL。 [默认值: redis://localhost:6379/0\ ] (带密码的示例: redis://:mypassword@localhost:6379/0\ )
DEFENDER_REDIS_PASSWORD_QUOTE\ : 布尔值:如果 redis 密码中有特殊字符(如 '@'),可以使用 urllib.parse.quote("password!@#") 对密码进行转义,并将其设置为 True。 [默认值: False\ ]
DEFENDER_REDIS_NAME\ : 字符串:Django 设置中 CACHES 中的缓存名称(例如 "default")。如果设置了此项,将忽略 DEFENDER_REDIS_URL。 [默认值: None\ ]
DEFENDER_STORE_ACCESS_ATTEMPTS\ : 布尔值:如果希望将登录尝试存储到数据库,设置为 True。如果为 False,则不保存。 [默认值: True\ ]
DEFENDER_USE_CELERY\ : 布尔值:如果希望使用 Celery 将登录尝试存储到数据库,设置为 True。如果为 False,则内联保存。 [默认值: False\ ]
DEFENDER_ACCESS_ATTEMPT_EXPIRATION\ : 整数:在管理命令清理之前,访问尝试记录在数据库中保留的时间(小时)。 [默认值: 24\ ]
DEFENDER_GET_USERNAME_FROM_REQUEST_PATH\ : 字符串:从请求中获取用户名的函数的导入路径。如果你想使用自定义函数从请求中获取和处理用户名,可以在此指定。 [默认值: defender.utils.username_from_request\ ]