通过Django密码尝试次数锁定和基于随机且带有有效期限的URL进行身份验证来解锁

环境

Windows 11 Home (操作系统)
Python 3.10.2 (编程语言)
Django 4.0.2 (Web框架)
venv 可用 (虚拟环境)
(PyPI) (Python包索引)
django-axes==5.31.0 (Django插件版本)
条件:相关文章第一篇已完成。(请使用阅读这篇文章来了解更多详情。)

有关文章

Django 第1節:创建Django自定义用户模型
Django 第2節:在Django初次登录时强制要求修改密码
Django 第3節:对于长时间未更改密码的用户,强制要求修改密码
Django 第4節:生成随机且带有有效期限的URL,经过上级批准后进行账户发行
Django 第5節:通过密码尝试次数锁定和通过随机且带有有效期限的URL进行本人确认解除现场

本文描述的是一个人或事物存在的环境、生活背景或历史背景。

image.png

步骤1:安装django-axes。

image.png

如果看到显示“成功安装”的话,就表示一切顺利。

步骤二:mysite\settings.py

INSTALLED_APPS = [
    ...
    'axes',
]

MIDDLEWARE = [
    ...
    'axes.middleware.AxesMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'axes.backends.AxesBackend',
    'django.contrib.auth.backends.ModelBackend',
]

AXES_FAILURE_LIMIT = 5 # ログイン試行回数
AXES_COOLOFF_TIME = 2 # 自動でロックを解除するまでの時間(単位は時間)
AXES_ONLY_USER_FAILURES = True # Trueにすることでロック対象をIPアドレスではなくユーザ名で判断
AXES_RESET_ON_SUCCESS = True # ログイン成功したらログイン失敗回数をリセットする
AXES_META_PRECEDENCE_ORDER = [
    'HTTP_X_FORWARDED_FOR', # リバースプロキシを使った場合でも利用できるようにする
]

步骤三:迁移

需要进行迁移以创建用于axes的数据库
python manage.py migrate

image.png

步骤4:测试是否被锁定

image.png

我們將設法讓使用者能夠通過表單投稿來解除此鎖定狀態。

步骤5:用户/模型.py

...
class UnlockUser(models.Model):
    """ロック解除申請用のユーザ、何度でも申請できる"""
    class Meta:
        verbose_name = 'ロック解除申請ユーザ'
        verbose_name_plural = 'ロック解除申請ユーザ'

    unlock_uuid = models.UUIDField(default=uuid_lib.uuid4, primary_key=True, editable=False) # 管理ID
    unlock_email = models.EmailField(unique=False) # メールアドレス = これで認証する
    unlock_application_date = models.DateTimeField() # ユーザ申請日時
    unlock_token = models.CharField(max_length=50) # ランダムURL用のトークン
    unlock_expired_seconds = models.IntegerField() # ランダムURLの有効期限(秒)
    unlock_expired_limit = models.DateTimeField()
    unlock_url = models.URLField() # ランダムURL
    unlock_time = models.DateTimeField(blank=True, null=True) # ロック解除日時

步骤6:用户/表单.py

image.png
from django import forms
from .models import UnlockUser

class UnlockForm(forms.ModelForm):
    class Meta:
        model = UnlockUser
        fields = ['unlock_email']
        label = {
            'unlock_email': 'ロック解除するメールアドレス',
        }
        Widgets = {
            'unlock_email': forms.TextInput(attrs={'placeholder': 'xxx.yyy@gmail.com'})
        }

步骤七:用户/views.py

from django.shortcuts import render
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.utils import timezone
from datetime import timedelta
import random
import string
import math
from .forms import UnlockForm

TOKEN_LENGTH = 30 # トークンの文字数
UNLOCK_EXPIRED_SECONDS = 60 * 60 * 2 # トークンの有効期限 = ロック解除メールの有効期限 (秒)

def get_random_chars(char_num=TOKEN_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])

def Unlock(request):
    """アカウントロック解除画面"""
    if request.method == 'POST':
        form = UnlockForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            # 期限付きランダムURLの作成
            timestamp_signer = TimestampSigner()
            context = {}
            context['expired_seconds'] = UNLOCK_EXPIRED_SECONDS
            token = get_random_chars()
            token_signed = timestamp_signer.sign(token)
            context['token_signed'] = token_signed
            expired_limit = timezone.now() + timezone.timedelta(seconds=UNLOCK_EXPIRED_SECONDS)

            obj.unlock_application_date = timezone.now()
            obj.unlock_expired_seconds = UNLOCK_EXPIRED_SECONDS
            obj.unlock_expired_limit = expired_limit
            obj.unlock_url = f'http://127.0.0.1:8000/users/{token_signed}'
            obj.unlock_token = token
            obj.save()

            # メール送信部分(print文で代用)
            user_id = obj.unlock_uuid
            email = form.cleaned_data['unlock_email']

            print(f"""
            {email}さん
            ロック解除申請を受け付けました。
            以下のURLより本人確認をしてください。
            {obj.unlock_url}

            (管理ID:{user_id})
            """)

            params = {
                'title': 'アカウントロック解除申請',
                'message': f'ロック解除申請を受け付けました。解除用のメールをお送りしていますのでご確認ください。(管理ID:{user_id})'
            }
            return render(request, 'users/unlock_requested.html', params)

        else:
            # データが不正ならフォームを再描画する
            context = {'form': form}
            return render(request, 'user/unlock.html', context)

    else:
        """GETの際=フォームを描画"""
        params={
            'title': 'アカウントロック解除',
            'form': UnlockForm(),
        }
        return render(request, 'users/unlock.html', params)

步骤8:创建HTML文档

image.png

在其中创建以下三个文件

image.png
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="{% url 'users:unlock' %}"method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <tr><th><td>
                        <input type="submit" value="送信" class="btn">
                    </td></th></tr>

            </table>
    </form>
</body>
</html>
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <form action="{% url 'users:unlock' %}"method="post">
        {% csrf_token %}
            <table class="table">
                {{ form.as_table }}
                    <p>{{ message }}</p>
            </table>
    </form>
</body>
</html>
<!doctype html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    {% csrf_token %}
        <table class="table">
            {{ form.as_table }}
                <p>{{ message }}</p>
        </table>
</body>
</html>

第9步 開啟 mysite\urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
]

第十步:用户\urls.py

image.png
from django.urls import URLPattern, path
from . import views

# URLパターンを逆引きできるように名前を付ける
app_name = 'users'

urlpatterns = [
    path('unlock/', views.Unlock, name='unlock'), # アカウントロック解除
]

第11步骤:行动测试

image.png
image.png
image.png
image.png

步骤12:更新views.py。

我添加了UnlockDoneViewclass,并添加了import文。全文如下。

from django.shortcuts import render
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.views.generic.base import TemplateView
from django.utils import timezone
from datetime import timedelta
from axes.handlers.proxy import AxesProxyHandler
import random
import string
from .forms import UnlockForm
from .models import UnlockUser

TOKEN_LENGTH = 30 # トークンの文字数
UNLOCK_EXPIRED_SECONDS = 60 * 60 * 2 # トークンの有効期限 = ロック解除メールの有効期限 (秒)

def get_random_chars(char_num=TOKEN_LENGTH):
    """ランダムな文字列を作る"""
    return "".join([random.choice(string.ascii_letters + string.digits) for i in range(char_num)])

def Unlock(request):
    """アカウントロック解除画面"""
    if request.method == 'POST':
        form = UnlockForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)

            # 期限付きランダムURLの作成
            timestamp_signer = TimestampSigner()
            context = {}
            context['expired_seconds'] = UNLOCK_EXPIRED_SECONDS
            token = get_random_chars()
            token_signed = timestamp_signer.sign(token)
            context['token_signed'] = token_signed
            expired_limit = timezone.now() + timezone.timedelta(seconds=UNLOCK_EXPIRED_SECONDS)

            obj.unlock_application_date = timezone.now()
            obj.unlock_expired_seconds = UNLOCK_EXPIRED_SECONDS
            obj.unlock_expired_limit = expired_limit
            obj.unlock_url = f'http://127.0.0.1:8000/users/{token_signed}'
            obj.unlock_token = token
            obj.save()

            # メール送信部分(print文で代用)
            user_id = obj.unlock_uuid
            email = form.cleaned_data['unlock_email']

            print(f"""
            {email}さん
            ロック解除申請を受け付けました。
            以下のURLより本人確認をしてください。
            {obj.unlock_url}

            (管理ID:{user_id})
            """)

            params = {
                'title': 'アカウントロック解除申請',
                'message': f'ロック解除申請を受け付けました。解除用のメールをお送りしていますのでご確認ください。'
            }
            return render(request, 'users/unlock_requested.html', params)

        else:
            # データが不正ならフォームを再描画する
            context = {'form': form}
            return render(request, 'user/unlock.html', context)

    else:
        """GETの際=フォームを描画"""
        params={
            'title': 'アカウントロック解除',
            'form': UnlockForm(),
        }
        return render(request, 'users/unlock.html', params)

class UnlockDoneView(TemplateView):
    template_name = 'users/unlock_done.html'
    timestamp_signer = TimestampSigner()

    def get(self, request, token=None):
        context = {}
        context['expired_seconds'] = UNLOCK_EXPIRED_SECONDS
        
        if token:
            try:
                unsigned_token = self.timestamp_signer.unsign(
                    token,
                    max_age=timedelta(seconds=UNLOCK_EXPIRED_SECONDS)
                )
                queryset = UnlockUser.objects.get(unlock_token=unsigned_token)
                email = queryset.unlock_email

                if queryset.unlock_result == False:
                    context["message"] = '有効なトークンです'

                    # ロック解除(解除成功で戻り値1)
                    result = AxesProxyHandler.reset_attempts(username=email)
                    print('result-----',result)
                    
                    if result == 1:
                        queryset.unlock_time = timezone.now()
                        queryset.unlock_result = True
                        queryset.save()

                        params = {
                            'title': 'アカウントロック解除',
                            'message': 'ロックを解除しました。', 
                        }
                    else:
                        params = {
                            'title': 'アカウントロック解除',
                            'message': '解除できませんでした。ロックされていない可能性があります', 
                        }
                else:
                    # 使用済み
                    context["message"] = '使用済みのトークンです'
                    params = {
                        'title': 'アカウントロック解除',
                        'message': '解除済みです。', 
                    }
            except SignatureExpired:
                # 有効期限切れ
                context["message"] = 'このトークンは有効期限が切れています'
                params = {
                    'title': 'アカウントロック解除',
                    'message': '期限切れです。申請をやり直してください。', 
                }
            except BadSignature:
                # TOKENが不正(URLが間違っている)
                context["message"] = 'トークンが正しくありません'
                params = {
                    'title': 'アカウントロック解除',
                    'message': 'URLが正しくありません。', 
                }

        return render(request, self.template_name, params)

步骤13:用户\urls.py

Django 第1回的续篇全文如下所记。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext, gettext_lazy as _
from .models import User, UnlockUser

@admin.register(User)
class UserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal Info',), {'fields': ('email',)}),
        (_('Permissions',), {'fields': ('is_active', 'is_staff', 'is_superuser',)}),
        (_('Password',), {'fields': ('password_changed', 'password_changed_date',)}),
        (_('Important Dates',), {'fields': ('last_login', 'date_joined',)}),
    )

    list_display = ('username', 'email', 'is_active',)

@admin.register(UnlockUser)
class UnlockUserAdmin(admin.ModelAdmin):
    fieldsets = (
        (_('User',), {'fields': ('unlock_uuid', 'unlock_email',)}),
        (_('Application',), {'fields': ('unlock_application_date', 'unlock_token', 'unlock_expired_seconds', 'unlock_expired_limit', 'unlock_url',)}),
        (_('Result',), {'fields': ('unlock_time', 'unlock_result',)}),
    )

    list_display = ('unlock_application_date', 'unlock_email', 'unlock_result',)
    search_fields = ('unlock_email',)
    ordering = ('-unlock_application_date',)

第14步:操作测试2

image.png

即使在URL上点击解锁申请时没有被锁定,由于Views.py的结果不会返回1,因此无法确认解锁页面。

image.png
image.png
image.png
image.png

第15步:添加管理界面。

image.png
image.png

请你提供更多参考。

“Django-axes” 是一个可以为 Django 添加登录尝试次数限制的工具的使用方法。
向 Django 的用户认证功能中添加登录尝试次数限制。
Django 第1讲:创建 Django 自定义用户模型。
Django 第2讲:在初始登录时强制要求更改密码。
Django 第3讲:强制要求长时间未更改密码的用户进行密码更改。
Django 第4讲:生成随机且带有效期的 URL,通过上级批准来发行帐户。
Django 第5讲:通过密码尝试次数锁定和使用随机且带有效期的 URL 进行本人确认来解除锁定。