Maison >développement back-end >Tutoriel Python >Guide pour créer une application de blog complète avec Django en utilisant la méthodologie TDD et PostgreSQL (authentification utilisateur partiellement sécurisée
Bienvenue à tous ! Dans la partie précédente, nous avons établi un processus d'enregistrement sécurisé des utilisateurs pour notre application de blog Django. Cependant, après une inscription réussie, nous avons été redirigés vers la page d’accueil. Ce comportement sera modifié une fois que nous aurons implémenté l'authentification des utilisateurs. L'authentification des utilisateurs garantit que seuls les utilisateurs autorisés peuvent accéder à certaines fonctionnalités et protège les informations sensibles.
Dans cette série, nous construisons une application de blog complète, guidée par le diagramme entité-relation (ERD) suivant. Pour cette fois, nous nous concentrerons sur la mise en place d’un processus d’authentification sécurisé des utilisateurs. Si vous trouvez ce contenu utile, aimez, commentez et abonnez-vous pour rester informé lorsque la prochaine partie sera publiée.
Ceci est un aperçu de l'apparence de notre page de connexion une fois que nous aurons implémenté la fonctionnalité de connexion. Si vous n'avez pas lu les parties précédentes de la série, je vous recommande de le faire, car ce tutoriel est une continuation des étapes précédentes.
D'accord, commençons !!
Django est livré avec une application intégrée appelée contrib.auth, qui simplifie pour nous la gestion de l'authentification des utilisateurs. Vous pouvez vérifier le fichier blog_env/settings.py, sous INSTALLED_APPS, vous verrez que l'authentification est déjà répertoriée.
# 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", ]
L'application d'authentification nous fournit plusieurs vues d'authentification pour gérer la connexion, la déconnexion, le changement de mot de passe, la réinitialisation du mot de passe, etc. Cela signifie que la fonctionnalité d'authentification essentielle, telle que la connexion de l'utilisateur, l'enregistrement et les autorisations, est prête à être utilisée sans avoir besoin pour tout construire à partir de zéro.
Dans ce didacticiel, nous nous concentrerons uniquement sur les vues de connexion et de déconnexion, et couvrirons le reste des vues dans les parties ultérieures de la série.
En suivant notre approche TDD, commençons par créer des tests pour le formulaire de connexion. Puisque nous n'avons pas encore créé de formulaire de connexion, accédez au fichier users/forms.py et créez une nouvelle classe héritant d'AuthenticationForm.
# users/forms.py from django.contrib.auth import AuthenticationForm class LoginForm(AuthenticationForm):
Une fois le formulaire défini, nous pouvons ajouter des cas de test dans users/tests/test_forms.py pour vérifier sa fonctionnalité.
# 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']))
Ces tests couvrent des scénarios tels qu'une connexion réussie avec des informations d'identification valides, un échec de connexion avec des informations d'identification non valides et la gestion appropriée des messages d'erreur.
La classe AuthenticationForm fournit une validation de base par défaut. Cependant, avec notre LoginForm, nous pouvons adapter son comportement et ajouter toutes les règles de validation nécessaires pour répondre à nos exigences spécifiques.
# 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", ]
Nous avons créé un formulaire de connexion personnalisé qui comprend les champs suivants : e-mail, mot de passe et remember_me. La case à cocher Remember_me permet aux utilisateurs de maintenir leur session de connexion au fil des sessions de navigateur.
Étant donné que notre formulaire étend AuthenticationForm, nous avons remplacé certains comportements par défaut :
# users/forms.py from django.contrib.auth import AuthenticationForm class LoginForm(AuthenticationForm):
Comme nous n'avons pas encore la vue pour la connexion, naviguons vers le fichier users/views.py et créons une nouvelle classe héritant du LoginView de l'application d'authentification
# 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']))
Au bas du fichier users/tests/test_views.py, ajoutez ces cas de test
# 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')
Nous devons nous assurer que ces tests échouent à ce stade.
Dans le fichier users/views.py en bas du fichier ajoutez le code ci-dessous :
(.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'...
Dans le code ci-dessus, nous accomplissons ce qui suit :
# -- other code from .forms import CustomUserCreationForm, LoginForm from django.contrib.auth import get_user_model, views # -- other code class CustomLoginView(views.LoginForm):
Pour connecter votre fonctionnalité de connexion personnalisée et permettre aux utilisateurs d'accéder à la page de connexion, nous définirons des modèles d'URL dans le fichier users/urls.py. Ce fichier mappera des URL spécifiques (/log_in/ dans ce cas) aux vues correspondantes (CustomLoginView). De plus, nous inclurons un chemin pour la fonctionnalité de déconnexion à l'aide de LogoutView intégré à Django.
# 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", ]
Tout semble être en ordre, mais nous devrions préciser où rediriger les utilisateurs en cas de connexion et de déconnexion réussies. Pour ce faire, nous utiliserons les paramètres LOGIN_REDIRECT_URL et LOGOUT_REDIRECT_URL. Au bas de votre fichier blog_app/settings.py, ajoutez les lignes suivantes pour rediriger les utilisateurs vers la page d'accueil :
# users/forms.py from django.contrib.auth import AuthenticationForm class LoginForm(AuthenticationForm):
Maintenant que nous avons l'URL de connexion, mettons à jour notre SignUpView dans le fichier users/views.py pour rediriger vers la page de connexion une fois l'inscription réussie.
# 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']))
Nous mettrons également à jour nos SignUpTexts, en particulier test_signup_correct_data(self), pour refléter le nouveau comportement et garantir que nos modifications sont correctement testées.
# 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')
Créez ensuite un fichier users/templates/registration/login.html avec votre éditeur de texte et incluez le code suivant :
(.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'...
Nous ajouterons la fonctionnalité Mot de passe oublié plus tard dans cette série, mais maintenant ce n'est qu'un lien mort.
Maintenant, mettons à jour notre modèle layout.html pour inclure les liens de connexion, d'inscription et de déconnexion.
# -- other code from .forms import CustomUserCreationForm, LoginForm from django.contrib.auth import get_user_model, views # -- other code class CustomLoginView(views.LoginForm):
Dans notre modèle, nous vérifions si l'utilisateur est authentifié. Si l'utilisateur est connecté, nous affichons le lien de déconnexion et le nom complet de l'utilisateur. Sinon, nous affichons les liens de connexion et d'inscription.
Maintenant, faisons tous les tests
# 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)
Maintenant que nous avons configuré la fonctionnalité de connexion et de déconnexion, il est temps de tout tester dans notre navigateur Web. Démarrons le serveur de développement
# 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", ]
Accédez à la page d'inscription et saisissez des informations d'identification valides. Après une inscription réussie, vous devriez être redirigé vers la page de connexion. Entrez les informations utilisateur dans le formulaire de connexion et une fois connecté, cliquez sur le bouton de déconnexion. Vous devriez ensuite être déconnecté et redirigé vers la page d'accueil. Enfin, vérifiez que vous n'êtes plus connecté et que les liens d'inscription et de connexion s'affichent à nouveau.
Tout fonctionne parfaitement, mais j'ai remarqué que lorsqu'un utilisateur est connecté et visite la page d'inscription à l'adresse http://127.0.0.1:8000/users/sign_up/, il a toujours accès au formulaire d'inscription. Idéalement, une fois qu'un utilisateur est connecté, il ne devrait pas pouvoir accéder à la page d'inscription.
Ce comportement peut introduire plusieurs vulnérabilités de sécurité dans notre projet. Pour résoudre ce problème, nous devons mettre à jour SignUpView pour rediriger tout utilisateur connecté vers la page d'accueil.
Mais d'abord, mettons à jour notre LoginTest pour ajouter un nouveau test qui couvre le scénario. Donc, dans le fichier users/tests/test_views.py, ajoutez ce code.
# users/forms.py from django.contrib.auth import AuthenticationForm class LoginForm(AuthenticationForm):
Maintenant, nous pouvons mettre à jour notre 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']))
Dans le code ci-dessus, nous remplaçons la méthode dispatch() de notre SignUpView pour rediriger tout utilisateur déjà connecté et essayant d'accéder à la page d'inscription. Cette redirection utilisera le LOGIN_REDIRECT_URL défini dans notre fichier settings.py, qui dans ce cas pointe vers la page d'accueil.
D'accord! Encore une fois, effectuons tous nos tests pour confirmer que nos mises à jour fonctionnent comme prévu
# 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')
Je sais qu'il y a encore beaucoup à accomplir, mais prenons un moment pour apprécier ce que nous avons accompli jusqu'à présent. Ensemble, nous avons configuré notre environnement de projet, connecté une base de données PostgreSQL et mis en œuvre un système sécurisé d'enregistrement et de connexion des utilisateurs pour notre application de blog Django. Dans la partie suivante, nous aborderons la création d'une page de profil utilisateur, permettant aux utilisateurs de modifier leurs informations et de réinitialiser leur mot de passe ! Restez à l'écoute pour des développements plus passionnants alors que nous poursuivons notre voyage avec l'application de blog Django !
Vos commentaires sont toujours appréciés. Veuillez partager vos réflexions, questions ou suggestions dans les commentaires ci-dessous. N'oubliez pas d'aimer, de laisser un commentaire et de vous abonner pour rester informé des derniers développements !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!