使用 Django 使用 TDD 方法和 PostgreSQL 构建完整博客应用程序的指南(部分安全用户身份验证)

Patricia Arquette
发布: 2024-10-18 18:18:03
原创
832 人浏览过

欢迎回来,大家!在上一部分中,我们为 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 下,你会看到 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'...
登录后复制
登录后复制

我们将在本系列后面添加忘记密码功能,但现在它只是一个死链接。
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. 测试浏览器中的一切是否正常

现在我们已经配置了登录和注销功能,是时候测试网络浏览器中的所有内容了。让我们启动开发服务器

# 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 博客应用程序之旅,请继续关注更多令人兴奋的开发!

我们始终重视您的反馈。请在下面的评论中分享您的想法、问题或建议。不要忘记点赞、发表评论并订阅以了解最新动态!

以上是使用 Django 使用 TDD 方法和 PostgreSQL 构建完整博客应用程序的指南(部分安全用户身份验证)的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!