Mise en œuvre de l'authentification asynchrone, protection CSRF, middlewares et pagination sur mesure avec Django

Mise en œuvre de l'authentification asnychrone via AJAX

Pour établir un mécanisme de connexion sans rechargement de page, nous configurons une application Django. Commencez par définir les routes dans le fichier de configuration des URL.

from application import controleurs

urlpatterns = [
    path('administration/', admin.site.urls),
    path('connexion/', controleurs.page_connexion),
    path('tableau-de-bord/', controleurs.page_accueil),
]

Ensuite, ajustez les paramètres pour servir correctement les fichiers statiques.

STATIC_URL = '/statiques/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'statiques'),
)

Développez les vues pour gérer la logique d'authentification et la redirection.

from django.shortcuts import render, HttpResponse
import json

def page_accueil(requete):
    return HttpResponse('Bienvenue sur le tableau de bord')

def page_connexion(requete):
    if requete.method == "POST":
        identifiant = requete.POST.get("identifiant")
        mot_de_passe = requete.POST.get("mot_de_passe")
        resultat = {"etat": 0, 'destination': ''}
        
        if identifiant == "administrateur" and mot_de_passe == "secret123":
            resultat['etat'] = 1
            resultat['destination'] = '/tableau-de-bord/'
            
        return HttpResponse(json.dumps(resultat))

    return render(requete, "formulaire_connexion.html")

Créez le template HTML intégrant le script AJAX pour soumettre les identifiants.


<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Authentification</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
    <form id="form-connexion" class="w-50 mx-auto">
        {% csrf_token %}
        <h2 class="mb-4">Connexion au système</h2>
        <div class="form-group">
            <label for="champ-identifiant">Identifiant</label>
            <input type="text" id="champ-identifiant" class="form-control" name="identifiant" required>
        </div>
        <div class="form-group">
            <label for="champ-mot-de-passe">Mot de passe</label>
            <input type="password" id="champ-mot-de-passe" class="form-control" name="mot_de_passe" required>
        </div>
        <button type="button" id="btn-valider" class="btn btn-primary btn-block">Se connecter</button>
    </form>
</div>

<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
    $('#btn-valider').click(function () {
        $.ajax({
            url: '/connexion/',
            type: 'post',
            data: {
                csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
                identifiant: $('[name="identifiant"]').val(),
                mot_de_passe: $('[name="mot_de_passe"]').val()
            },
            success: function (reponse) {
                let donnees = JSON.parse(reponse);
                if (donnees.etat === 1) {
                    window.location.href = donnees.destination;
                } else {
                    alert('Identifiants incorrects');
                }
            }
        });
    });
</script>
</body>
</html>

Protection contre les attaques CSRF

Lors de l'envoi de requêtes POST asynchrones, Django exige un jeton de sécurité. Plutôt que de l'injecter directement dans le corps de la requête, il est préférable de l'inclure dans les en-têtes HTTP via une configuration globale de jQuery.

function methodeHttpSecurisee(methode) {
    // Les méthodes suivantes ne nécessitent pas de protection CSRF
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(methode));
}

$.ajaxSetup({
    beforeSend: function (xhr, parametres) {
        if (!methodeHttpSecurisee(parametres.type) && !this.crossDomain) {
            // Extraction du jeton depuis les cookies et injection dans l'en-tête
            xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
        }
    }
});

Cette approche garantit que toutes les requêtes AJAX sortantes sont automatiquement sécurisées sans avoir à modifier chaque appel individuellement.

Fonctionnement et création de Middlewares

Un middleware agit comme un intercepteur au niveau du framework, permettant de traiter les requêtes entrantes et les réponses sortantes de manière centralisée. Il est particulièrement utile pour des tâches transversales comme la journalisation ou le contrôle d'accès.

Voici l'implémentation d'un middleware personnalisé pour restreindre l'accès aux routes non autorisées :

from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import redirect, HttpResponse

class ControleAccesMiddleware(MiddlewareMixin):
    chemins_autorises = ['/connexion/', '/statiques/']
    chemins_bloques = ['/zone-restreinte/']

    def process_request(self, requete):
        chemin_cible = requete.path_info
        
        # Blocage immédiat pour les chemins interdits
        if chemin_cible in self.chemins_bloques:
            return HttpResponse("Accès interdit par le système", status=403)
            
        # Autorisation si le chemin est public ou si l'utilisateur est authentifié
        if chemin_cible in self.chemins_autorises or requete.session.get("utilisateur_connecte"):
            return None
            
        # Redirection vers la page de connexion pour les autres cas
        return redirect('/connexion/')

    def process_response(self, requete, reponse):
        # Ajout d'un en-tête de sécurité personnalisé
        reponse['X-Frame-Options'] = 'DENY'
        return reponse

Pour activer ce composant, il doit être enregistré dans la liste MIDDLEWARE du fichier de configuration. L'ordre d'enregistrement est crucial : les requêtes traversent les middlewares de haut en bas, tandis que les réponses remontent de bas en haut.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'application.middlewares.ControleAccesMiddleware', # Middleware personnalisé
]

Développement d'un composant de pagination personnalisé

Bien que Django fournisse un paginateur intégré, la création d'une classe sur mesure offre une flexibilité totale sur le rendu HTML et la logique de découpage des données.

class GenerateurDePagination:
    def __init__(self, page_courante, total_elements, taille_page=10, fenetre_affichage=11):
        self.page_courante = page_courante
        self.total_elements = total_elements
        self.taille_page = taille_page
        self.fenetre_affichage = fenetre_affichage
        self.demi_fenetre = self.fenetre_affichage // 2
        
        self.total_pages, reste = divmod(total_elements, taille_page)
        if reste > 0:
            self.total_pages += 1
            
        # Normalisation de la page courante
        if self.page_courante > self.total_pages:
            self.page_courante = self.total_pages
        elif self.page_courante < 1:
            self.page_courante = 1

    @property
    def indice_debut(self):
        return (self.page_courante - 1) * self.taille_page

    @property
    def indice_fin(self):
        return self.page_courante * self.taille_page

    @property
    def code_html(self):
        debut_fenetre = self.page_courante - self.demi_fenetre
        fin_fenetre = self.page_courante + self.demi_fenetre + 1
        
        if self.page_courante + self.demi_fenetre >= self.total_pages:
            fin_fenetre = self.total_pages + 1
            debut_fenetre = self.total_pages - self.fenetre_affichage + 1
        if self.page_courante <= self.demi_fenetre:
            debut_fenetre = 1
            fin_fenetre = self.fenetre_affichage + 1
            
        elements_html = []
        elements_html.append('<li><a href="?page=1">Premier</a></li>')
        
        page_precedente = self.page_courante - 1
        if page_precedente < 1:
            elements_html.append('<li class="disabled"><a href="#">Précédent</a></li>')
        else:
            elements_html.append(f'<li><a href="?page={page_precedente}">Précédent</a></li>')
            
        for numero in range(debut_fenetre, fin_fenetre):
            if numero == self.page_courante:
                elements_html.append(f'<li class="active"><a href="?page={numero}">{numero}</a></li>')
            else:
                elements_html.append(f'<li><a href="?page={numero}">{numero}</a></li>')
                
        page_suivante = self.page_courante + 1
        if page_suivante > self.total_pages:
            elements_html.append('<li class="disabled"><a href="#">Suivant</a></li>')
        else:
            elements_html.append(f'<li><a href="?page={page_suivante}">Suivant</a></li>')
            
        elements_html.append(f'<li><a href="?page={self.total_pages}">Dernier</a></li>')
        
        return ''.join(elements_html)

Intégration de cette classe dans le contrôleur de la vue :

from django.shortcuts import render
from .pagination import GenerateurDePagination

def liste_utilisateurs(requete):
    # Simulation d'un jeu de données
    base_de_donnees = [{'nom': f'Utilisateur_{i}', 'age': 20 + (i % 40)} for i in range(1, 302)]
    
    try:
        numero_page = int(requete.GET.get('page', 1))
    except ValueError:
        numero_page = 1

    paginateur = GenerateurDePagination(numero_page, len(base_de_donnees))
    donnees_pagees = base_de_donnees[paginateur.indice_debut:paginateur.indice_fin]
    
    contexte = {
        'liste_utilisateurs': donnees_pagees, 
        'navigation': paginateur.code_html
    }
    return render(requete, 'liste_utilisateurs.html', contexte)

Le template se charge ensuite d'itérer sur les données et d'injecter la navigation générée :

<div class="container">
    <table class="table table-striped">
        <thead>
            <tr><th>Nom</th><th>Âge</th></tr>
        </thead>
        <tbody>
            {% for utilisateur in liste_utilisateurs %}
            <tr>
                <td>{{ utilisateur.nom }}</td>
                <td>{{ utilisateur.age }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    
    <nav aria-label="Navigation des pages">
        <ul class="pagination justify-content-center">
            {{ navigation|safe }}
        </ul>
    </nav>
</div>

Étiquettes: Django AJAX jQuery CSRF middleware

Publié le 2 octobre à 11h07