欢迎回来,大家!在上一部分中,我们为 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应用程序为我们提供了多种身份验证视图,用于处理登录、注销、密码更改、密码重置等。这意味着基本的身份验证功能,例如用户登录、注册和权限,无需使用即可使用从头开始构建一切。
在本教程中,我们将仅关注登录和注销视图,并在本系列的后续部分中介绍其余视图。
按照我们的 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']))
我们还将更新我们的 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')
然后使用文本编辑器创建一个 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)
现在我们已经配置了登录和注销功能,是时候测试网络浏览器中的所有内容了。让我们启动开发服务器
# 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中文网其他相关文章!