web-dev-qa-db-fra.com

Symfony2 AJAX Login

J'ai un exemple où j'essaie de créer une connexion AJAX en utilisant Symfony2 et FOSUserBundle. Je configure mes propres success_handler Et failure_handler Sous form_login dans mon fichier security.yml.

Voici la classe:

class AjaxAuthenticationListener implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
{  
    /**
     * This is called when an interactive authentication attempt succeeds. This
     * is called by authentication listeners inheriting from
     * AbstractAuthenticationListener.
     *
     * @see \Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener
     * @param Request        $request
     * @param TokenInterface $token
     * @return Response the response to return
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        if ($request->isXmlHttpRequest()) {
            $result = array('success' => true);
            $response = new Response(json_encode($result));
            $response->headers->set('Content-Type', 'application/json');
            return $response;
        }
    }

    /**
     * This is called when an interactive authentication attempt fails. This is
     * called by authentication listeners inheriting from
     * AbstractAuthenticationListener.
     *
     * @param Request                 $request
     * @param AuthenticationException $exception    
     * @return Response the response to return
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        if ($request->isXmlHttpRequest()) {
            $result = array('success' => false, 'message' => $exception->getMessage());
            $response = new Response(json_encode($result));
            $response->headers->set('Content-Type', 'application/json');
            return $response;
        }
    }
}

Cela fonctionne très bien pour gérer les tentatives de connexion réussies et échouées AJAX. Cependant, lorsqu'il est activé - je ne peux pas me connecter via le formulaire standard POST (non AJAX) .Je reçois l'erreur suivante:

Catchable Fatal Error: Argument 1 passed to Symfony\Component\HttpKernel\Event\GetResponseEvent::setResponse() must be an instance of Symfony\Component\HttpFoundation\Response, null given

Je voudrais que mes remplacements onAuthenticationSuccess et onAuthenticationFailure ne soient exécutés que pour XmlHttpRequests (demandes AJAX) et simplement remettent l'exécution au gestionnaire d'origine sinon.

Y a-t-il un moyen de faire cela?

TL; DR Je veux AJAX les tentatives de connexion demandées pour renvoyer une réponse JSON pour le succès et l'échec, mais je veux que cela n'affecte pas la connexion standard via le formulaire POST.

43
leek

David's réponse est bon, mais il manque un peu de détails pour les newbs - c'est donc pour remplir les blancs.

En plus de créer l'AuthenticationHandler, vous devrez le configurer en tant que service à l'aide de la configuration de service dans le bundle où vous avez créé le gestionnaire. La génération de bundle par défaut crée un fichier xml, mais je préfère yml. Voici un exemple de fichier services.yml:

#src/Vendor/BundleName/Resources/config/services.yml

parameters:
    vendor_security.authentication_handler: Vendor\BundleName\Handler\AuthenticationHandler

services:
    authentication_handler:
        class:  %vendor_security.authentication_handler%
        arguments:  [@router]
        tags:
            - { name: 'monolog.logger', channel: 'security' }

Vous devez modifier l'extension de bundle DependencyInjection pour utiliser yml au lieu de xml comme ceci:

#src/Vendor/BundleName/DependencyInjection/BundleExtension.php

$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');

Ensuite, dans la configuration de sécurité de votre application, vous définissez les références au service authentication_handler que vous venez de définir:

# app/config/security.yml

security:
    firewalls:
        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path:  /login
                check_path:  /login_check
                success_handler: authentication_handler
                failure_handler: authentication_handler
50
semateos
namespace YourVendor\UserBundle\Handler;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class AuthenticationHandler
implements AuthenticationSuccessHandlerInterface,
           AuthenticationFailureHandlerInterface
{
    private $router;

    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        if ($request->isXmlHttpRequest()) {
            // Handle XHR here
        } else {
            // If the user tried to access a protected resource and was forces to login
            // redirect him back to that resource
            if ($targetPath = $request->getSession()->get('_security.target_path')) {
                $url = $targetPath;
            } else {
                // Otherwise, redirect him to wherever you want
                $url = $this->router->generate('user_view', array(
                    'nickname' => $token->getUser()->getNickname()
                ));
            }

            return new RedirectResponse($url);
        }
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        if ($request->isXmlHttpRequest()) {
            // Handle XHR here
        } else {
            // Create a flash message with the authentication error message
            $request->getSession()->setFlash('error', $exception->getMessage());
            $url = $this->router->generate('user_login');

            return new RedirectResponse($url);
        }
    }
}
31
David Morales

Si vous souhaitez la prise en charge des erreurs de formulaire FOS UserBundle, vous devez utiliser:

$request->getSession()->set(SecurityContext::AUTHENTICATION_ERROR, $exception);

au lieu de:

$request->getSession()->setFlash('error', $exception->getMessage());

Dans la première réponse.

(bien sûr, souvenez-vous de l'en-tête: utilisez Symfony\Component\Security\Core\SecurityContext;)

4
user2048716

J'ai géré cela entièrement avec javascript:

if($('a.login').length > 0) { // if login button shows up (only if logged out)
        var formDialog = new MyAppLib.AjaxFormDialog({ // create a new ajax dialog, which loads the loginpage
            title: 'Login',
            url: $('a.login').attr('href'),
            formId: '#login-form',
            successCallback: function(nullvalue, dialog) { // when the ajax request is finished, look for a login error. if no error shows up -> reload the current page
                if(dialog.find('.error').length == 0) {
                    $('.ui-dialog-content').slideUp();
                    window.location.reload();
                }
            }
        });

        $('a.login').click(function(){
            formDialog.show();
            return false;
        });
    }

Voici la classe AjaxFormDialog. Malheureusement, je ne l'ai pas encore porté sur un plugin jQuery ... https://Gist.github.com/16018

3
stoefln

Vous devez renvoyer un objet Response dans les deux cas (Ajax ou non). Ajoutez un "autre" et vous êtes prêt à partir.

L'implémentation par défaut est:

$response = $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));

dans AbstractAuthenticationListener::onSuccess

2
DaveT

J'ai fait un petit bundle pour les nouveaux utilisateurs pour fournir un AJAX: https://github.com/Divi/AjaxLoginBundle

Il vous suffit de remplacer l'authentification par form_login par ajax_form_login dans le security.yml.

N'hésitez pas à suggérer de nouvelles fonctionnalités dans le tracker de problème Github!

1
Divi

Ce n'est peut-être pas ce que le PO a demandé, mais je suis tombé sur cette question et j'ai pensé que d'autres pourraient avoir le même problème que moi.

Pour ceux qui implémentent une connexion AJAX en utilisant la méthode décrite dans la réponse acceptée et qui utilisent AUSSI AngularJS pour effectuer la demande AJAX, cela a gagné ne fonctionne pas par défaut. $http angulaire ne définit pas les en-têtes que Symfony utilise lors de l'appel de la méthode $request->isXmlHttpRequest(). Pour utiliser cette méthode, vous devez définir l'en-tête approprié dans le Angular request. Voici ce que j'ai fait pour contourner le problème:

$http({
    method  : 'POST',
    url     : {{ path('login_check') }},
    data    : data,
    headers: {'X-Requested-With': 'XMLHttpRequest'}
})

Avant d'utiliser cette méthode, sachez que cet en-tête ne fonctionne pas correctement avec CORS. Voir cette question

0
Sehael