Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
django-defender — 로그인 무차별 대입 공격을 차단하는 간단하고 매우 빠른 Django 재사용 가능 앱 | Kitploit
도구/GitHubGitHub/jazzband/django-defender
Defensive ToolsPassword AttacksWeb SecurityAuthentication
GitHubjazzband/django-defender

django-defender

로그인 무차별 대입 공격을 차단하는 간단하고 매우 빠른 Django 재사용 가능 앱

저장소 보기
1.1k1446개월 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

django-defender

.. 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

로그인 시도를 무차별 대입(brute force)하는 사람들을 차단하는 간단한 Django 재사용 가능 앱입니다. 목표는 로그인 속도를 늦추지 않도록 최대한 빠르게 만드는 것입니다.

각 로그인 시도마다 데이터베이스를 확인하기 위해 데이터베이스에 접근하지 않아도 되도록 캐시를 사용합니다. 첫 번째 버전은 Redis를 기반으로 하지만, 사람들이 자신의 필요에 가장 잘 맞는 백엔드를 사용할 수 있도록 구성 가능하게 만드는 것이 목표입니다.

django-defender를 사용하는 사이트

사이트에서 defender를 사용하고 있다면, 목록에 추가할 PR을 제출해 주세요.

  • https://hub.docker.com
  • https://www.mycosbuilder.com

문서

문서는 Read the Docs에서 확인할 수 있습니다:

https://django-defender.readthedocs.io

기능

  • 모든 로그인 시도를 데이터베이스에 기록

  • IP 주소에 대해 다른 헤더를 사용하는 리버스 프록시 지원

  • 다음 기준으로 속도 제한

    • 사용자 이름
    • IP 주소
  • 블랙리스트에 Redis 사용

  • 구성

    • Redis 서버

      • 호스트
      • 포트
      • 데이터베이스
      • 비밀번호
      • 키 접두사
    • 차단 기간

    • 차단 전 허용되는 잘못된 시도 횟수

  • 95% 코드 커버리지

  • 완전한 문서

  • 로그인 시도를 데이터베이스에 저장하는 기능

  • 로그인 시도 데이터베이스 테이블을 정리하는 관리 명령어

  • 관리자 페이지

    • 차단된 사용자 이름 및 IP 주소 목록
    • 최근 로그인 시도 목록
    • 사람들을 차단 해제하는 기능
  • 사용자 정의 인증 방법에 쉽게 적용 가능

  • 사용자 이름 또는 IP 차단 시 신호(signal) 전송

관리자 페이지


.. 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: 대체 태그

.. 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: 대체 태그

요구 사항

  • Python: 3.8, 3.9, 3.10, 3.11, 3.12, PyPy
  • Django: 3.2, 4.2, 5.0, 5.1, 5.2
  • Redis: 5.x, 6.x, 7.x

설치

코드를 다운로드하고, 방법에 따라 다음 중 하나의 방식으로 setup을 실행합니다.

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

먼저, 이 프로젝트를 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

이를 매일 또는 매주 크론 작업으로 설정하여 테이블 크기를 줄일 수 있습니다.

.. code-block:: bash

매일 오전 12:24에 실행.

24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log

장기 목표

  • 플러그 가능한 백엔드, 그래서 사람들이 Redis 외의 것을 사용할 수 있도록
  • 계정이 차단되면 사용자에게 이메일 보내기
  • 절대 차단하지 않을 사용자 이름 및 IP(관리자 등)에 대한 화이트리스트 추가
  • IP 주소에 대한 영구 블랙리스트 추가
  • 알려진 프록시 IP를 검색하고 해당 IP에서 오는 요청을 차단하지 않음 (좋은 IP가 차단될 가능성 감소)
  • 오래된 (구성 가능한) 로그인 시도를 정리하는 관리 명령어 추가

성능


defender의 목표는 로그인 프로세스를 늦추지 않도록 최대한 빠르게 만드는 것입니다. 목표가 달성되고 있는지 확인하기 위해 애플리케이션을 테스트할 방법이 필요합니다. 가장 좋은 방법은 일반 Django 로그인이 defender 및 django-axes와 비교하여 얼마나 빠른지 테스트하는 것입니다.

일반 Django 로그인은 기준선이 되며, 추가 검사가 없기 때문에 세 가지 방법 중 가장 빠를 것으로 예상됩니다.

defender 로그인은 아마도 Django 로그인보다 느리지만, django-axes 로그인보다는 빠를 것으로 기대합니다. 목표는 일반 원시 로그인과 defender 사이의 차이를 최소화하는 것입니다.

django-axes 로그인 속도는 아마도 세 가지 중 가장 느릴 것입니다. 더 많은 검사를 수행하고 많은 데이터베이스 쿼리를 수행하기 때문입니다.

로그인 속도를 결정하는 가장 좋은 방법은 각 설정으로 애플리케이션에 부하 테스트를 수행하고 각 유형의 로그인 시간을 비교하는 것입니다.

부하 테스트


모든 유형의 로그인을 다루기 위해 부하 테스트에서 하나 이상의 테스트가 필요합니다.

#. 모두 성공: 성공적인 로그인만으로 부하 테스트를 수행합니다.

#. 혼합: 일부 성공, 일부 실패: 성공적인 로그인과 실패를 혼합하여 부하 테스트를 수행하여 실패가 성능에 미치는 영향을 확인합니다.

#. 모두 실패: 모든 로그인이 실패하는 부하 테스트를 수행하고 성능 차이를 확인합니다.

부하 테스트에 사용할 샘플 애플리케이션이 필요하며, 유일한 차이점은 defender, axes 또는 둘 다 로드하지 않는 구성입니다.

호스팅된 부하 테스트 서비스 또는 jmeter와 같은 도구를 사용할 수 있습니다. 어떤 방법을 사용하든 모든 테스트에 일관성을 유지해야 합니다. jmeter를 사용하는 경우, 다른 사람들이 자체 테스트를 실행할 수 있도록 jmeter 구성을 제공해야 합니다.

부하 테스트 결과


결과를 여기에 게시할 것입니다. 각 테스트를 설명하고, 결과를 차트와 함께 보여줄 것입니다.

왜 django-axes가 아닌가

django-axes는 훌륭하지만 모든 것을 데이터베이스에 저장하며, 데이터가 많을 때 병목 현상을 일으킵니다. 인증 요청을 200-300ms까지 느리게 만들 수 있습니다. 이는 일부 사이트에서는 크지 않을 수 있지만, 다른 사이트에서는 너무 깁니다.

이 프로젝트는 django-axes의 포크로 시작했으며, 가능한 한 많은 코드를 사용하고 필요하지 않은 부분을 제거하며 조회 속도를 높여 로그인 속도를 개선했습니다.

django-defender 작동 방식

#. 누군가 로그인을 시도하면, 먼저 현재 차단되었는지 확인합니다. 사용하려는 사용자 이름과 IP 주소를 확인합니다. 차단된 경우 5단계로 이동합니다. 차단되지 않은 경우 2단계로 이동합니다.

#. 차단되지 않았으므로 로그인이 유효한지 확인합니다. 유효하면 6단계로 이동합니다. 유효하지 않으면 3단계로 이동합니다.

#. 로그인 시도가 유효하지 않습니다. 이 시도의 사용자 이름과 IP 주소를 캐시에 추가합니다. 이로 인해 한도를 초과하면 차단 목록에 추가한 후 5단계로 이동합니다. 한도를 초과하지 않으면 4단계로 이동합니다.

#. 로그인이 유효하지 않지만 한도를 초과하지 않았습니다. 다시 시도할 수 있도록 로그인 화면으로 되돌려 보냅니다.

#. 사용자가 차단되었습니다: 차단 페이지로 보내 사용자가 차단되었음을 알리고 차단이 해제될 예상 시간을 알려줍니다.

#. 로그인이 유효합니다. 실패한 로그인 시도를 재설정하고 목적지로 전달합니다.

캐시 백엔드

Defender는 실패한 시도를 저장하기 위해 캐시를 사용합니다.

캐시 키


카운터:

  • prefix:failed:ip:[ip] (count, TTL)
  • prefix:failed:username:[username] (count, TTL)

부울 (존재하면 차단됨):

  • prefix:blocked:ip:[ip] (true, TTL)
  • prefix:blocked:username:[username] (true, TTL)

django-defender 사용자 정의

django-defender를 약간 사용자 정의할 수 있는 몇 가지 옵션이 있습니다. 이들은 settings.py 파일에 정의되어야 합니다.

  • DEFENDER_LOGIN_FAILURE_LIMIT\ : Int: 실패한 로그인에 대한 레코드가 생성되기 전에 허용되는 로그인 시도 횟수. [기본값: 3\ ]

  • DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME\ : Int: 사용자 이름에 대해 실패한 로그인에 대한 레코드가 생성되기 전에 허용되는 로그인 시도 횟수. [기본값: DEFENDER_LOGIN_FAILURE_LIMIT\ ]

  • DEFENDER_LOGIN_FAILURE_LIMIT_IP\ : Int: IP에서 실패한 로그인에 대한 레코드가 생성되기 전에 허용되는 로그인 시도 횟수. [기본값: DEFENDER_LOGIN_FAILURE_LIMIT\ ]

  • DEFENDER_BEHIND_REVERSE_PROXY\ : Boolean: defender가 리버스 프록시 뒤에 있습니까? [기본값: False\ ]

  • DEFENDER_REVERSE_PROXY_HEADER\ : String: 리버스 프록시 IP 주소가 포함된 HTTP 헤더의 이름 [기본값: HTTP_X_FORWARDED_FOR\ ]

  • DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : Boolean: IP와 사용자 이름의 조합을 기반으로 사용자를 잠급니다. 이는 동일한 IP 주소 뒤에서 앱에 접근하는 다른 모든 사용자에 대한 액세스를 거부하는 것을 방지합니다. [기본값: \ ]

DEFENDER_ATTEMPT_COOLOFF_TIME 및 DEFENDER_LOCKOUT_COOLOFF_TIME 사용에 대한 설명


대부분의 사용 사례에서 DEFENDER_COOLOFF_TIME만으로 충분하지만, 높은 보안 설정과 같은 특정 시나리오에서 defender를 사용할 때 개발자는 잠금 고려 중인 유효하지 않은 로그인 시도가 "기억"되는 기간과 실제로 잠금 키가 시스템에서 잠겨 있는 시간을 더 세밀하게 제어하려고 할 수 있습니다. 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으로 정의된 기간 동안 의도적으로 과대평가된 실패한 로그인 횟수를 생성합니다. 이 계산에서 실패한 로그인 시도 사이의 시간을 고려하지 않기 때문에 이는 과대평가로 끝납니다. 이것이 가혹해 보일 수 있지만, 특정 시나리오에서는 느린 공격에 대한 추가 보호가 시스템의 실제 사용자에게 발생할 수 있는 잠재적인 불편을 감수할 가치가 있습니다.

이러한 예 중 하나는 사용자의 민감한 정보(예: 개인 재정 기록)를 보관하는 공개 웹 접근 가능한 웹 애플리케이션일 수 있습니다. 애플리케이션과 그 안의 데이터는 최소한의 중단으로 접근 가능해야 하지만, 보안이 필수적이므로 지연은 어느 정도 용인될 수 있습니다. 이러한 상황에서 우리는 단순히 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]

root@kitploit:~
   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를 settings.py의 다른 모든 인증 방법보다 위에 있는 DEFAULT_AUTHENTICATION_CLASSES에 추가하세요.

다른 인증 방법에 적용하기 :- djangorestframework의 django-rest-auth

defender는 django-rest-framework와 django-rest-auth의 조합으로 통합될 수 있으며, 이를 통해 사용자를 인증할 수 있습니다.

참고


  • https://www.django-rest-framework.org/
  • https://django-rest-auth.readthedocs.io/en/latest/

다음은 사용자 인증을 위해 django-rest-auth 라이브러리를 사용하는 rest_framework.authentication.TokenAuthentication 기반의 샘플 BasicAuthenticationDefender 클래스입니다.

.. 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, )

Get the UserModel

UserModel = get_user_model()

class BasicAuthenticationDefender(serializers.Serializer):

root@kitploit:~
  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

root@kitploit:~
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 시그널

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/*

도구 다운로드
False
  • DEFENDER_DISABLE_IP_LOCKOUT\ : Boolean: True이면 사용자의 IP 주소를 잠그지 않고 사용자 이름만 잠급니다. [기본값: False]

  • DEFENDER_DISABLE_USERNAME_LOCKOUT\ : Boolean: True이면 사용자 이름을 잠그지 않고 IP 주소만 잠급니다. [기본값: False]

  • DEFENDER_COOLOFF_TIME\ : Int: 설정된 경우, 이전 실패한 로그인 시도와 사용자 이름/IP 잠금이 잊혀지는 비활성 기간을 정의합니다. 정수이며 초 단위로 해석됩니다. 0이면 실패한 로그인 시도와 사용자 이름/IP 잠금이 모두 만료되지 않습니다. [기본값: 300\ ]

  • DEFENDER_ATTEMPT_COOLOFF_TIME\ : Int: 설정된 경우, DEFENDER_COOLOFF_TIME에 의해 설정된 이전 실패한 로그인 시도가 잊혀지는 비활성 기간을 재정의합니다. 정수이며 초 단위로 해석됩니다. 0이면 실패한 로그인 시도가 만료되지 않습니다. [기본값: DEFENDER_COOLOFF_TIME\ ]

  • DEFENDER_LOCKOUT_COOLOFF_TIME\ : Int 또는 List: 설정된 경우, DEFENDER_COOLOFF_TIME에 의해 설정된 사용자 이름/IP 잠금이 잊혀지는 비활성 기간을 재정의합니다. 정수이며 초 단위로 해석됩니다. 정수 목록인 경우, 마지막 DEFENDER_ACCESS_ATTEMPT_EXPIRATION 시간 동안 발생한 이전 잠금 횟수(최대값까지)의 인덱스가 초 단위를 나타냅니다. 이 속성이 0 또는 []로 설정되면 사용자 이름/IP 잠금이 만료되지 않습니다. [기본값: DEFENDER_COOLOFF_TIME\ ]

  • DEFENDER_LOCKOUT_TEMPLATE\ : String: [기본값: None\ ] 설정된 경우, 사용자가 잠겼을 때 렌더링할 템플릿을 지정합니다. 템플릿은 다음 컨텍스트 변수를 받습니다:

    • cooloff_time_seconds\ : 초 단위의 쿨오프 시간
    • cooloff_time_minutes\ : 분 단위의 쿨오프 시간
    • failure_limit\ : 차단되기 전의 실패 횟수
  • DEFENDER_USERNAME_FORM_FIELD\ : String: 사용자 사용자 이름을 포함하는 양식 필드의 이름. [기본값: username\ ]

  • DEFENDER_CACHE_PREFIX\ : String: defender 키의 캐시 접두사. [기본값: defender\ ]

  • DEFENDER_LOCKOUT_URL\ : String: 누군가 잠겼을 때 리디렉션할 URL.

  • DEFENDER_REDIS_URL\ : String: defender의 Redis URL. [기본값: redis://localhost:6379/0\ ] (비밀번호 예시: redis://:mypassword@localhost:6379/0\ )

  • DEFENDER_REDIS_PASSWORD_QUOTE\ : Boolean: Redis 비밀번호에 특수 문자(예: '@')가 있는 경우, 비밀번호를 urllib.parse.quote("password!@#")로 인용하고 True로 설정할 수 있습니다. [기본값: False\ ]

  • DEFENDER_REDIS_NAME\ : String: Django 설정의 CACHES에서 캐시 이름 (예: "default"). 설정된 경우, DEFENDER_REDIS_URL은 무시됩니다. [기본값: None\ ]

  • DEFENDER_STORE_ACCESS_ATTEMPTS\ : Boolean: 로그인 시도를 데이터베이스에 저장하려면 True로 설정합니다. False이면 저장되지 않습니다. [기본값: True\ ]

  • DEFENDER_USE_CELERY\ : Boolean: Celery를 사용하여 로그인 시도를 데이터베이스에 저장하려면 True로 설정합니다. False이면 인라인으로 저장됩니다. [기본값: False\ ]

  • DEFENDER_ACCESS_ATTEMPT_EXPIRATION\ : Int: 관리 명령어가 정리하기 전에 접근 시도 레코드를 데이터베이스에 보관하는 시간(시간 단위). [기본값: 24\ ]

  • DEFENDER_GET_USERNAME_FROM_REQUEST_PATH\ : String: 요청에서 사용자 이름에 접근하는 함수의 가져오기 경로. 요청에서 사용자 이름에 접근하고 처리하는 사용자 정의 함수를 사용하려면 여기서 지정할 수 있습니다. [기본값: defender.utils.username_from_request\ ]