en esta etapa realizaremos lo que se conoce como logueo, o login.
vamos a usar authenfication de tipo Guard que aunque es mas trabajosa de implementar es mas flexible (a comparacion de sistemas built-in como form_login)
vamos a necesitar crear:
- SecurityController.php : tendrá el loginAction
- User.php : esta entidad implementará UserInterface
- LoginFom.php : creará el formulario para el ingreso de username y password
- LoginFormAuthenticator.php : aqui se realizará la obtencion de los datos de login y su validación.
- login.html.twig
y también deberemos configurar:
- services.yml : para registrar como servicio a LoginFormAuthenticator
- security.yml : para indicar que la seguridad será manejada por el service ligada a LoginFormAuthenticator
class SecurityController extends Controller
{
/**
* @Route("/login", name="security_login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
$form = $this->createForm(LoginForm::class, [
'_username' => $lastUsername,
]);
return $this->render(
'security/login.html.twig',
array(
'form' => $form->createView(),
'error' => $error,
)
);
}
}
**********************************************
/**
* User
*
* @ORM\Table(name="user")
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User implements UserInterface
{
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", unique=true)
*/
private $email;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritDoc}
* @see \Symfony\Component\Security\Core\User\UserInterface::getRoles()
*/
public function getRoles() {
// TODO: Auto-generated method stub
return ['ROLE_USER'];
}
public function getUsername() {
// TODO: Auto-generated method stub
return $this->getEmail();
}
public function getEmail() {
return $this->email;
}
public function setEmail($email) {
$this->email = $email;
return $this;
}
}
(la validación se hará luego contra una cadena cualquiera ,p.e: "123123")
el método getUsername, sólo devuelve el nombre de usuario para mostrar, en nuestro caso el nombre de usuario será el email.
**************
class LoginForm extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username')
->add('_password', PasswordType::class)
;
//_username for us, it's an email.
}
}
***************
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator {
private $formFactory;
private $em;
private $router;
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router)
{
$this->formFactory = $formFactory;
$this->em = $em;
$this->router = $router;
}
public function getCredentials(Request $request) {
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if (!$isLoginSubmit) {
// skip authentication
return;
}
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
$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 ($password == '123123') {
return true;
}
return false;
}
protected function getLoginUrl()
{
//nos determina a donde ir si el login falla
return $this->router->generate('security_login');
}
protected function getDefaultSuccessRedirectUrl()
{
//cuando el logueo es exitoso se regresará a la pagina desde la que se solicitó la authentication
//si no existiera esa pagina se retornorá por default a la home
return $this->router->generate('homepage');
}
}
esta clase será creada dentro de la nueva carpeta Security.
este authenticator se comunica con el SecurityController a travez de almacenar cosas en la session
por ejemplo guardando el ultimo username que falló en el logueo.
$request->getSession()->set(
Security::LAST_USERNAME,
$data['_username']
);
**********************************************
{# app/Resources/views/security/login.html.twig #} {# ... you will
probably extend your base template, like base.html.twig #}
{% 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) }}
</form>
</div>
</div>
</div>
{% endblock %}
*********************************
services.yml
services:
app.security.login_form_authenticator:
class: AppBundle\Security\LoginFormAuthenticator
autowire: true
**********************************
security.yml
security:
providers:
our_users:
entity: { class: AppBundle\Entity\User, property: email }
firewalls:
main:
guard:
authenticators:
- app.security.login_form_authenticator
vamos ahora a hacer una verdadera validacion de password, entonces debemos agregar el campo en nuestra entidad User, password será el campo encodeado que se guardará en la tabla y plainPassword
el que guardara temporalmente el valor que se ingrese antes de encodearlo. Este campo no se persiste
/**
* The encoded password
*
* @ORM\Column(type="string")
*/
private $password;
/**
* campo non-persisted que se usará para crear el encoded password.
*
* @var string
*/
private $plainPassword;
public function eraseCredentials() {
$this->plainPassword = null;
/*
* Symfony calls this after logging in,
* and it's just a minor security measure
* to prevent the plain-text password from being accidentally saved anywhere.
*/
}
public function setPlainPassword($plainPassword) {
$this->plainPassword = $plainPassword;
// forces the object to look "dirty" to Doctrine. Avoids
// Doctrine *not* saving this entity, if only plainPassword changes
$this->password = null;
}
+++++++++++++
ahora vamos a implementar un listener que tendrá la tarea de disparar eventos para cuando se intente crear una entidad del tipo User (prePersist) o cuando se la intente actualizar (preUpdated)
este listener luego se lo registrará como servicio
lo crearemos dentro de una nueva carpeta llamada Doctrine:
class HashPasswordListener implements EventSubscriber {
/*
* Symfony comes with a built-in service that's really good at encoding passwords.
* It's called security.password_encoder
* and its class is UserPasswordEncoder
*/
private $passwordEncoder;
public function __construct(UserPasswordEncoder $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function getSubscribedEvents() {
/* These are two event names that Doctrine makes available.
* prePersist is called right before an entity is originally inserted.
* preUpdate is called right before an entity is updated. */
return ['prePersist', 'preUpdate'];
}
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();//obtenemos que entidad esta siendo salvada
/*
* Now, if this is not an instanceof User, just return and do nothing
*/
if (!$entity instanceof User) {
return;
}
$this->encodePassword($entity);
}
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof User) {
return;
}
$this->encodePassword($entity);
// necessary to force the update to see the change
$em = $args->getEntityManager();
$meta = $em->getClassMetadata(get_class($entity));
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
}
private function encodePassword(User $entity)
{
if (!$entity->getPlainPassword()) {
return;
}
$encoded = $this->passwordEncoder->encodePassword(
$entity,
$entity->getPlainPassword()
);
$entity->setPassword($encoded);
}
}
en services.yml
services:
app.doctrine.hash_password_listener:
class: AppBundle\Doctrine\HashPasswordListener
autowire: true
tags:
- { name: doctrine.event_subscriber }
en LoginFormAuthenticator.php
en el constructor agregamos un parametro extra para hacer referencia al encoder del password:
asi lo podemos usar para la validacion en checkCredentials
private $passwordEncoder;
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router, UserPasswordEncoder $passwordEncoder)
{
$this->formFactory = $formFactory;
$this->em = $em;
$this->router = $router;
$this->passwordEncoder = $passwordEncoder;
}
public function checkCredentials($credentials, UserInterface $user) {
$password = $credentials['_password'];
if ($this->passwordEncoder->isPasswordValid($user, $password)) {
return true;
}
return false;
}
finalmente, en security.yml,
indicamos que encoder vamos a usar para encriptar los passwords
security: | |
encoders: | |
AppBundle\Entity\User: bcrypt |