
تطبيق Django بسيط وسريع جداً وقابل لإعادة الاستخدام يمنع الأشخاص من هجمات القوة الغاشمة على تسجيل الدخول.
.. 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: Supported Python versions :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: Supported Django versions
.. 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: Coverage
.. image:: https://readthedocs.org/projects/django-defender/badge/?version=latest :alt: Documentation Status :target: https://django-defender.readthedocs.io/en/latest/?badge=latest
تطبيق Django قابل لإعادة الاستخدام بسيط يمنع محاولات تسجيل الدخول القسرية (Brute Force). الهدف هو جعله بأسرع ما يمكن، حتى لا نبطئ محاولات تسجيل الدخول.
سنستخدم ذاكرة تخزين مؤقت (Cache) بحيث لا نضطر لمراجعة قاعدة البيانات عند كل محاولة دخول. الإصدار الأول سيعتمد على Redis، لكن الهدف هو جعل ذلك قابلاً للتكوين بحيث يمكن للمستخدمين استخدام أي خلفية تناسب احتياجاتهم.
إذا كنت تستخدم defender على موقعك، فيرجى تقديم طلب Pull Request لإضافته إلى القائمة.
التوثيق متاح على Read the Docs:
https://django-defender.readthedocs.io
تسجيل جميع محاولات تسجيل الدخول في قاعدة البيانات
دعم للبروكسيات العكسية (Reverse Proxies) مع رؤوس مختلفة لعناوين IP
تحديد معدل المحاولات بناءً على
استخدام Redis في القائمة السوداء
إعدادات
خادم Redis
مدة الحظر
عدد المحاولات الخاطئة قبل الحظر
تغطية 95% للكود
توثيق كامل
إمكانية تخزين محاولات تسجيل الدخول في قاعدة البيانات
أمر إداري لتنظيف جدول محاولات تسجيل الدخول
صفحات إدارة
يمكن تكييفه بسهولة مع طريقة المصادقة المخصصة.
يتم إرسال إشارات (Signals) عند حظر اسم المستخدم أو عنوان 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: alt tag
.. 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: alt tag
قم بتنزيل الكود، ثم قم بتشغيل الإعداد بإحدى الطرق التالية حسب الطريقة.
لتثبيت الإصدار الجاهز للإنتاج من PyPI:
.. code-block:: bash
pip install django-defender
لتثبيت الإصدار التطويري من الكود المصدري بعد التنزيل:
.. code-block:: bash
python setup.py install
لتثبيت الإصدار التطويري من الفرع الرئيسي من مستودع GitHub:
.. code-block:: bash
pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev
أولاً، يجب إضافة هذا المشروع إلى قائمة INSTALLED_APPS في ملف
settings.py
.. 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 admin path('admin/', admin.site.urls), # normal admin # your own patterns follow... ]
الهجرات (Migrations)
ستحتاج إلى إنشاء جداول في قاعدة البيانات الخاصة بك ضرورية للتشغيل.
.. 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 هو جعله بأسرع ما يمكن حتى لا يبطئ عملية تسجيل الدخول. من أجل التأكد من تحقيق أهدافنا، نحتاج إلى طريقة لاختبار التطبيق للتأكد من أننا على المسار الصحيح. أفضل طريقة للقيام بذلك هي مقارنة سرعة تسجيل الدخول العادي في Django مع defender و django-axes.
تسجيل الدخول العادي في Django سيكون خط الأساس، ونتوقع أن يكون الأسرع من بين الطرق الثلاث، لأنه لا توجد فحوصات إضافية.
تسجيل الدخول باستخدام defender سيكون على الأرجح أبطأ من Django، ونأمل أن يكون أسرع من django-axes. الهدف هو جعل الفرق صغيرًا قدر الإمكان بين تسجيل الدخول العادي واستخدام defender.
سرعة تسجيل الدخول باستخدام django-axes ستكون على الأرجح الأبطأ بين الثلاثة لأنه يقوم بمزيد من الفحوصات ويقوم بالعديد من استعلامات قاعدة البيانات.
أفضل طريقة لتحديد سرعة تسجيل الدخول هي إجراء اختبار تحميل على تطبيق مع كل إعداد، ومقارنة أوقات تسجيل الدخول لكل نوع.
اختبار التحميل
من أجل التأكد من تغطية جميع أنواع تسجيل الدخول المختلفة، في اختبار التحميل نحتاج إلى أكثر من اختبار واحد.
#. جميعها ناجحة: سنقوم باختبار تحميل لا يحتوي إلا على محاولات تسجيل دخول ناجحة.
#. مختلط: بعض الناجح وبعض الفاشل: سنقوم باختبار تحميل مع بعض محاولات الدخول الناجحة وبعضها الفاشلة لرؤية كيف تؤثر الفاشلة على الأداء.
#. جميعها فاشلة: سنقوم باختبار تحميل مع جميع محاولات تسجيل الدخول الفاشلة ورؤية الفرق في الأداء.
سنحتاج إلى تطبيق عينة يمكننا استخدامه لاختبار التحميل، مع الاختلاف الوحيد هو التكوين حيث نقوم إما بتحميل defender، أو axes، أو لا شيء منهما.
يمكننا استخدام خدمة اختبار تحميل مستضافة، أو شيء مثل jmeter. بأي طريقة، نحتاج إلى أن نكون متسقين لجميع الاختبارات. إذا استخدمنا jmeter، يجب أن يكون لدينا تكوين jmeter ليقوم الآخرون بتشغيل الاختبارات بأنفسهم.
نتائج اختبارات التحميل
سنقوم بنشر النتائج هنا. سنشرح كل اختبار، ونعرض النتائج مع بعض الرسوم البيانية.
django-axes رائع لكنه يضع كل شيء في قاعدة البيانات، وهذا يسبب عنق زجاجة عندما يكون لديك الكثير من البيانات. إنه يبطئ طلبات المصادقة بمقدار 200-300 مللي ثانية. قد لا يكون هذا كثيرًا لبعض المواقع، لكنه كثير جدًا بالنسبة للآخرين.
بدأ هذا كمشتق من 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\ : نص: اسم رأس HTTP مع عنوان
IP الخاص بالبروكسي العكسي [الافتراضي: HTTP_X_FORWARDED_FOR\ ]
DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : قيمة منطقية: يقفل المستخدم بناءً على مزيج من
عنوان IP واسم المستخدم. هذا يمنع المستخدم من منع الوصول إلى التطبيق لجميع المستخدمين الآخرين
الذين يصلون إلى التطبيق من نفس عنوان IP. [الافتراضي: \ ]
الأساس المنطقي لاستخدام DEFENDER_ATTEMPT_COOLOFF_TIME و DEFENDER_LOCKOUT_COOLOFF_TIME
بينما استخدام DEFENDER_COOLOFF_TIME وحده كافٍ لمعظم حالات الاستخدام، عند استخدام
defender في بعض السيناريوهات المحددة مثل إعدادات الأمان العالية، قد يرغب المطورون
في الحصول على تحكم أدق في مدة "تذكر" محاولات الدخول غير الصالحة أثناء النظر في إمكانية
القفل مقارنة بالوقت الذي تكون فيه مفاتيح القفل مقفلة فعليًا من النظام.
DEFENDER_ATTEMPT_COOLOFF_TIME و DEFENDER_LOCKOUT_COOLOFF_TIME تسمحان بهذا
التكوين الدقيق تمامًا.
يمكننا أيضًا أخذ مثال على أمان منخفض وحجم منخفض مثل موقع مدرسة ثانوية. قد يتم تشغيل
مثل هذا الموقع على بعض أجهزة الكمبيوتر في المدرسة ويديره موظفو تكنولوجيا المعلومات
ومعلّمو علوم الكمبيوتر (إذا كانوا محظوظين بما يكفي لوجودهم). في هذا السيناريو، يمكننا
تخيل أن هناك أجزاء كبيرة من الموقع يمكن الوصول إليها بدون مصادقة، لكن تسجيل الدخول
إلى الموقع قد يوفر الوصول إلى بعض المعلومات المميزة نسبيًا مثل اسم الطالب، البريد
الإلكتروني، الدرجات، والجدول الدراسي. أخيرًا، نظرًا لوجود بريد إلكتروني مرتبط بالحساب،
سنفترض أن هناك وظيفة إعادة تعيين كلمة المرور التي تفتح الحساب عند اكتمالها. في مثل
هذه الحالة، يمكن للمرء أن يتخيل أنه لا حاجة لتذكر محاولات الدخول الفاشلة لفترات
طويلة لأن التطبيق ببساطة يرغب في الحماية من هجمات حجب الخدمة المحتملة. يمكن تحقيق
ذلك عن طريق الحفاظ على 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.
ينتهي هذا التقدير المبالغ فيه لأنه لا يتم أخذ الوقت بين محاولات الدخول الفاشلة في
الاعتبار عند إجراء هذا الحساب. على الرغم من أن هذا قد يبدو قاسيًا، إلا أنه في بعض
السيناريوهات المحددة، يمكن أن تستحق الحماية الإضافية ضد الهجمات الأبطأ الإزعاج
المحتمل الذي يسببه للمستخدمين الحقيقيين للنظام.
أحد الأمثلة على ذلك يمكن أن يكون تطبيق ويب عام عبر الإنترنت يخزن معلومات حساسة
لمستخدميه (لنقل السجلات المالية الشخصية). يجب أن يكون التطبيق والبيانات الموجودة
فيه قابلة للوصول بأقل قدر من الانقطاع، ومع ذلك فإن الأمان أساسي لذا يمكن تحمل
التأخيرات إلى حد ما. في هذه الظروف، قد يكون لدينا رغبة في تعيين 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 بحاجة إلى الحماية من
هجمات القوة الغاشمة، يمكن تنفيذ طريقة مصادقة مخصصة.
هناك نموذج لفئة BasicAuthenticationDefender استنادًا إلى
djangorestframework.BasicAuthentication:
.. 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
لجعلها تعمل، أضف BasicAuthenticationDefender إلى DEFAULT_AUTHENTICATION_CLASSES فوق جميع طرق المصادقة الأخرى في ملف settings.py الخاص بك.
يمكن دمج defender مع مزيج من django-rest-framework و django-rest-auth اللذَيْن يُستخدمان لمصادقة المستخدمين.
المراجع
فيما يلي نموذج لفئة BasicAuthenticationDefender مبنية على rest_framework.authentication.TokenAuthentication والتي تستخدم مكتبة 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 إلى قاموس REST_AUTH_SERIALIZERS في ملف settings.py الخاص بك تحت المفتاح LOGIN_SERIALIZER.
على سبيل المثال، في ملف settings.py الخاص بك أضف السطر التالي:
.. code-block:: python
REST_AUTH_SERIALIZERS = { 'LOGIN_SERIALIZER': '.BasicAuthenticationDefender', }
يمكن تكييف defender مع PasswordResetView الخاص بـ Django لمنع الإرسالات المفرطة.
نحتاج إلى إنشاء بعض المشاهدات الجديدة التي ترث من LoginView و PasswordResetView و PasswordResetConfirmView المضمنة في Django — ثم استخدام هذه المشاهدات في ملف urls.py الخاص بنا كبدائل للمشاهدات المضمنة.
تعمل هذه المشاهدات على الحظر بناءً على عنوان البريد الإلكتروني المقدم في نموذج إعادة تعيين كلمة المرور. يختلف هذا عن التنفيذ الافتراضي (الذي يستخدم اسم المستخدم)، لذا يجب أن نكون حريصين على التنظيف بعد أنفسنا عند تسجيل الدخول أو عند اكتمال إعادة تعيين كلمة المرور.
.. 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/*
FalseDEFENDER_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\ : عدد صحيح أو قائمة: إذا تم تعيينه، فإنه يتجاوز فترة
الخمول التي بعدها ستُنسى أقفال اسم المستخدم/IP التي تم تعيينها بواسطة
DEFENDER_COOLOFF_TIME. عدد صحيح، سيفسر على أنه عدد من الثواني.
قائمة من الأعداد الصحيحة، ستفسر على أنها عدد من الثواني للمستخدمين مع
فهرس العدد الذي يمثل عدد الأقفال السابقة (حتى حد أقصى) التي حدثت
في آخر 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\ : نص: عنوان URL الخاص بـ redis لـ defender.
[الافتراضي: redis://localhost:6379/0\ ]
(مثال مع كلمة مرور: redis://:mypassword@localhost:6379/0\ )
DEFENDER_REDIS_PASSWORD_QUOTE\ : قيمة منطقية: إذا كان هناك حرف خاص في كلمة مرور redis
(مثل '@')، يمكننا اقتباس كلمة المرور urllib.parse.quote("password!@#")،
وتعيينها إلى True. [الافتراضي: False\ ]
DEFENDER_REDIS_NAME\ : نص: اسم التخزين المؤقت من CACHES في إعدادات Django
(مثل "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\ ]