Espace membre avec Symfony
Vous souhaitez créer un espace membre dans votre application Symfony ?Pas très compliqué.
Je suppose ici que vous avez déjà votre base de données et une table où vous stockez vos utilisateurs.
Votre UserProvider
Par exemple, vous avez déjà une entitéUser
comme ceci :<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
public function getId()
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
}
Il suffit que vous implémentiez l'interface UserInterface
(use Symfony\Component\Security\Core\User\UserInterface;
- https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/UserInterface.html) qui contient les méthodes getRoles()
, getPassword()
, getSalt()
, getUsername()
et eraseCredentials()
à votre entité User
, comme ceci :
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
public function getId()
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getUsername()
{
return $this->email;
}
public function getSalt()
{
return null;
}
// La méthode getPassword() est déjà implémentée
//public function getPassword()
//{
// return $this->password;
//}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
}
Ensuite, il suffit de rajouter quelques lignes de configuration comme ceci :
# config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: plaintext
# ...
providers:
our_db_provider:
entity:
class: App\Entity\User
property: username
# if you're using multiple entity managers
# manager_name: customer
firewalls:
main:
anonymous: true
form_login:
login_path: login
check_path: login
Configuration un peu plus avancée
Symfony propose d'implémenter l'interfaceAdvancedUserInterface
(https://api.symfony.com/4.1/Symfony/Component/Security/Core/User/AdvancedUserInterface.html) qui permet d'ajouter des conditions afin d’autoriser un utilisateur à se connecter.Cette interface implémente les méthodes
isAccountNonExpired()
, isAccountNonLocked()
, isCredentialsNonExpired()
et isEnabled()
. Ces méthodes, comme leurs noms l'indiquent, permettent de refuser la connexion d'un utilisateur si son compte est respectivement expiré, bloqué, possède des identifiants expiré ou est désactivé.Bien entendu, vous pouvez utilisez ces méthodes pour faire autre chose que ce dont elles ont été prévues pour. Par exemple, vous pouvez vérifier l'ip de l'utilisateur et renvoyer
false
à partir de la méthode isAccountNonExpired()
.Il ne faut plus utiliser cette méthode qui est déprécié à partir de la version 4.1 de Symfony.
A la place, il faut utiliser la méthode des
UserChecker
qui permet de vérifier les mêmes conditions que l'interface AdvancedUserInterface
et bien plus encore.La documentation de Symfony explique très bien comment s'utilise cette méthode : https://symfony.com/doc/current/security/user_checkers.html.
Le formulaire de connexion
Créons un mini formulaire de connexion tout bête :{# templates/login.html.twig #}
<!DOCTYPE html>
<html lang="{{ app.request.locale }}">
<head>
<meta charset="UTF-8">
<title>Connexion</title>
</head>
<body>
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1 id="connexion">Connexion</h1>
</div>
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('login') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
If you want to control the URL the user
is redirected to on success (more details below)
<input type="hidden" name="_target_path" value="/account" />
#}
<button type="submit">login</button>
</form>
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<p>Connecté : Username: {{ app.user.username }}</p>
{% else %}
déconnecté
{% endif %}
</body>
</html>
Le contrôleur
Créons un petit contrôleur qui va permettre de se connecter :<?php
// src/Controller/AuthentificationController.php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class AuthentificationController extends Controller
{
/**
* @Route("/login", name="login")
*/
public function login(Request $request, AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
}
Le formulaire d'inscription
Dans notre entité User, on rajoute le champplainPassword
qui va contenir le mot de passe en brute.On rajoute ceci :
<?php
// src/Entity/User.php
namespace App\Entity;
// ...
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
// ...
/**
* @Assert\NotBlank()
* @Assert\Length(max=4096)
*/
private $plainPassword;
// ...
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
}
Ce qui nous donne en entier :
<?php
// src/Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $email;
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
/**
* @Assert\NotBlank()
* @Assert\Length(max=4096)
*/
private $plainPassword;
public function getId()
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getPlainPassword()
{
return $this->plainPassword;
}
public function setPlainPassword($password)
{
$this->plainPassword = $password;
}
public function getUsername()
{
return $this->email;
}
public function getSalt()
{
return null;
}
// La méthode getPassword() est déjà implémentée
//public function getPassword()
//{
// return $this->password;
//}
public function getRoles()
{
return array('ROLE_USER');
}
public function eraseCredentials()
{
}
}
Le formulaire
<?php
// src/Form/UserType.php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('plainPassword', RepeatedType::class, array(
'type' => PasswordType::class,
'first_options' => array('label' => 'Password'),
'second_options' => array('label' => 'Repeat Password'),
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
Déconnexion
Suffit de rajouter une ligne de configuration comme ceci :# config/packages/security.yaml
security:
# ...
firewalls:
main:
# ...
logout:
path: /logout
target: /
Ainsi qu'un petit contrôleur vide sur la route du path que vous indiquez dans la configuration :
<?php
// src/Controller/AuthentificationController.php
namespace App\Controller;
// ...
class AuthentificationController extends Controller
{
// ...
/**
* @Route("/logout", name="logout")
*/
public function logout()
{
}
}
(Documentation Symfony : https://symfony.com/doc/current/security.html#logging-out)Encoder les mots de passe
Pour encoder manuellement un mot de passe dans un contrôleur : https://symfony.com/doc/current/security/password_encoding.html.Pour encoder les mots de passe qui sont actuellement en brute dans votre base de données : https://symfony.com/doc/current/security.html#c-encoding-the-user-s-password.
Protéger les formulaires de connexion/inscription contre les failles CSRF
La documentation de Symfony est très claire et explicite à ce sujet, tout se trouve ici : https://symfony.com/doc/current/security/csrf.html.Protection contre les longs mots de passe
De même, Symfony explique très bien la problématique dans ce sujet : https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form.Il suffit de rajouter l'assertion
@Assert\Length(max=4096)
sur le champ plainPassword de votre entité User, comme ceci :
<?php
// src/Entity/User.php
namespace App\Entity;
// ...
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface
{
// ...
/**
* @Assert\NotBlank()
* @Assert\Length(max=4096)
*/
private $plainPassword;
// ...
}
Et c'est tout.