Cours Python · Module 5: Flask et applications web

Flask-Login - authentification des utilisateurs

8 min de lecture
Dans cette leçon12

Bon retour ! Darwin est là avec la dernière leçon avant le déploiement - l'authentification des utilisateurs avec Flask-Login !

Analogie Safari : L'authentification, c'est comme des laissez-passer pour les touristes - seules les personnes munies de billets valides peuvent entrer dans le parc, observer les animaux et emprunter les sentiers exclusifs ! Sans authentification, n'importe qui pourrait entrer et modifier ta base de données !

Pourquoi l'authentification ?

Problème : Sans authentification, n'importe qui peut :

  • Ajouter/supprimer des espèces
  • Modifier des données
  • Accéder à des informations sensibles
  • Aucune personnalisation

Solution : Un système de connexion/inscription avec Flask-Login !

Flask-Login - gestion des sessions

Flask-Login est une extension pour gérer les sessions utilisateur :

Connexion/déconnexion des utilisateurs Mémorisation des sessions (cookies) Protection des routes (@login_required) Accès à current_user Fonctionnalité « Se souvenir de moi »

Installation

1pip install Flask-Login

Modèle User

Flask-Login nécessite une classe User avec des méthodes spécifiques. Le plus simple est d'utiliser UserMixin :

1# models.py
2from flask_login import UserMixin
3from werkzeug.security import generate_password_hash, check_password_hash
4
5class User(UserMixin):
6    def __init__(self, id, username, email, password_hash):
7        self.id = id
8        self.username = username
9        self.email = email
10        self.password_hash = password_hash
11
12    def set_password(self, password):
13        """Hacher le mot de passe"""
14        self.password_hash = generate_password_hash(password)
15
16    def check_password(self, password):
17        """Vérifier le mot de passe"""
18        return check_password_hash(self.password_hash, password)
19
20    def __repr__(self):
21        return f'<User {self.username}>'

UserMixin fournit :

  • is_authenticated - est connecté
  • is_active - est actif
  • is_anonymous - est anonyme
  • get_id() - renvoie l'ID sous forme de chaîne

Configuration de Flask-Login

1# app.py
2from flask import Flask
3from flask_login import LoginManager
4
5app = Flask(__name__)
6app.config['SECRET_KEY'] = 'your-secret-key-123'
7
8# Initialiser LoginManager
9login_manager = LoginManager()
10login_manager.init_app(app)
11login_manager.login_view = 'login'  # Redirection pour les utilisateurs non authentifiés
12login_manager.login_message = 'Merci de te connecter pour accéder à cette page.'
13
14# Base de données des utilisateurs (en production : SQLite/PostgreSQL)
15users_db = {}
16next_user_id = 1
17
18# User loader - requis par Flask-Login
19@login_manager.user_loader
20def load_user(user_id):
21    """Charger un utilisateur par son ID"""
22    return users_db.get(int(user_id))

@login_manager.user_loader - Flask-Login appelle cette fonction pour charger un utilisateur depuis la session.

Inscription d'un utilisateur

forms.py - formulaire d'inscription

1from flask_wtf import FlaskForm
2from wtforms import StringField, PasswordField, SubmitField
3from wtforms.validators import DataRequired, Email, EqualTo, Length
4
5class RegistrationForm(FlaskForm):
6    username = StringField('Nom d\'utilisateur',
7                          validators=[DataRequired(), Length(min=4, max=20)])
8
9    email = StringField('Email',
10                       validators=[DataRequired(), Email()])
11
12    password = PasswordField('Mot de passe',
13                            validators=[DataRequired(), Length(min=8)])
14
15    confirm_password = PasswordField('Confirmer le mot de passe',
16                                    validators=[DataRequired(),
17                                               EqualTo('password', message='Les mots de passe doivent correspondre')])
18
19    submit = SubmitField('S\'inscrire')

app.py - route d'inscription

1from flask import render_template, redirect, url_for, flash
2from flask_login import login_user
3from models import User
4from forms import RegistrationForm
5
6@app.route('/register', methods=['GET', 'POST'])
7def register():
8    form = RegistrationForm()
9
10    if form.validate_on_submit():
11        global next_user_id
12
13        # Vérifier si le nom d'utilisateur/email existe déjà
14        for user in users_db.values():
15            if user.username == form.username.data:
16                flash('Nom d\'utilisateur déjà pris !', 'error')
17                return render_template('register.html', form=form)
18
19            if user.email == form.email.data:
20                flash('Email déjà enregistré !', 'error')
21                return render_template('register.html', form=form)
22
23        # Créer un nouvel utilisateur
24        user = User(
25            id=next_user_id,
26            username=form.username.data,
27            email=form.email.data,
28            password_hash=None
29        )
30        user.set_password(form.password.data)  # Hacher le mot de passe
31
32        users_db[next_user_id] = user
33        next_user_id += 1
34
35        flash(f'Compte {user.username} créé ! Tu peux maintenant te connecter.', 'success')
36        return redirect(url_for('login'))
37
38    return render_template('register.html', form=form)

Connexion d'un utilisateur

forms.py - formulaire de connexion

1class LoginForm(FlaskForm):
2    username = StringField('Nom d\'utilisateur', validators=[DataRequired()])
3    password = PasswordField('Mot de passe', validators=[DataRequired()])
4    remember = BooleanField('Se souvenir de moi')
5    submit = SubmitField('Se connecter')

app.py - route de connexion

1from flask_login import login_user, current_user, logout_user
2
3@app.route('/login', methods=['GET', 'POST'])
4def login():
5    # Si déjà connecté, rediriger
6    if current_user.is_authenticated:
7        return redirect(url_for('home'))
8
9    form = LoginForm()
10
11    if form.validate_on_submit():
12        # Trouver l'utilisateur
13        user = None
14        for u in users_db.values():
15            if u.username == form.username.data:
16                user = u
17                break
18
19        # Vérifier le mot de passe
20        if user and user.check_password(form.password.data):
21            login_user(user, remember=form.remember.data)
22            flash(f'Bon retour, {user.username} !', 'success')
23
24            # Rediriger vers next_page ou l'accueil
25            next_page = request.args.get('next')
26            return redirect(next_page) if next_page else redirect(url_for('home'))
27        else:
28            flash('Nom d\'utilisateur ou mot de passe invalide !', 'error')
29
30    return render_template('login.html', form=form)

login_user(user, remember=True) - connecte l'utilisateur et crée une session.

Déconnexion

1@app.route('/logout')
2def logout():
3    logout_user()
4    flash('Tu as été déconnecté.', 'info')
5    return redirect(url_for('home'))

logout_user() - déconnecte l'utilisateur et détruit la session.

@login_required - protéger les routes

Le décorateur @login_required protège les routes contre les utilisateurs non authentifiés :

1from flask_login import login_required, current_user
2
3@app.route('/species/add', methods=['GET', 'POST'])
4@login_required  # Utilisateurs connectés uniquement
5def add_species():
6    form = AddSpeciesForm()
7
8    if form.validate_on_submit():
9        # ... ajouter l'espèce ...
10        flash(f'Espèce ajoutée par {current_user.username} !', 'success')
11        return redirect(url_for('list_species'))
12
13    return render_template('add_species.html', form=form)
14
15@app.route('/profile')
16@login_required
17def profile():
18    return render_template('profile.html', user=current_user)

Si un utilisateur non authentifié visite /species/add :

  1. Il sera redirigé vers /login
  2. Après connexion, il reviendra à /species/add

current_user - l'utilisateur actuel

current_user est un proxy vers l'utilisateur connecté :

1from flask_login import current_user
2
3@app.route('/')
4def home():
5    if current_user.is_authenticated:
6        return render_template('home.html',
7                             username=current_user.username,
8                             species_count=len(species_db))
9    else:
10        return render_template('home.html',
11                             username=None,
12                             species_count=len(species_db))

Dans les templates

1{% if current_user.is_authenticated %}
2    <p>Connecté en tant que : <strong>{{ current_user.username }}</strong></p>
3    <a href="{{ url_for('logout') }}">Se déconnecter</a>
4{% else %}
5    <a href="{{ url_for('login') }}">Se connecter</a> |
6    <a href="{{ url_for('register') }}">S'inscrire</a>
7{% endif %}

Hachage des mots de passe - sécurité

Ne stocke JAMAIS les mots de passe en clair ! Utilise le hachage :

1from werkzeug.security import generate_password_hash, check_password_hash
2
3# Inscription - hacher le mot de passe
4password_hash = generate_password_hash('SecretPassword123')
5# Sortie : 'pbkdf2:sha256:260000$...'
6
7# Connexion - vérifier le mot de passe
8is_correct = check_password_hash(password_hash, 'SecretPassword123')
9# Sortie : True

generate_password_hash() utilise PBKDF2 avec un sel et de multiples itérations - sécurisé !

Application complète avec authentification

models.py

1from flask_login import UserMixin
2from werkzeug.security import generate_password_hash, check_password_hash
3
4class User(UserMixin):
5    def __init__(self, id, username, email, password_hash):
6        self.id = id
7        self.username = username
8        self.email = email
9        self.password_hash = password_hash
10
11    def set_password(self, password):
12        self.password_hash = generate_password_hash(password)
13
14    def check_password(self, password):
15        return check_password_hash(self.password_hash, password)

forms.py

1from flask_wtf import FlaskForm
2from wtforms import StringField, PasswordField, BooleanField, SubmitField
3from wtforms.validators import DataRequired, Email, EqualTo, Length
4
5class RegistrationForm(FlaskForm):
6    username = StringField('Nom d\'utilisateur', validators=[DataRequired(), Length(min=4, max=20)])
7    email = StringField('Email', validators=[DataRequired(), Email()])
8    password = PasswordField('Mot de passe', validators=[DataRequired(), Length(min=8)])
9    confirm_password = PasswordField('Confirmer le mot de passe',
10                                    validators=[DataRequired(), EqualTo('password')])
11    submit = SubmitField('S\'inscrire')
12
13class LoginForm(FlaskForm):
14    username = StringField('Nom d\'utilisateur', validators=[DataRequired()])
15    password = PasswordField('Mot de passe', validators=[DataRequired()])
16    remember = BooleanField('Se souvenir de moi')
17    submit = SubmitField('Se connecter')

app.py

1from flask import Flask, render_template, redirect, url_for, flash, request
2from flask_login import LoginManager, login_user, logout_user, login_required, current_user
3from models import User
4from forms import RegistrationForm, LoginForm, AddSpeciesForm
5
6app = Flask(__name__)
7app.config['SECRET_KEY'] = 'your-secret-key-123'
8
9# Flask-Login
10login_manager = LoginManager()
11login_manager.init_app(app)
12login_manager.login_view = 'login'
13
14# Bases de données (en mémoire)
15users_db = {}
16next_user_id = 1
17
18species_db = {}
19next_species_id = 1
20
21@login_manager.user_loader
22def load_user(user_id):
23    return users_db.get(int(user_id))
24
25@app.route('/')
26def home():
27    return render_template('home.html',
28                         species_count=len(species_db))
29
30@app.route('/register', methods=['GET', 'POST'])
31def register():
32    if current_user.is_authenticated:
33        return redirect(url_for('home'))
34
35    form = RegistrationForm()
36
37    if form.validate_on_submit():
38        global next_user_id
39
40        # Vérifier les doublons
41        for user in users_db.values():
42            if user.username == form.username.data:
43                flash('Nom d\'utilisateur déjà pris !', 'error')
44                return render_template('register.html', form=form)
45
46        # Créer l'utilisateur
47        user = User(next_user_id, form.username.data, form.email.data, None)
48        user.set_password(form.password.data)
49        users_db[next_user_id] = user
50        next_user_id += 1
51
52        flash(f'Compte créé ! Connecte-toi.', 'success')
53        return redirect(url_for('login'))
54
55    return render_template('register.html', form=form)
56
57@app.route('/login', methods=['GET', 'POST'])
58def login():
59    if current_user.is_authenticated:
60        return redirect(url_for('home'))
61
62    form = LoginForm()
63
64    if form.validate_on_submit():
65        user = None
66        for u in users_db.values():
67            if u.username == form.username.data:
68                user = u
69                break
70
71        if user and user.check_password(form.password.data):
72            login_user(user, remember=form.remember.data)
73            flash(f'Bienvenue, {user.username} !', 'success')
74
75            next_page = request.args.get('next')
76            return redirect(next_page) if next_page else redirect(url_for('home'))
77        else:
78            flash('Nom d\'utilisateur ou mot de passe invalide !', 'error')
79
80    return render_template('login.html', form=form)
81
82@app.route('/logout')
83@login_required
84def logout():
85    logout_user()
86    flash('Déconnecté.', 'info')
87    return redirect(url_for('home'))
88
89@app.route('/species/add', methods=['GET', 'POST'])
90@login_required
91def add_species():
92    form = AddSpeciesForm()
93
94    if form.validate_on_submit():
95        global next_species_id
96
97        new_species = {
98            'id': next_species_id,
99            'name': form.name.data,
100            'added_by': current_user.username
101        }
102
103        species_db[next_species_id] = new_species
104        next_species_id += 1
105
106        flash(f'Espèce ajoutée par {current_user.username} !', 'success')
107        return redirect(url_for('list_species'))
108
109    return render_template('add_species.html', form=form)
110
111if __name__ == '__main__':
112    app.run(debug=True)

templates/base.html - navigation avec auth

1<nav>
2    <a href="{{ url_for('home') }}">Accueil</a>
3    <a href="{{ url_for('list_species') }}">Espèces</a>
4
5    {% if current_user.is_authenticated %}
6        <a href="{{ url_for('add_species') }}">Ajouter une espèce</a>
7        <a href="{{ url_for('profile') }}">Profil ({{ current_user.username }})</a>
8        <a href="{{ url_for('logout') }}">Se déconnecter</a>
9    {% else %}
10        <a href="{{ url_for('login') }}">Se connecter</a>
11        <a href="{{ url_for('register') }}">S'inscrire</a>
12    {% endif %}
13</nav>

Résumé

Dans cette leçon tu as appris :

  • Pourquoi l'authentification dans les applications web
  • Flask-Login - gestion des sessions
  • Le modèle User avec UserMixin
  • L'inscription des utilisateurs
  • La connexion/déconnexion
  • @login_required - protection des routes
  • current_user - accès à l'utilisateur connecté
  • Le hachage des mots de passe (werkzeug.security)
  • L'application complète Safari Database avec authentification

Analogie Safari finale : Flask-Login est comme un système de laissez-passer de safari - seuls les touristes munis de billets valides (current_user.is_authenticated) peuvent entrer dans la zone protégée des sentiers (@login_required), et chaque touriste a son identifiant unique (user.id) et son mot de passe (haché) !

Prochaine leçon : Darwin te montrera le déploiement - publier ton application Flask sur Internet (Gunicorn, Render, Railway) !

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que représente l'objet current_user dans Flask-Login ?

  2. 2. Comment installe-t-on Flask-Login ?

Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Range la définition d'un modèle User avec Flask-Login :

  • Clique dans l'ordre

    Range l'appel à la fonction login_user :

  • Éditeur de code

    Écris une vue /dashboard avec le décorateur @login_required qui affiche un message de bienvenue pour current_user.

Articles utiles