ホームページ >バックエンド開発 >Python チュートリアル >TDD 手法と PostgreSQL を使用して Django で完全なブログ アプリを構築するためのガイド (部分的に安全なユーザー認証)

TDD 手法と PostgreSQL を使用して Django で完全なブログ アプリを構築するためのガイド (部分的に安全なユーザー認証)

Patricia Arquette
Patricia Arquetteオリジナル
2024-10-18 18:18:03945ブラウズ

皆さん、おかえりなさい!前のパートでは、Django ブログ アプリケーションの安全なユーザー登録プロセスを確立しました。ただし、登録に成功すると、ホームページにリダイレクトされました。ユーザー認証を実装すると、この動作は変更されます。ユーザー認証により、許可されたユーザーのみが特定の機能にアクセスできるようになり、機密情報が保護されます。
Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part  Secure User Authentication
このシリーズでは、次のエンティティ関係図 (ERD) に基づいて、完全なブログ アプリケーションを構築します。今回は、安全なユーザー認証プロセスの設定に焦点を当てます。このコンテンツが役に立ったと思われる場合は、次のパートがリリースされたときに 「いいね!」、コメント、購読して最新情報を入手してください
Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part  Secure User Authentication
これは、ログイン機能を実装した後のログイン ページがどのように見えるかを示すプレビューです。このチュートリアルは前のステップの続きであるため、シリーズの前の部分をまだ読んでいない場合は、読むことをお勧めします。

よし、始めましょう!!

Django には contrib.auth という組み込みアプリが付属しており、これによりユーザー認証の処理が簡素化されます。 blog_env/settings.py ファイルを確認すると、INSTALLED_APPS の下に認証がすでにリストされていることがわかります。

# django_project/settings.py
INSTALLED_APPS = [
    # "django.contrib.admin",
    "django.contrib.auth",  # <-- Auth app
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

認証アプリは、ログイン、ログアウト、パスワード変更、パスワードリセットなどを処理するための複数の認証ビューを提供します。これは、ユーザーのログイン、登録、権限などの重要な認証機能が、必要なく使用できることを意味します。すべてをゼロから構築します。

このチュートリアルでは、ログイン ビューとログアウト ビューのみに焦点を当て、残りのビューについてはシリーズの後半で説明します。

1. ログインフォームを作成する

TDD アプローチに従って、ログイン フォームのテストを作成することから始めましょう。ログイン フォームをまだ作成していないため、users/forms.py ファイルに移動し、AuthenticationForm を継承する新しいクラスを作成します。

# users/forms.py
from django.contrib.auth import AuthenticationForm

class LoginForm(AuthenticationForm):


フォームを定義したら、users/tests/test_forms.py にテスト ケースを追加して、その機能を検証できます。

# users/tests/test_forms.py

#   --- other code

class LoginFormTest(TestCase):
  def setUp(self):
    self.user = User.objects.create_user(
      full_name= 'Tester User',
      email= 'tester@gmail.com',
      bio= 'new bio for tester',
      password= 'password12345'
    )

  def test_valid_credentials(self):
    """
    With valid credentials, the form should be valid
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'password12345',
      'remember_me': False
    }

    form = LoginForm(data = credentials)
    self.assertTrue(form.is_valid())

  def test_wrong_credentials(self):
    """
    With wrong credentials, the form should raise Invalid email or password error
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'wrongpassword',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertIn('Invalid email or password', str(form.errors['__all__']))

  def test_credentials_with_empty_email(self):
    """
    Should raise an error when the email field is empty
    """
    credentials = {
      'email': '',
      'password': 'password12345',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['email']))

  def test_credentials_with_empty_password(self):
    """
    Should raise error when the password field is empty
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': '',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['password']))

これらのテストは、有効な認証情報によるログインの成功、無効な認証情報によるログインの失敗、エラー メッセージの適切な処理などのシナリオをカバーします。

AuthenticationForm クラスは、デフォルトでいくつかの基本的な検証を提供します。ただし、LoginForm を使用すると、その動作を調整し、特定の要件を満たすために必要な検証ルールを追加できます。

# django_project/settings.py
INSTALLED_APPS = [
    # "django.contrib.admin",
    "django.contrib.auth",  # <-- Auth app
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

次のフィールドを含むカスタム ログイン フォームを作成しました: メールパスワード、および remember_me。 remember_me チェックボックスを使用すると、ユーザーはブラウザ セッション間でログイン セッションを維持できます。

私たちのフォームは AuthenticationForm を拡張しているため、いくつかのデフォルトの動作をオーバーライドしました。

  • ** __init__ メソッド**: 電子メールベースの認証に合わせて、デフォルトのユーザー名フィールドをフォームから削除しました。
  • clean() メソッド: このメソッドは、電子メール フィールドとパスワード フィールドを検証します。資格情報が有効な場合、Django の組み込み認証メカニズムを使用してユーザーを認証します。
  • confirm_login_allowed() メソッド: この組み込みメソッドは、ログイン前に追加の検証を行う機会を提供します。必要に応じて、このメソッドをオーバーライドしてカスタム チェックを実装できます。 これで、テストに合格するはずです。
# users/forms.py
from django.contrib.auth import AuthenticationForm

class LoginForm(AuthenticationForm):


2. ログインビューを作成します

2.1 ログインビューのテストを作成する

ログイン用のビューがまだないため、users/views.py ファイルに移動して、認証アプリの LoginView を継承する新しいクラスを作成しましょう

# users/tests/test_forms.py

#   --- other code

class LoginFormTest(TestCase):
  def setUp(self):
    self.user = User.objects.create_user(
      full_name= 'Tester User',
      email= 'tester@gmail.com',
      bio= 'new bio for tester',
      password= 'password12345'
    )

  def test_valid_credentials(self):
    """
    With valid credentials, the form should be valid
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'password12345',
      'remember_me': False
    }

    form = LoginForm(data = credentials)
    self.assertTrue(form.is_valid())

  def test_wrong_credentials(self):
    """
    With wrong credentials, the form should raise Invalid email or password error
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'wrongpassword',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertIn('Invalid email or password', str(form.errors['__all__']))

  def test_credentials_with_empty_email(self):
    """
    Should raise an error when the email field is empty
    """
    credentials = {
      'email': '',
      'password': 'password12345',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['email']))

  def test_credentials_with_empty_password(self):
    """
    Should raise error when the password field is empty
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': '',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['password']))

users/tests/test_views.py ファイルの最後に、これらのテスト ケースを追加します

# users/forms.py

# -- other code
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm # new line
from django.contrib.auth import get_user_model, authenticate # new line


# --- other code

class LoginForm(AuthenticationForm):
  email = forms.EmailField(
    required=True,
    widget=forms.EmailInput(attrs={'placeholder': 'Email','class': 'form-control',})
  )
  password = forms.CharField(
    required=True,
    widget=forms.PasswordInput(attrs={
                                'placeholder': 'Password',
                                'class': 'form-control',
                                'data-toggle': 'password',
                                'id': 'password',
                                'name': 'password',
                                })
  )
  remember_me = forms.BooleanField(required=False)

  def __init__(self, *args, **kwargs):
    super(LoginForm, self).__init__(*args, **kwargs)
    # Remove username field

    if 'username' in self.fields:
      del self.fields['username']

  def clean(self):
    email = self.cleaned_data.get('email')
    password = self.cleaned_data.get('password')

    # Authenticate using email and password
    if email and password:
      self.user_cache = authenticate(self.request, email=email, password=password)
      if self.user_cache is None:
        raise forms.ValidationError("Invalid email or password")
      else:
        self.confirm_login_allowed(self.user_cache)
    return self.cleaned_data

  class Meta:
    model = User
    fields = ('email', 'password', 'remember_me')

この段階でこれらのテストが失敗していることを確認する必要があります。

2.2 ログインビューの作成

ファイルの下部にある users/views.py ファイルに、以下のコードを追加します。

(.venv)$ python3 manage.py test users.tests.test_forms
Found 9 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.........
----------------------------------------------------------------------
Ran 9 tests in 3.334s
OK
Destroying test database for alias 'default'...

上記のコードでは、次のことを実現します:

  • form_class 属性を設定: デフォルトの AuthenticationForm はもう使用しないため、カスタム LoginForm を form_class 属性として指定します。
  • form_valid メソッドをオーバーライドします: 有効なフォーム データが投稿されたときに呼び出される form_valid メソッドをオーバーライドします。これにより、ユーザーが正常にログインした後にカスタム動作を実装できるようになります。
  • セッションの有効期限の処理: ユーザーが remember_me ボックスをオンにしない場合、ブラウザを閉じるときにセッションは自動的に期限切れになります。ただし、remember_me ボックスがオンになっている場合、セッションは settings.py で定義されている期間継続します。デフォルトのセッション期間は 2 週間ですが、settings.py の SESSION_COOKIE_AGE 変数を使用してこれを変更できます。たとえば、Cookie の有効期間を 7 日に設定するには、設定に次の行を追加します。
# -- other code 
from .forms import CustomUserCreationForm, LoginForm
from django.contrib.auth import get_user_model, views
# -- other code

class CustomLoginView(views.LoginForm):


カスタム ログイン機能を接続し、ユーザーがログイン ページにアクセスできるようにするには、users/urls.py ファイルで URL パターンを定義します。このファイルは、特定の URL (この場合は /log_in/) を対応するビュー (CustomLoginView) にマップします。さらに、Django の組み込み LogoutView を使用したログアウト機能のパスも含めます。

# django_project/settings.py
INSTALLED_APPS = [
    # "django.contrib.admin",
    "django.contrib.auth",  # <-- Auth app
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

すべて順調に見えますが、ログインとログアウトが成功したときにユーザーをリダイレクトする場所を指定する必要があります。これを行うには、LOGIN_REDIRECT_URL およびLOGOUT_REDIRECT_URL 設定を使用します。 blog_app/settings.py ファイルの最後に、ユーザーをホームページにリダイレクトする次の行を追加します。

# users/forms.py
from django.contrib.auth import AuthenticationForm

class LoginForm(AuthenticationForm):


ログイン URL がわかったので、users/views.py ファイル内の SignUpView を更新して、サインアップが成功したときにログイン ページにリダイレクトしましょう。

# users/tests/test_forms.py

#   --- other code

class LoginFormTest(TestCase):
  def setUp(self):
    self.user = User.objects.create_user(
      full_name= 'Tester User',
      email= 'tester@gmail.com',
      bio= 'new bio for tester',
      password= 'password12345'
    )

  def test_valid_credentials(self):
    """
    With valid credentials, the form should be valid
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'password12345',
      'remember_me': False
    }

    form = LoginForm(data = credentials)
    self.assertTrue(form.is_valid())

  def test_wrong_credentials(self):
    """
    With wrong credentials, the form should raise Invalid email or password error
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'wrongpassword',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertIn('Invalid email or password', str(form.errors['__all__']))

  def test_credentials_with_empty_email(self):
    """
    Should raise an error when the email field is empty
    """
    credentials = {
      'email': '',
      'password': 'password12345',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['email']))

  def test_credentials_with_empty_password(self):
    """
    Should raise error when the password field is empty
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': '',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['password']))

また、SignUpText、特に test_signup_correct_data(self) を更新して、新しい動作を反映し、変更が適切にテストされていることを確認します。

# users/forms.py

# -- other code
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm # new line
from django.contrib.auth import get_user_model, authenticate # new line


# --- other code

class LoginForm(AuthenticationForm):
  email = forms.EmailField(
    required=True,
    widget=forms.EmailInput(attrs={'placeholder': 'Email','class': 'form-control',})
  )
  password = forms.CharField(
    required=True,
    widget=forms.PasswordInput(attrs={
                                'placeholder': 'Password',
                                'class': 'form-control',
                                'data-toggle': 'password',
                                'id': 'password',
                                'name': 'password',
                                })
  )
  remember_me = forms.BooleanField(required=False)

  def __init__(self, *args, **kwargs):
    super(LoginForm, self).__init__(*args, **kwargs)
    # Remove username field

    if 'username' in self.fields:
      del self.fields['username']

  def clean(self):
    email = self.cleaned_data.get('email')
    password = self.cleaned_data.get('password')

    # Authenticate using email and password
    if email and password:
      self.user_cache = authenticate(self.request, email=email, password=password)
      if self.user_cache is None:
        raise forms.ValidationError("Invalid email or password")
      else:
        self.confirm_login_allowed(self.user_cache)
    return self.cleaned_data

  class Meta:
    model = User
    fields = ('email', 'password', 'remember_me')

2.3 ログイン用のテンプレートを作成する

次に、テキスト エディターで users/templates/registration/login.html ファイルを作成し、次のコードを含めます。

(.venv)$ python3 manage.py test users.tests.test_forms
Found 9 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.........
----------------------------------------------------------------------
Ran 9 tests in 3.334s
OK
Destroying test database for alias 'default'...

このシリーズの後半でパスワードを忘れた場合の機能を追加する予定ですが、現在は単なるデッドリンクです。
Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part  Secure User Authentication
次に、layout.html テンプレートを更新して、ログイン、サインアップ、ログアウトのリンクを含めましょう。

# -- other code 
from .forms import CustomUserCreationForm, LoginForm
from django.contrib.auth import get_user_model, views
# -- other code

class CustomLoginView(views.LoginForm):


私たちのテンプレートでは、ユーザーが認証されているかどうかを確認します。ユーザーがログインしている場合は、ログアウト リンクとユーザーのフルネームが表示されます。それ以外の場合は、サインインとサインアップのリンクが表示されます。
それでは、すべてのテストを実行してみましょう

# users/tests/test_views.py

# -- other code

class LoginTests(TestCase):
  def setUp(self):
    User.objects.create_user(
      full_name= 'Tester User',
      email= 'tester@gmail.com',
      bio= 'new bio for tester',
      password= 'password12345'
    )
    self.valid_credentials = {
      'email': 'tester@gmail.com',
      'password': 'password12345',
      'remember_me': False
    }

  def test_login_url(self):
    """User can navigate to the login page"""
    response = self.client.get(reverse('users:login'))
    self.assertEqual(response.status_code, 200)

  def test_login_template(self):
    """Login page render the correct template"""
    response = self.client.get(reverse('users:login'))
    self.assertTemplateUsed(response, template_name='registration/login.html')
    self.assertContains(response, '<a class="btn btn-outline-dark text-white" href="/users/sign_up/">Sign Up</a>')

  def test_login_with_valid_credentials(self):
    """User should be log in when enter valid credentials"""
    response = self.client.post(reverse('users:login'), self.valid_credentials, follow=True)
    self.assertEqual(response.status_code, 200)
    self.assertRedirects(response, reverse('home'))
    self.assertTrue(response.context['user'].is_authenticated)
    self.assertContains(response, '<button type="submit" class="btn btn-danger"><i class="bi bi-door-open-fill"></i> Log out</button>')

  def test_login_with_wrong_credentials(self):
    """Get error message when enter wrong credentials"""
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'wrongpassword',
      'remember_me': False
    }

    response = self.client.post(reverse('users:login'), credentials, follow=True)
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, 'Invalid email or password')
    self.assertFalse(response.context['user'].is_authenticated)

3. ブラウザですべてが正常に動作するかどうかをテストします。

ログインおよびログアウト機能の設定が完了したので、Web ブラウザーですべてをテストします。開発サーバーを起動しましょう

# django_project/settings.py
INSTALLED_APPS = [
    # "django.contrib.admin",
    "django.contrib.auth",  # <-- Auth app
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

登録ページに移動し、有効な認証情報を入力します。登録が成功すると、ログイン ページにリダイレクトされます。ログインフォームにユーザー情報を入力し、ログイン後、ログアウトボタンをクリックしてください。その後、ログアウトされ、ホームページにリダイレクトされます。最後に、ログインしていないこと、およびサインアップとサインインのリンクが再度表示されていることを確認します。
すべてが完璧に機能しますが、ユーザーがログインして http://127.0.0.1:8000/users/sign_up/ の登録ページにアクセスすると、依然として登録フォームにアクセスできることに気付きました。理想的には、ユーザーがログインした後は、サインアップ ページにアクセスできないようにする必要があります。
Guide to Building a Complete Blog App with Django using TDD Methodology and PostgreSQL (Part  Secure User Authentication
この動作により、プロジェクトにいくつかのセキュリティ上の脆弱性が生じる可能性があります。これに対処するには、ログインしているユーザーをホームページにリダイレクトするように SignUpView を更新する必要があります。
まず、LoginTest を更新して、このシナリオをカバーする新しいテストを追加しましょう。したがって、users/tests/test_views.py にこのコードを追加します。

# users/forms.py
from django.contrib.auth import AuthenticationForm

class LoginForm(AuthenticationForm):


これで、SignUpView を更新できます

# users/tests/test_forms.py

#   --- other code

class LoginFormTest(TestCase):
  def setUp(self):
    self.user = User.objects.create_user(
      full_name= 'Tester User',
      email= 'tester@gmail.com',
      bio= 'new bio for tester',
      password= 'password12345'
    )

  def test_valid_credentials(self):
    """
    With valid credentials, the form should be valid
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'password12345',
      'remember_me': False
    }

    form = LoginForm(data = credentials)
    self.assertTrue(form.is_valid())

  def test_wrong_credentials(self):
    """
    With wrong credentials, the form should raise Invalid email or password error
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': 'wrongpassword',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertIn('Invalid email or password', str(form.errors['__all__']))

  def test_credentials_with_empty_email(self):
    """
    Should raise an error when the email field is empty
    """
    credentials = {
      'email': '',
      'password': 'password12345',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['email']))

  def test_credentials_with_empty_password(self):
    """
    Should raise error when the password field is empty
    """
    credentials = {
      'email': 'tester@gmail.com',
      'password': '',
      'remember_me': False
    }
    form = LoginForm(data = credentials)
    self.assertFalse(form.is_valid())
    self.assertIn('This field is required', str(form.errors['password']))

上記のコードでは、SignUpView のdispatch() メソッドをオーバーライドして、すでにログインしていて登録ページにアクセスしようとしているユーザーをリダイレクトします。このリダイレクトでは、settings.py ファイルに設定されている LOGIN_REDIRECT_URL が使用されます。この場合、ホームページを指します。
わかった!もう一度、すべてのテストを実行して、アップデートが期待どおりに機能していることを確認しましょう

# users/forms.py

# -- other code
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm # new line
from django.contrib.auth import get_user_model, authenticate # new line


# --- other code

class LoginForm(AuthenticationForm):
  email = forms.EmailField(
    required=True,
    widget=forms.EmailInput(attrs={'placeholder': 'Email','class': 'form-control',})
  )
  password = forms.CharField(
    required=True,
    widget=forms.PasswordInput(attrs={
                                'placeholder': 'Password',
                                'class': 'form-control',
                                'data-toggle': 'password',
                                'id': 'password',
                                'name': 'password',
                                })
  )
  remember_me = forms.BooleanField(required=False)

  def __init__(self, *args, **kwargs):
    super(LoginForm, self).__init__(*args, **kwargs)
    # Remove username field

    if 'username' in self.fields:
      del self.fields['username']

  def clean(self):
    email = self.cleaned_data.get('email')
    password = self.cleaned_data.get('password')

    # Authenticate using email and password
    if email and password:
      self.user_cache = authenticate(self.request, email=email, password=password)
      if self.user_cache is None:
        raise forms.ValidationError("Invalid email or password")
      else:
        self.confirm_login_allowed(self.user_cache)
    return self.cleaned_data

  class Meta:
    model = User
    fields = ('email', 'password', 'remember_me')

達成すべきことはまだたくさんあると思いますが、これまでに達成したことを少しだけ評価してみましょう。私たちは協力してプロジェクト環境をセットアップし、PostgreSQL データベースに接続し、Django ブログ アプリケーションに安全なユーザー登録とログイン システムを実装しました。次のパートでは、ユーザー プロフィール ページの作成、ユーザーが自分の情報を編集できるようにする方法、およびパスワードのリセットについて詳しく説明します。 Django ブログ アプリの旅を続ける中で、さらにエキサイティングな開発にご期待ください!

あなたのフィードバックは常に大切にされています。ご意見、ご質問、ご提案を以下のコメント欄で共有してください。最新の開発情報を入手するには、「いいね!」、コメントを残す、購読することを忘れないでください!

以上がTDD 手法と PostgreSQL を使用して Django で完全なブログ アプリを構築するためのガイド (部分的に安全なユーザー認証)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。