Laravel : gérer les rôles utilisateur / admin avec un middleware

Je voulais faire ce tutoriel depuis longtemps, et le grand jour est maintenant arrivé. Je vais une nouvelle fois vous parler de Laravel, un framework que j’apprécie particulièrement pour sa simplicité et sa robustesse. De plus, avec sa grande popularité, vous trouverez toujours de l’aide sur Internet et la bonne librairie à ajouter via Composer pour résoudre votre besoin.

Dans ce tutoriel sur Laravel, je vais vous apprendre à gérer des rôles basiques à l’aide d’un middleware. Nous allons avoir le rôle des utilisateurs classiques, lorsqu’ils s’inscrivent tout simple sur notre site Web sous Laravel et le rôle administrateur qui a des droits plus étendus. Le but vous l’avez compris, avec l’aide du middleware, c’est que nos utilisateurs classiques ne puissent pas accéder aux routes de l’administrateur. Par contre, l’administrateur pourra bien évidemment se balader sur l’ensemble des pages du site et exécuter toutes les routes comme un utilisateur normal de notre espace membre.

Si vous débutez avec ce framework, et que vous voulez mener à bien ce tutoriel, je vous conseille d’aller voir le précédent article que j’avais fait sur l’authentification dans Laravel, tout est expliqué pour que ce soit le plus simple possible. En effet, pour gérer des rôles, il faut que vous utilisateurs puissent s’inscrire à votre site et pour cela, il faut donc avoir un formulaire d’inscription et ensuite de connexion. D’ailleurs, dans ce nouveau tutoriel, je tenterai de rester là aussi le plus simple possible. On va vraiment s’attacher à comprendre le rôle du middleware pour protéger nos routes entre un utilisateur et un administrateur sans trop de perturbations autour. Vous êtes bien installés ? Alors c’est parti pour gérer les rôles d’un utilisateur et d’un administrateur dans Laravel !

Modification de la base de donnée via les migrations

On commence par créer une première migration dans Laravel qui va s’occuper de stocker nos différents rôles pour les utilisateurs :

php artisan make:migration create_roles_table

Une fois cette migration create_roles_table crée, voici le code qu’il faut ajouter :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('roles');
    }
};

Maintenant que l’on a de quoi stocker nos rôles, je vais ajouter un seed pour remplir cette table avec des valeurs par défaut qui ne devraient pas beaucoup évoluer. Par la suite, vous pourrez bien sûr ajouter autant de rôles que vous voulez et les gérer comme nous allons le voir plus bas. Vous pouvez créer ce seed avec la commande suivante :

php artisan make:seed RolesTableSeeder

J’ai décidé de créer 3 rôles par défaut dans mon application :

  • Le rôle administrateur qui aura accès à toutes les pages de mon application avec par exemple un futur panel d’administration que nous verrons dans un autre tutoriel.
  • Le rôle utilisateur classique que l’on obtient juste après son inscription et qui ne permet pas d’accéder à l’administration du site (heureusement).
  • Le rôle abonné quand dans le cas où une partie de mon site est payante, je pourrais basculer un utilisateur du rôle user à subscriber.

Voici le code de mon seed correspondant :

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class RolesTableSeeder extends Seeder
{
    public function run(): void
    {
        // Insérer les rôles dans la table "roles"
        DB::table('roles')->insert([
            ['name' => 'admin'],
            ['name' => 'user'],
            ['name' => 'subscriber'],
        ]);
    }
}

Pour pouvoir lancer l’exécution de notre seed fraîchement crée il faut que la table existe réellement dans la base de données. Attention la commande suivante va réinitialiser votre base de données et donc la vider totalement ! Sur une application en développement, vous pouvez donc faire un :

php artisan migrate:fresh

Puis lancez ensuite la commande qui va remplir notre table rôle :

php artisan db:seed --class=RolesTableSeeder

Si vous avez bien suivi, vous êtes sûrement en train de vous dire qu’il manque quelque chose ? En effet, il faut qu’on lie cette nouvelle table roles à la table des users ! Étant donné qu’on a déjà la table user par défaut dans Laravel avec le précédent tutoriel sur l’authentification, on ne va pas la toucher et on va plutôt refaire une migration pour ajouter cette relation de roles sur users. Je crée donc une nouvelle migration add_role_id_to_users_table :

php artisan make:migration add_role_id_to_users_table

Le contenu de cette nouvelle table add_role_id_to_users_table est alors le suivant :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->foreignId('role_id')->default(2); // Le rôle par défaut est "user"
            $table->foreign('role_id')->references('id')->on('roles');
        });
    }
    
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropForeign(['role_id']);
            $table->dropColumn('role_id');
        });
    }
};

Cette migration ajoute la colonne « role_id » à la table « users » avec une clé étrangère vers la table « roles ». De plus, le rôle par défaut d’un nouvel utilisateur qui s’inscrit est 2 et donc simple user. Lancez la commande suivante pour mettre à jour votre base de données avec cette nouvelle migration :

php artisan migrate

Ajout et modification de modèle pour nos rôles

De ce précédent article, je vous avais fait une introduction sur les modèles Eloquent de Laravel, c’est le moment de réutiliser ce que l’on a appris ! Je vais donc créer un modèle Role avec la commande suivante :

php artisan make:model Role

Un rôle va servir pour plusieurs utilisateurs, il faut donc créer la bonne relation. On va ajouter le contenu suivant dans notre nouveau model Role :

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function users()
    {
        return $this->hasMany(User::class);
    }
}

Par contre et pour ce tutoriel reste simple, un utilisateur ne peut avoir qu’un seul rôle. Donc attention, il ne faut surtout pas oublier d’ajouter cette partie dans notre model User. Si vous ne le faites pas, alors notre middleware ne pourra pas récupérer et vérifier le rôle de l’utilisateur courant qui est authentifié. Voici ce que vous devez ajouter dans le model User :

public function role()
{
    return $this->belongsTo(Role::class, 'role_id');
}

Normalement, on a maintenant tout ce qu’il nous faut pour la partie modèle dans notre projet Laravel.

Création d’un middleware pour gérer les rôles

On passe maintenant sur notre middleware qui va nous permettre de gérer nos rôles et ensuite d’autoriser ou pas l’accès à une page pour notre utilisateur connecté. Pour commencer, on va donc créer un middleware CheckRole avec la commande suivante :

php artisan make:middleware CheckRole

Je vais ajouter le contenu suivant dans mon fichier app/Http/Middleware/CheckRole.php qui vient d’être créé :

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class CheckRole
{
    public function handle(Request $request, Closure $next, $role): Response
    {
        try {
            $user = auth()->user();

            if (!$user || !$user->role || $user->role->name !== $role) {
                // Journalisation de la tentative d'accès non autorisé
                \Log::warning("Tentative d'accès non autorisé à la route avec le rôle : $role");

                return response()->json(['message' => 'Accès non autorisé.'], 403);
            }
        } catch (\Exception $e) {
            // En cas d'erreur, retournez une réponse 403 générique
            return response()->json(['message' => 'Accès non autorisé.'], 403);
        }

        return $next($request);
    }
}

Quelques explications sur ce code qui va nous permettre de vérifier les rôles pour l’accès aux pages de notre application Laravel :

  • Authentification de l’utilisateur : la fonction commence par récupérer l’utilisateur actuellement authentifié en utilisant auth()->user(). Si l’utilisateur n’est pas authentifié, il n’y a pas d’utilisateur connecté, et la vérification s’arrête ici.
  • Vérification du rôle : si un utilisateur est authentifié, la fonction vérifie ensuite si cet utilisateur a un rôle attribué. La présence d’un rôle est cruciale pour déterminer s’il est autorisé à accéder à la route spécifiée. C’est pour cela que par défaut, on attribue le rôle basique user plus haut dans le tutoriel.
  • Comparaison des rôles : la fonction compare ensuite le nom du rôle de l’utilisateur avec le rôle spécifié en tant qu’argument $role. Si le rôle de l’utilisateur ne correspond pas au rôle requis, l’accès est refusé.
  • Journalisation des tentatives d’accès non autorisées : en cas de tentative d’accès non autorisé, la fonction enregistre un message de journalisation en utilisant Log::warning. Cette journalisation peut être très utile pour suivre les activités suspectes ou pour diagnostiquer les problèmes de sécurité. Il est bien sur possible d’ajouter d’autres informations dans le message et vous pouvez même faire votre propre journal de log personnalisé juste pour cet usage.
  • Réponse d’erreur 403 : si l’accès est refusé, la fonction retourne une réponse JSON avec un message d’erreur indiquant que l’accès n’est pas autorisé et un code de statut HTTP 403 Accès refusé. J’ai fait le plus simple possible dans le cadre de ce tutoriel. Il est bien sur possible de modifier comment l’erreur s’affiche avec par exemple une belle page dédiée ou de renvoyer l’utilisateur vers une page de paiement s’il a tenté d’accéder à une ressource payante.
  • Gestion des exceptions : la fonction est enveloppée dans un bloc try-catch classique, ce qui signifie qu’elle gère les exceptions qui pourraient survenir lors de la vérification du rôle. En cas d’erreur inattendue, elle renvoie une réponse générique 403 pour des raisons de sécurité.

Il ne reste plus qu’une dernière modification pour cette partie, ajouter ce middleware CheckRole dans app/Http/Kernel.php avec le bloc suivant :

    protected $routeMiddleware = [
        'role' => \App\Http\Middleware\CheckRole::class,
    ];

Test du middleware avec les routes de Laravel

Encore une fois le but est de rester le plus simple possible. Je vais donc créer des routes basiques dans le fichier routes/web.php :

// Routes pour les membres (utilisateurs et administrateurs)
Route::middleware(['auth'])->group(function () {
    // Route accessible à tous les membres (utilisateurs et administrateurs)
    Route::get('/testuser', function () {
        $role = auth()->user()->role->name;
        return "Bonjour $role !";
    })->name('testuser');

    // Routes accessibles uniquement aux administrateurs
    Route::middleware(['role:admin'])->group(function () {
        Route::get('/testadmin', function () {
            $role = auth()->user()->role->name;
            return "Wow un $role !";
        })->name('testadmin');
    });
});

Ces petites routes d’exemple permettent de vérifier que le rôle de l’utilisateur est le bon et d’afficher quel rôle il possède lorsque c’est le cas et qu’il est bien autorisé à accéder à la page demandée.

On peut enfin passer à la partie test à proprement parler pour vérifier que nos rôles fonctionnent bien et que notre application sous Laravel est bien protégée. Si j’appelle directement la route /testuser ou /testadmin sans m’inscrire, je suis directement renvoyé vers /login, c’est le comportement normal. Jusqu’ici, tout va bien.

Maintenant, passons aux choses sérieuses. En partant du tutoriel présent au début de l’article sur l’authentification dans Laravel, vous avez normalement une authentification basique qui fonctionne. Je m’inscris sur mon site et j’obtiens alors le rôle par défaut : 2 (user).

J’arrive ensuite sur la page par du dashboard défaut une fois bien connecté :

Je suis inscrit et bien connecté, je tente donc d’aller sur la première route /testuser :

Tout s’est bien passé, je suis bien reconnu en tant que simple user. Si je tente à présent d’appeler /testadmin avec mon simple user, j’ai bien une erreur avec une ligne de plus dans mes logs Laravel car je ne suis pas administrateur :

Je vais modifier la base de données à la main avec une requête SQL pour m’accorder le rôle administrateur sur mon compte :

Je retourne sur la route /testuser et j’ai bien mon nouveau rôle qui s’affiche :

Enfin, si je tente d’accéder à la route /testadmin ça fonctionne aussi et je vois que je bien reconnu en tant qu’administrateur :

Après la lecture de ce tutoriel, vous êtes maintenant capable d’assurer la création et la gestion des rôles sur le framework Laravel. Si vous avez des questions, n’hésitez pas à me le dire dans les commentaires et j’y répondrai avec plaisir !

1 réflexion au sujet de « Laravel : gérer les rôles utilisateur / admin avec un middleware »

Laisser un commentaire