Le composant Security Symfony

Symfony Security gère en une seule config : authentification (qui es-tu ?), autorisation (as-tu le droit ?) et protection (CSRF, brute-force, remember-me…).
composer require symfony/security-bundle
php bin/console make:user
php bin/console make:security:form-login

security.yaml — configuration complète

# config/packages/security.yaml
security:
    password_hashers:
        App\Entity\User:
            algorithm: auto       # bcrypt/argon2id selon PHP

    providers:
        app_user_provider:
            entity:
                class:    App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern:    ^/api
            stateless:  true
            jwt: ~               # LexikJWTAuthenticationBundle

        main:
            lazy:    true
            provider: app_user_provider
            form_login:
                login_path:           app_login
                check_path:           app_login
                enable_csrf:          true
                default_target_path:  app_dashboard
            logout:
                path: app_logout
            remember_me:
                secret:   '%kernel.secret%'
                lifetime: 604800     # 7 jours

    access_control:
        - { path: ^/admin,     roles: ROLE_ADMIN }
        - { path: ^/profile,   roles: ROLE_USER }
        - { path: ^/api/admin, roles: ROLE_ADMIN }

    role_hierarchy:
        ROLE_MODERATOR: ROLE_USER
        ROLE_ADMIN:     [ROLE_MODERATOR, ROLE_USER]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Entité User — UserInterface

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity]
#[UniqueEntity(fields: ['email'], message: 'Cet email est déjà utilisé')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    private string $email = '';

    #[ORM\Column(type: 'json')]
    private array $roles = [];

    #[ORM\Column]
    private string $password = '';

    // Interface UserInterface
    public function getUserIdentifier(): string { return $this->email; }
    public function getPassword(): string        { return $this->password; }

    public function getRoles(): array
    {
        $roles = $this->roles;
        $roles[] = 'ROLE_USER';  // tous les users ont ce rôle
        return array_unique($roles);
    }

    public function eraseCredentials(): void
    {
        // efface le mot de passe en clair si stocké temporairement
    }

    // Getters/setters classiques
    public function getId(): ?int    { return $this->id; }
    public function getEmail(): string { return $this->email; }
    public function setEmail(string $e): static { $this->email = $e; return $this; }
    public function setPassword(string $p): static { $this->password = $p; return $this; }
    public function setRoles(array $r): static { $this->roles = $r; return $this; }
}

Firewalls & Authenticators

<?php
// Authenticator personnalisé (ex: token API header)
namespace App\Security;

use Symfony\Component\HttpFoundation\{Request, Response, JsonResponse};
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\{Passport, SelfValidatingPassport};
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class ApiTokenAuthenticator extends AbstractAuthenticator
{
    public function __construct(private readonly UserRepository $userRepo) {}

    public function supports(Request $request): ?bool
    {
        return $request->headers->has('X-API-Token');
    }

    public function authenticate(Request $request): Passport
    {
        $token = $request->headers->get('X-API-Token');

        return new SelfValidatingPassport(
            new UserBadge($token, function(string $token) {
                $user = $this->userRepo->findOneBy(['apiToken' => $token]);
                if (!$user) throw new AuthenticationException('Token invalide');
                return $user;
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, $token, string $firewallName): ?Response
    {
        return null; // continue la requête
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
    {
        return new JsonResponse(['error' => 'Unauthorized'], 401);
    }
}

Formulaire de connexion

php bin/console make:security:form-login
<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function login(AuthenticationUtils $utils): Response
    {
        if ($this->getUser()) {
            return $this->redirectToRoute('app_dashboard');
        }

        return $this->render('security/login.html.twig', [
            'last_username' => $utils->getLastUsername(),
            'error'         => $utils->getLastAuthenticationError(),
        ]);
    }

    #[Route('/logout', name: 'app_logout')]
    public function logout(): never
    {
        // intercepté par Symfony — ne jamais atteindre ici
        throw new \LogicException('This should not be reached.');
    }
}
{# templates/security/login.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
  <form method="post">
    {% if error %}<div class="alert alert-danger">{{ error.messageKey|trans }}</div>{% endif %}
    <input type="email" name="email" value="{{ last_username }}" required autofocus>
    <input type="password" name="password" required>
    <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
    <label><input type="checkbox" name="_remember_me"> Se souvenir de moi</label>
    <button type="submit">Connexion</button>
  </form>
{% endblock %}

Voters — autorisation fine

php bin/console make:voter PostVoter
<?php
namespace App\Security\Voter;

use App\Entity\{Post, User};
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    const VIEW   = 'view';
    const EDIT   = 'edit';
    const DELETE = 'delete';

    public function __construct(private readonly Security $security) {}

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        /** @var Post $post */
        $post = $subject;
        $user = $token->getUser();

        if ($this->security->isGranted('ROLE_ADMIN')) return true;
        if (!$user instanceof User) return false;

        return match($attribute) {
            self::VIEW   => $post->isPublished() || $post->getAuthor() === $user,
            self::EDIT,
            self::DELETE => $post->getAuthor() === $user,
            default      => false,
        };
    }
}
<?php
// Controller — utilisation du Voter
#[Route('/post/{id}/edit')]
public function edit(Post $post): Response
{
    $this->denyAccessUnlessGranted('edit', $post);
    // … suite du traitement
}
{# Twig — utilisation du Voter #}
{% if is_granted('edit', post) %}
  <a href="{{ path('post_edit', {id: post.id}) }}">Modifier</a>
{% endif %}

JWT Auth avec LexikJWTAuthenticationBundle

composer require lexik/jwt-authentication-bundle
php bin/console lexik:jwt:generate-keypair
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl:  3600    # 1 heure
# security.yaml — firewall API avec JWT
firewalls:
    api:
        pattern:   ^/api
        stateless: true
        jwt: ~
        json_login:
            check_path:              /api/login
            username_path:           email
            password_path:           password
            success_handler:         lexik_jwt_authentication.handler.authentication_success
            failure_handler:         lexik_jwt_authentication.handler.authentication_failure

access_control:
    - { path: ^/api/login, roles: PUBLIC_ACCESS }
    - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
# Obtenir un token JWT
curl -X POST https://api.example.com/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"secret"}'

# Réponse : {"token":"eyJ0eXAiOiJKV1QiLCJhbGci..."}

# Appel authentifié
curl -X GET https://api.example.com/api/me \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGci..."

Rôles & contrôle d'accès

<?php
// Attribut sur la méthode ou le controller
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController { /* … */ }

#[IsGranted('ROLE_USER')]
#[Route('/profile')]
public function profile(): Response { /* … */ }

// IsGranted avec message
#[IsGranted('ROLE_ADMIN', message: 'Accès réservé aux administrateurs', statusCode: 403)]
public function sensitiveAction(): Response { /* … */ }

// Dans le controller manuellement
if (!$this->isGranted('ROLE_ADMIN')) {
    throw $this->createAccessDeniedException();
}
{# Twig #}
{% if is_granted('ROLE_ADMIN') %}
  <a href="{{ path('admin_dashboard') }}">Administration</a>
{% endif %}

{% if app.user %}
  Bonjour {{ app.user.email }} !
  <a href="{{ path('app_logout') }}">Déconnexion</a>
{% else %}
  <a href="{{ path('app_login') }}">Connexion</a>
{% endif %}
▶ Mini-projet SF06 🧠 QCM SF06 Module 07 →