首頁 > 後端開發 > Python教學 > 使用 Django 使用 TDD 方法和 PostgreSQL 建立完整部落格應用程式的指南(部分安全用戶身份驗證)

使用 Django 使用 TDD 方法和 PostgreSQL 建立完整部落格應用程式的指南(部分安全用戶身份驗證)

Patricia Arquette
發布: 2024-10-18 18:18:03
原創
978 人瀏覽過

歡迎回來,大家!在上一部分中,我們為 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
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板