使用 Django 使用 TDD 方法和 PostgreSQL 构建完整博客应用程序的指南(部分安全用户身份验证)
欢迎回来,大家!在上一部分中,我们为 Django 博客应用程序建立了安全的用户注册流程。然而,注册成功后,我们被重定向到主页。一旦我们实现用户身份验证,这种行为就会被修改。用户身份验证确保只有授权用户才能访问某些功能并保护敏感信息。
在本系列中,我们将在以下实体关系图 (ERD) 的指导下构建一个完整的博客应用程序。这次,我们的重点将是建立安全的用户身份验证流程。如果您觉得此内容有帮助,请点赞、评论和订阅,以便在下一部分发布时保持更新。
这是我们实现登录功能后登录页面外观的预览。如果您还没有阅读本系列的前面部分,我建议您这样做,因为本教程是前面步骤的延续。
好的,我们开始吧!!
Django 附带了一个名为 contrib.auth 的内置应用程序,它简化了我们处理用户身份验证的过程。你可以检查 blog_env/settings.py 文件,在 INSTALLED_APPS 下,你会看到 auth 已经列出了。
# 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", ]
auth应用程序为我们提供了多种身份验证视图,用于处理登录、注销、密码更改、密码重置等。这意味着基本的身份验证功能,例如用户登录、注册和权限,无需使用即可使用从头开始构建一切。
在本教程中,我们将仅关注登录和注销视图,并在本系列的后续部分中介绍其余视图。
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 属性:我们将自定义的LoginForm 指定为form_class 属性,因为我们不再使用默认的AuthenticationForm。
- 重写 form_valid 方法:我们重写 form_valid 方法,该方法在发布有效的表单数据时调用。这允许我们在用户成功登录后实现自定义行为。
- 处理会话过期:如果用户没有选中 Remember_me 框,则会话将在浏览器关闭时自动过期。但是,如果选中 Remember_me 框,会话将持续 settings.py 中定义的持续时间。默认会话长度为两周,但我们可以使用 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']))
我们还将更新我们的 SignUpTexts,特别是 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'...
我们将在本系列后面添加忘记密码功能,但现在它只是一个死链接。
现在,让我们更新 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. 测试浏览器中的一切是否正常
现在我们已经配置了登录和注销功能,是时候测试网络浏览器中的所有内容了。让我们启动开发服务器
# 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 博客应用程序之旅,请继续关注更多令人兴奋的开发!
我们始终重视您的反馈。请在下面的评论中分享您的想法、问题或建议。不要忘记点赞、发表评论并订阅以了解最新动态!
以上是使用 Django 使用 TDD 方法和 PostgreSQL 构建完整博客应用程序的指南(部分安全用户身份验证)的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

Python适合数据科学、Web开发和自动化任务,而C 适用于系统编程、游戏开发和嵌入式系统。 Python以简洁和强大的生态系统着称,C 则以高性能和底层控制能力闻名。

Python在游戏和GUI开发中表现出色。1)游戏开发使用Pygame,提供绘图、音频等功能,适合创建2D游戏。2)GUI开发可选择Tkinter或PyQt,Tkinter简单易用,PyQt功能丰富,适合专业开发。

两小时内可以学到Python的基础知识。1.学习变量和数据类型,2.掌握控制结构如if语句和循环,3.了解函数的定义和使用。这些将帮助你开始编写简单的Python程序。

2小时内可以学会Python的基本编程概念和技能。1.学习变量和数据类型,2.掌握控制流(条件语句和循环),3.理解函数的定义和使用,4.通过简单示例和代码片段快速上手Python编程。

Python更易学且易用,C 则更强大但复杂。1.Python语法简洁,适合初学者,动态类型和自动内存管理使其易用,但可能导致运行时错误。2.C 提供低级控制和高级特性,适合高性能应用,但学习门槛高,需手动管理内存和类型安全。

Python在web开发、数据科学、机器学习、自动化和脚本编写等领域有广泛应用。1)在web开发中,Django和Flask框架简化了开发过程。2)数据科学和机器学习领域,NumPy、Pandas、Scikit-learn和TensorFlow库提供了强大支持。3)自动化和脚本编写方面,Python适用于自动化测试和系统管理等任务。

要在有限的时间内最大化学习Python的效率,可以使用Python的datetime、time和schedule模块。1.datetime模块用于记录和规划学习时间。2.time模块帮助设置学习和休息时间。3.schedule模块自动化安排每周学习任务。

Python在自动化、脚本编写和任务管理中表现出色。1)自动化:通过标准库如os、shutil实现文件备份。2)脚本编写:使用psutil库监控系统资源。3)任务管理:利用schedule库调度任务。Python的易用性和丰富库支持使其在这些领域中成为首选工具。
