Créer et gérer des utilisateurs sur un site est courant et presque indispensable désormais. Ce TD se contentera d'apporter les bases de la gestion utilisateurs.
Afin que tout le monde réalise ce TD sur une application Flask sans erreurs et correctement configurée:
.env
Comme pour les modèles de base de données ou les modèles de formulaires, les utilisateurs vont utiliser une méthode que l'on connaît déjà.
Une table users
a été créée dans la base de données selon DDL suivant:
CREATE TABLE "users" (
"id" INTEGER NOT NULL,
"prenom" TEXT NOT NULL,
"password" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
Ecrire la classe SQLAlchemy qui représente cette table
Adosser à cette classe une méthode statique (avec le décorateur @staticmethod
), qui permettra de créer facilement un utilisateur ainsi : User.ajout("jean", "mot de passe")
. L'avantage de la méthode statique est qu'elle n'a pas besoin d'un objet User()
existant pour fonctionner. On iteragit donc avec la classe sans que l'objet lui-même existe. On l'appele statique car elle n'est pas liée à une instance, contrairement aux méthodes classiques.
# models/users.py
from werkzeug.security import generate_password_hash
class Users(db.Model):
...
@staticmethod
def ajout(prenom, password):
erreurs = []
if not prenom:
erreurs.append("Le prénom est vide")
if not password or len(password) < 6:
erreurs.append("Le mot de passe est vide ou trop court")
unique = Users.query.filter(
db.or_(Users.prenom == prenom)
).count()
if unique > 0:
erreurs.append("Le prénom existe déjà")
if len(erreurs) > 0:
return False, erreurs
utilisateur = Users(
prenom=prenom,
password=generate_password_hash(password)
)
try:
db.session.add(utilisateur)
db.session.commit()
return True, utilisateur
except Exception as erreur:
return False, [str(erreur)]
Commentaires:
if unique > 0
. Ici la clé n'est que le prénom, il faut bien entendu dans un contexte réel effectuer cette vérification sur une adresse mail par exemplegenerate_password_hash
qui va, grâce à la chaîne de caractères fournit par l'utilisateur et à du sel, générate un hash que l'on pourra stcoker dans la base de données. Le seul moyen de retrouver la valeur du mot de passe sera d'avoir le bon sel et le bon mot de passe.A propos du sel Pour fonctionner, generate_password_hash a besoin d'une variable SECRET_KEY
que l'on définit dans .env
et dans config.py
. Cette clé secrète doit être unique, non devinable et le plus complexe possible. ATtention, ne pas changer sa valeur durant la vie de l'application, sinon tout ce qui aura été hashé par l'application deviendra introuvable.
# .env
SECRET_KEY=duselnondevinableetunique
# config.py
...
class Config():
...
SECRET_KEY = os.environ.get("SECRET_KEY")
Maintenant que le modèle de users
est fait et que sa méthode ajout
a été développé, il faut désormais créer ce qui permettra d'exécuter cette méthode ajout
, c'est à dire une route, qui renverra un formulaire permettant de saisir un prénom et un mot de passe.
AjoutUtilisateur
avec deux champs prenom et passwordtemplates/partials/formulaires/ajout_utilisateur.html
ajout_utilisateur()
: /utilisateurs/ajout
templates/pages/ajout_utilisateur.html
de la routePour gérer les utilisateurs, il existe la librairie Flask-Login
, qui va permettre de savoir, très simplement, pour chaque route, si un utilisateur est connecté ou non, quel est son identifiant, etc. Il permet également de créer des sessions utilisateurs et de poser les cookies nécessaires chez le client de manière à ce que la connexion reste établie. Pour l'installer, pip install flask-login
.
Comme pour Flask avec app
et Flask-SQLAlchemy avec db
, il va falloir instancier Flask-Login dans app.py
.
# app/app.py
from flask_login import LoginManager
...
login = LoginManager(app)
login
va alors proposer 4 méthodes, dont certaines nous seront utiles:
is_authenticated
: retourne True si l'utilisateur est connectéis_active
: retourne True si l'utilisateur est connecté et a un compte valide (non suspendu, validé)is_anonymous
: retourne True si l'utilisateur est anonymeget_id()
: retourne une string qui identifie l'utilisateur connectéPour éviter à avoir à développer ces fonctionnalités de cnotre côté, Flask-Login propose le UserMixin, dont on fera hériter la classe Users
:
# models/users.py
from flask_login import UserMixin
class Users(UserMixin, db.Model):
...
Les seules méthodes de classe que nous avons à développer pour permettre à Flask-Login de fonctionner sont les suivantes:
# models/users.py
from ..app import app, db, login
class Users(UserMixin, db.Model):
...
def get_id(self):
return self.id
@login.user_loader
def get_user_by_id(id):
return Users.query.get(int(id))
Commentaires:
get_id(self)
est une redéfinition de la méthode LoginManager().get_id()
. On indique ici quel est l'attribut de Users qu'il faut considérer comme identifiant unique pour Flask-Loginget_user_by_id()
permet de récupérer un utilisateur à partir de son identifiantOn peut désormais utiliser la gestion des utilisateurs. A nouveau, pour permettre à l'utilisateur de se connecter, il faut faire les étapes habituelles de :
Users.identification
Connexion
connexion
La méthode statique Users.identification(prenom, mot_de_passe)
permettra d'identifier l'utilisateur grâce au prénom et au mot de passe qu'il fournira dans le formulaire de connexion.
# models/users.py
from werkzeug.security import check_password_hash
class Users(UserMixin, db.Model):
...
@staticmethod
def identification(prenom, password):
utilisateur = Users.query.filter(Users.prenom == prenom).first()
if utilisateur and check_password_hash(utilisateur.password, password):
return utilisateur
return None
Commentaires:
il faut commencer par récupérer l'utilisateur via l'identifiant unique (ici le prénom). A ce stade, on en peut pas encore attester que cet utilisateur est vraiment le bon
il faut donc vérifier le mot de passe. Comme le mot de passe n'est pas stocké en clair, on ne peut pas faire Users.query.filter(Users.prenom == prenom, Users.password == password).first()
. Werkzeug offre la méthode check_password_hash
, c'est à dire qu'il va générer à nouveau un hash avec le mot de passe reçu par le formulaire de connexion, puis va le comparer au hash de l'utilisateur que l'on pense être le bon: si les deux hash correspondent, alors l'identification est correcte
Créer un FlaskForm Connexion() avec deux champs prenom et password
Compléter le template du formulaire partials/formulaires/connexion.html
Créer la route de connexion connexion()
: /utilisateurs/connexion
Compléter le template pages/connxeion.html
de la route
Aides:
login_user()
qui permet de connecter un utilisateurlogin.login_view = 'connexion'
permet d'indiquer à Flask-Login quelle est la route de connexion. Si une erreur 401 survient, il renverra à cette route afin que l'utilisateur puisse se connecter et accéder à la ressource demandée.La déconnexion est très simple avec Flask-Login, elle tient en une route et en une bonne configuration de la barre de navigation en HTML.
# routes/users.py
from flask_login import current_user, logout_user
@app.route("/utilisateurs/deconnexion", methods=["POST", "GET"])
def deconnexion():
if current_user.is_authenticated is True:
logout_user()
flash("Vous êtes déconnecté", "info")
return redirect(url_for("accueil"))
Commentaires:
login_user
pour la connexion, il y a un logout_user
pour la déconnexion: Flask-Login se charge de toutcurrent_user
est un proxy vers l'utilisateur courant, il permet d'accéder aux méthodes de Flask-Login comme is_authenticated
ou is_anonymous
Pour une meilleure expérience utilisateur, il convient de cacher le lien de déconnexion quand il n'est pas connecté, et d'afficher le lien de connexion quand il n'est pas connecté.
<!-- partials/conteneur.html -->
{% if current_user.is_authenticated %}
<a class="dropdown-item" href="{{ url_for('deconnexion') }}">Se déconnecter</a>
{% else %}
<a class="dropdown-item" href="{{ url_for('connexion') }}">Se connecter</a>
{% endif %}
Imaginons que l'on souhaite rendre la recherche avancée dans le Factbook payante. Il faudrait alors restreindre l'accès à cette page uniquement aux utilisateurs ayant payé. Pour cela, il ne suffit que d'une ligne de code dans les routes, en utilisant le décorateur @login_required
.
# routes/generales.py
from flask_login import login_required
@app.route("/recherche", methods=['GET', 'POST'])
@app.route("/recherche/<int:page>", methods=['GET', 'POST'])
@login_required
def recherche(page=1):
...
Si un utilisateur non connecté arrive veut accéder à la route /recherche
, il sera alors redirigé vers la page de connexion (indiquée à Flask avec login.login_view = 'connexion'
dans routes/users.py
) : http://localhost:5000/utilisateurs/connexion?next=%2Frecherche
.