Create Entity named User
<?php |
|
|
|
namespace AppBundle\Entity; |
|
|
|
use Symfony\Component\Security\Core\User\UserInterface; |
|
use Doctrine\ORM\Mapping as ORM; |
|
|
|
/** |
|
* @ORM\Entity |
|
* @ORM\Table(name="user") |
|
*/ |
|
class User implements UserInterface |
|
{ |
|
/** |
|
* @ORM\Id |
|
* @ORM\GeneratedValue(strategy="AUTO") |
|
* @ORM\Column(type="integer") |
|
*/ |
|
private $id; |
|
|
|
/** |
|
* @ORM\Column(type="string", unique=true) |
|
*/ |
|
private $email; |
|
|
|
/** |
|
* @ORM\Column(type="string") |
|
*/ |
|
private $password; |
|
|
|
private $plainPassword; |
|
|
|
public function getUsername() |
|
{ |
|
return $this->email; |
|
} |
|
|
|
public function getRoles() |
|
{ |
|
return ['ROLE_USER']; |
|
} |
|
|
|
public function getPassword() |
|
{ |
|
return $this->password; |
|
} |
|
|
|
public function getSalt() |
|
{ |
|
} |
|
|
|
public function eraseCredentials() |
|
{ |
|
$this->plainPassword = null; |
|
} |
|
|
|
/** |
|
* @param mixed $email |
|
*/ |
|
public function setEmail($email) |
|
{ |
|
$this->email = $email; |
|
} |
|
|
|
/** |
|
* @param mixed $password |
|
*/ |
|
public function setPassword($password) |
|
{ |
|
$this->password = $password; |
|
} |
|
|
|
/** |
|
* @return mixed |
|
*/ |
|
public function getPlainPassword() |
|
{ |
|
return $this->plainPassword; |
|
} |
|
|
|
/** |
|
* @param mixed $plainPassword |
|
*/ |
|
public function setPlainPassword($plainPassword) |
|
{ |
|
$this->plainPassword = $plainPassword; |
|
//This should be here to set value for password to activate doctrine listeners |
|
$this->password = null; |
|
} |
|
|
|
|
|
|
|
} |
and generate migration:
#php bin/console doctrine:migrations:diff |
|
#php bin/console doctrine:migrations:migrate |
Now we need login form
Create class named SecurityController
<?php |
|
|
|
namespace AppBundle\Controller; |
|
|
|
use AppBundle\Form\LoginForm; |
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; |
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
|
use Symfony\Component\HttpFoundation\Request; |
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; |
|
|
|
class SecurityController extends Controller |
|
{ |
|
|
|
/** |
|
* @Route("/login", name="security_login") |
|
*/ |
|
public function loginAction(Request $request, AuthenticationUtils $authUtils) |
|
{ |
|
// get the login error if there is one |
|
$error = $authUtils->getLastAuthenticationError(); |
|
|
|
// last username entered by the user |
|
$lastUsername = $authUtils->getLastUsername(); |
|
|
|
$form = $this->createForm(LoginForm::class, [ |
|
'_username' => $lastUsername |
|
]); |
|
|
|
|
|
return $this->render('security/login.html.twig', array( |
|
'form' => $form->createView(), |
|
'error' => $error, |
|
)); |
|
} |
|
|
|
/** |
|
* @Route("/logout", name="security_logout") |
|
*/ |
|
public function logoutAction() |
|
{ |
|
throw new \Exception('this should not be reached!'); |
|
} |
|
} |
We need LoginForm class:
<?php |
|
|
|
namespace AppBundle\Form; |
|
|
|
use Symfony\Component\Form\AbstractType; |
|
use Symfony\Component\Form\Extension\Core\Type\PasswordType; |
|
use Symfony\Component\Form\FormBuilderInterface; |
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
|
|
|
class LoginForm extends AbstractType |
|
{ |
|
public function buildForm(FormBuilderInterface $builder, array $options) |
|
{ |
|
$builder |
|
->add('_username') |
|
->add('_password', PasswordType::class) |
|
; |
|
} |
|
|
|
} |
and than create login.html.twig in views/security folder.
{% extends 'base.html.twig' %} |
|
|
|
{% block body %} |
|
<div class="container"> |
|
<div class="row"> |
|
<div class="col-xs-12"> |
|
<h1>Login</h1> |
|
|
|
{% if error %} |
|
<div class="alert alert-danger"> |
|
{{ error.messageKey|trans(error.messageData, 'security') }} |
|
</div> |
|
{% endif %} |
|
|
|
{{ form_start(form) }} |
|
{{ form_row(form._username) }} |
|
{{ form_row(form._password) }} |
|
|
|
<button type="submit" class="btn btn-success">Login <span class="fa fa-lock"></span></button> |
|
{{ form_end(form) }} |
|
</div> |
|
</div> |
|
</div> |
|
{% endblock %} |
Now you can try it in your browser -> /login
It’s time for AUTHENTICATOR
We will use GUARD for our purposes. Create new class named LoginFormAuthenticator in AppBundle/Security folder:
<?php |
|
|
|
namespace AppBundle\Security; |
|
|
|
use AppBundle\Form\LoginForm; |
|
use Doctrine\ORM\EntityManagerInterface; |
|
use Symfony\Component\Form\FormFactoryInterface; |
|
use Symfony\Component\HttpFoundation\Request; |
|
use Symfony\Component\Routing\RouterInterface; |
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; |
|
use Symfony\Component\Security\Core\Exception\AuthenticationException; |
|
use Symfony\Component\Security\Core\Security; |
|
use Symfony\Component\Security\Core\User\UserInterface; |
|
use Symfony\Component\Security\Core\User\UserProviderInterface; |
|
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; |
|
|
|
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator |
|
{ |
|
private $formFactory; |
|
private $em; |
|
private $router; |
|
private $passwordEncoder; |
|
|
|
public function __construct(FormFactoryInterface $formFactory, EntityManagerInterface $em, RouterInterface $router, UserPasswordEncoderInterface $passwordEncoder) |
|
{ |
|
$this->formFactory = $formFactory; |
|
$this->em = $em; |
|
$this->router = $router; |
|
$this->passwordEncoder = $passwordEncoder; |
|
} |
|
|
|
public function getCredentials(Request $request) |
|
{ |
|
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); |
|
if(!$isLoginSubmit){ |
|
return; |
|
} |
|
|
|
$form = $this->formFactory->create(LoginForm::class); |
|
$form->handleRequest($request); |
|
$data = $form->getData(); |
|
|
|
//To prepopulate _username field |
|
$request->getSession()->set( |
|
Security::LAST_USERNAME, |
|
$data['_username'] |
|
); |
|
|
|
return $data; |
|
} |
|
|
|
public function getUser($credentials, UserProviderInterface $userProvider) |
|
{ |
|
$username = $credentials['_username']; |
|
|
|
return $this->em->getRepository('AppBundle:User') |
|
->findOneBy([ |
|
'email' => $username |
|
]); |
|
} |
|
|
|
public function checkCredentials($credentials, UserInterface $user) |
|
{ |
|
$password = $credentials['_password']; |
|
|
|
if($this->passwordEncoder->isPasswordValid($user, $password)){ |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
protected function getLoginUrl() |
|
{ |
|
return $this->router->generate('security_login'); |
|
} |
|
|
|
/** |
|
* Redirect to default page if there is no previous page |
|
* @return mixed |
|
*/ |
|
protected function getDefaultSuccessRedirectUrl() |
|
{ |
|
return $this->router->generate('homepage'); |
|
} |
|
|
|
} |
Next thing we need to do is to register our authenticator as a service. Go to services.yml and add to services:
app.security.login_form_authenticator: |
|
class: AppBundle\Security\LoginFormAuthenticator |
|
autowire: true |
To activate the authenticator go to security.yml and make your firewall to look like that:
firewalls: |
|
# disables authentication for assets and the profiler, adapt it according to your needs |
|
dev: |
|
pattern: ^/(_(profiler|wdt)|css|images|js)/ |
|
security: false |
|
|
|
main: |
|
anonymous: ~ |
|
guard: |
|
authenticators: |
|
- app.security.login_form_authenticator |
|
|
|
logout: |
|
path: /logout |
As soon as we do that the function getCredentials will be called on every request on the whole system.
Setting up USER PROVIDER
User provider task in symfony are simple:
- Load user from the session
- Make sure that user is up-to-date
- Handles “remember me” functionality
- Handles “impersonation”
Open security.yml again and make providers to look like that:
providers: |
|
our_users: |
|
entity: { class: AppBundle\Entity\User, property: email } |
The property part is what we will check on login.
Doctrine Listener – Encoding the user provider
Doctrine Listener is called when any entity is inserted or updated.
Create new directory named AppBundle/xDoctrine and new class named HashPasswordListener
<?php |
|
|
|
namespace AppBundle\xDoctrine; |
|
|
|
use Doctrine\Common\EventSubscriber; |
|
use Doctrine\ORM\Event\LifecycleEventArgs; |
|
use AppBundle\Entity\User; |
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; |
|
|
|
class HashPasswordListener implements EventSubscriber |
|
{ |
|
|
|
private $passwordEncoder; |
|
|
|
|
|
public function __construct(UserPasswordEncoderInterface $passwordEncoder) |
|
{ |
|
$this->passwordEncoder = $passwordEncoder; |
|
} |
|
|
|
public function prePersist(LifecycleEventArgs $args) |
|
{ |
|
$entity = $args->getEntity(); |
|
if(!$entity instanceof User){ |
|
return; |
|
} |
|
|
|
$this->encodePassword($entity); |
|
} |
|
|
|
public function preUpdate(LifecycleEventArgs $args) |
|
{ |
|
$entity = $args->getEntity(); |
|
if(!$entity instanceof User){ |
|
return; |
|
} |
|
|
|
$this->encodePassword($entity); |
|
|
|
//Magic |
|
$em = $args->getEntityManager(); |
|
$meta = $em->getClassMetadata(get_class($entity)); |
|
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity); |
|
} |
|
|
|
public function getSubscribedEvents() |
|
{ |
|
return ['prePersist', 'preUpdate']; |
|
} |
|
|
|
/** |
|
* @param User $entity |
|
*/ |
|
private function encodePassword(User $entity) |
|
{ |
|
if (!$entity->getPlainPassword()) { |
|
return; |
|
} |
|
|
|
$encoded = $this->passwordEncoder->encodePassword( |
|
$entity, |
|
$entity->getPlainPassword() |
|
); |
|
$entity->setPassword($encoded); |
|
} |
|
|
|
} |
To hook it up we need to register it as a service. Open services.yml and add
app.xdoctrine.hash_password_listener: |
|
class: AppBundle\xDoctrine\HashPasswordListener |
|
autowire: true |
|
tags: |
|
- { name: doctrine.event_subscriber } |
And the last thing we need to do is to set encryption type to our entity. Open security.yml and copy this:
security: |
|
|
|
encoders: |
|
AppBundle\Entity\User: bcrypt |