ホームページ >バックエンド開発 >Python チュートリアル >TDD 手法と PostgreSQL を使用して Django で完全なブログ アプリを構築するためのガイド (部分的に安全なユーザー認証)
皆さん、おかえりなさい!前のパートでは、Django ブログ アプリケーションの安全なユーザー登録プロセスを確立しました。ただし、登録に成功すると、ホームページにリダイレクトされました。ユーザー認証を実装すると、この動作は変更されます。ユーザー認証により、許可されたユーザーのみが特定の機能にアクセスできるようになり、機密情報が保護されます。
このシリーズでは、次のエンティティ関係図 (ERD) に基づいて、完全なブログ アプリケーションを構築します。今回は、安全なユーザー認証プロセスの設定に焦点を当てます。このコンテンツが役に立ったと思われる場合は、次のパートがリリースされたときに 「いいね!」、コメント、購読して最新情報を入手してください。
これは、ログイン機能を実装した後のログイン ページがどのように見えるかを示すプレビューです。このチュートリアルは前のステップの続きであるため、シリーズの前の部分をまだ読んでいない場合は、読むことをお勧めします。
よし、始めましょう!!
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", ]
認証アプリは、ログイン、ログアウト、パスワード変更、パスワードリセットなどを処理するための複数の認証ビューを提供します。これは、ユーザーのログイン、登録、権限などの重要な認証機能が、必要なく使用できることを意味します。すべてをゼロから構築します。
このチュートリアルでは、ログイン ビューとログアウト ビューのみに焦点を当て、残りのビューについてはシリーズの後半で説明します。
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 を拡張しているため、いくつかのデフォルトの動作をオーバーライドしました。
# users/forms.py from django.contrib.auth import AuthenticationForm class LoginForm(AuthenticationForm):
ログイン用のビューがまだないため、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')
この段階でこれらのテストが失敗していることを確認する必要があります。
ファイルの下部にある 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'...
上記のコードでは、次のことを実現します:
# -- 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')
次に、テキスト エディターで 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'...
このシリーズの後半でパスワードを忘れた場合の機能を追加する予定ですが、現在は単なるデッドリンクです。
次に、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)
ログインおよびログアウト機能の設定が完了したので、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/ の登録ページにアクセスすると、依然として登録フォームにアクセスできることに気付きました。理想的には、ユーザーがログインした後は、サインアップ ページにアクセスできないようにする必要があります。
この動作により、プロジェクトにいくつかのセキュリティ上の脆弱性が生じる可能性があります。これに対処するには、ログインしているユーザーをホームページにリダイレクトするように 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 サイトの他の関連記事を参照してください。