Symfony2 custom connection by web service

2019-08-07 13:55发布

问题:

I'm trying to create a custom connection where I should use web service. So I read the tutorial on security and this one on custom provider. Now I'm trying to create my own login form with 3 fields : Email, password and number. After validation I understood that my /login_check pass in the function loadUserByUsername($username), but this function took in argument just $username and doesn't take my fields email and number. To execute my web service I need to get my 3 args. How can I customize my login form?

The goal is: When users submit the login form I want to send a web service with login form args. If I get my response without error I want to connect my user loaded by web service to symfony2 toolbar else I want to display an error message.

You can see my code here :

Security.yml :

security:
    encoders:
        MonApp\MonBundle\Security\User\WebserviceUser: sha512
        #Symfony\Component\Security\Core\User\User: plaintext

    # http://symfony.com/doc/current/book/security.html#hierarchical-roles
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-    from-user-providers
    providers:
        #in_memory:
             #memory:
                #users:
                    #ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                    #admin: { password: kitten, roles: 'ROLE_ADMIN' }
        webservice:
            id: webservice_user_provider

    # the main part of the security, where you can set up firewalls
    # for specific sections of your app
    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        area_secured:
            pattern:    ^/
            anonymous:  ~
            form_login:
                login_path:  /login
                check_path:  /login_check
                default_target_path: /test
            logout:
                path:   /logout
                target: /

    # with these settings you can restrict or allow access for different parts
    # of your application based on roles, ip, host or methods
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_AUTHENTICATED }

WebserviceUser.php :

<?php

namespace MonApp\MonBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;

class WebserviceUser implements UserInterface
{
    private $email;
    private $password;
    private $num;
    private $salt;
    private $roles;

    public function __construct($email, $password, $num, $salt, array $roles)
    {
        $this->email = $email;
        $this->password = $password;
        $this->num = $num;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function getUsername()
    {
        return '';
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getNum()
    {
        return $this->num;
    }    

    public function getSalt()
    {
        return $this->salt;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function eraseCredentials()
    {}

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            return false;
        }

        if ($this->email !== $user->getEmail()) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->num !== $user->getNum()) {
            return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }

        return true;
    }
}

WebserviceUserProvider.php

<?php

namespace MonApp\MonBundle\Security\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

use MonApp\MonBundle\Security\User\WebserviceUser;

class WebserviceUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        //print_r($username);
        //die();
        // effectuez un appel à votre service web ici

        return new WebserviceUser('email', 'password', '45555', 'salt', array('ROLE_USER'));
        //throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        print_r($user);
        die();

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'MonApp\MonBundle\Security\User\WebserviceUser';
    }
}

service.yml

parameters:
    webservice_user_provider.class:  MonApp\MonBundle\Security\User\WebserviceUserProvider

services:
    webservice_user_provider:
        class: "%webservice_user_provider.class%"

I won't put all the code, but my login action, template and routing are exactly the same than security link. But my user new WebserviceUser('email', 'password', '45555', 'salt', array('ROLE_USER')) isn't connected to the toolbar. So I think I forgot something...

Do I need to use a Listener, UserToken and Factory to do that ?

回答1:

Ok boy, prepare for a long answer.

I assume that you have a folder named Security placed in /MonApp/MonBundle

First you need a custom Token placed in Security/Token/WebServiceToken

<?php 
namespace MonApp\MonBundle\Security\Token;


use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class WebServiceToken implements TokenInterface
{
protected $attributes;
protected $authenticated;
protected $user;

public function __construct($attributes)
{
    $this->setAttributes($attributes);
    $this->authenticated = false;
    $this->user = null;
}


/**
 * {@inheritdoc}
 */
public function serialize()
{
    return serialize(
            array(
                    is_object($this->user) ? clone $this->user : $this->user,
                    $this->authenticated,
                    $this->attributes
            )
    );
}

/**
 * {@inheritdoc}
 */
public function unserialize($serialized)
{
    list($this->user, $this->authenticated, $this->attributes) = unserialize($serialized);
}


public function __toString()
{
    $result = '';

    foreach($this->attributes as $name => $value)
    {
        $result .= "$name: $value ";
    }

    return "Token($result)";
}

/**
 * Returns the user roles.
 *
 * @return RoleInterface[] An array of RoleInterface instances.
*/
public function getRoles()
{
    return $this->user->getRoles();
}

public function getUser()
{
    return $this->user;
}

public function setUser($user)
{
    $this->user = $user;
}

public function getUsername()
{
    return $this->user->getUsername();
}

public function isAuthenticated()
{
    return $this->authenticated;
}

public function setAuthenticated($isAuthenticated)
{
    $this->authenticated = $isAuthenticated;
}

public function eraseCredentials()
{
    ;
}

public function getAttributes()
{
    return $this->attributes;
}

public function setAttributes(array $attributes)
{
    $this->attributes = $attributes;
}

public function hasAttribute($name)
{
    return array_key_exists($name, $this->attributes);
}

public function getAttribute($name)
{
    if (!array_key_exists($name, $this->attributes)) {
        throw new \InvalidArgumentException(sprintf('This token has no "%s" attribute.', $name));
    }

    return $this->attributes[$name];
}

public function setAttribute($name, $value)
{
    $this->attributes[$name] = $value;
}

public function getCredentials()
{
    return null;
}
}

Then you need a Firewall in Security/Authentication/WebServiceAuthenticationListener

<?php 

namespace MonApp\MonBundle\Security\Authentication;

use MonApp\MonBundle\Security\Token\WebServiceToken;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;

class WebServiceAuthenticationListener implements ListenerInterface
{

protected $securityContext;
protected $authentificationManager;
protected $logger;

/**
 * @param SecurityContextInterface $securityContext
 * @param AuthenticationManagerInterface $authenticationManager
 * @param LoggerInterface $logger
 */
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, LoggerInterface $logger = null)
{
    $this->securityContext       = $securityContext;
    $this->authenticationManager = $authenticationManager;
    $this->logger                = $logger;
}


/**
 * {@inheritdoc}
 * @see \Symfony\Component\Security\Http\Firewall\ListenerInterface::handle()
 */
final public function handle(GetResponseEvent $event)
{
    $request = $event->getRequest();

     /**
     * Fill $attributes with the data you want to set in the user
     */
    $attributes = array();
    $token =  new WebServiceToken($attributes);


    try {
        if (null !== $this->logger ) {
            $this->logger->debug(sprintf('Vérification du contexte de sécurité pour le token: %s', $token));
        }

        $token = $this->authenticationManager->authenticate($token);

        if (null !== $this->logger) {
            $this->logger->info(sprintf('Authentification réussie: %s', $token));
        }

        // Token authentifié
        $this->securityContext->setToken($token);
    }
    catch (AuthenticationException $failed) {
        throw $failed;
    }
}
}

Then you need an Authentication provider in Security/Authentication/WebServiceAuthenticationProvider

<?php

namespace MonApp\MonBundle\Security\Authentication;

use Symfony\Component\Security\Core\Exception\AuthenticationException;

use MonApp\MonBundle\Security\User\WebServiceUser;
use MonApp\MonBundle\Security\User\WebServiceUserProvider;
use MonApp\MonBundle\Security\Token\WebServiceToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;

class WebServiceAuthenticationProvider implements AuthenticationProviderInterface
{

protected $provider;

public function __construct(WebServiceUserProvider $provider)
{
    $this->provider = $provider;
}

 public function authenticate(TokenInterface $token)
 {
    if (!$this->supports($token)) {
        return new AuthenticationException('Token non supporté');
    }


    $user = $this->provider->createUser($token->getAttributes());
    $token->setUser($user);

    /**
     * CALL TO THE WEB SERVICE HERE
     */
    $myCallisASuccess = true;
    if($myCallisASuccess) {
        $token->setAuthenticated(true);
    }

    return $token;
 }


 public function supports(TokenInterface $token)
 {
    return $token instanceof WebServiceToken;
 }
}

Now the factory ... Security/Factory/WebServiceFactory

<?php 

namespace MonApp\MonBundle\Security\Factory;

use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;



class WebServiceFactory implements SecurityFactoryInterface
{
/**
 * {@inheritdoc}
 * @see \Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface::create()
 */
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
    $providerId = 'security.authentication.provider.web_service'.$id;
    $container->setDefinition($providerId, new DefinitionDecorator('web_service.security.authentication.provider'));

    $listenerId = 'security.authentication.listener.web_service.'.$id;
    $container->setDefinition($listenerId, new DefinitionDecorator('web_service.security.authentication.listener'));

    return array($providerId, $listenerId, $defaultEntryPoint);
}

/**
 * {@inheritdoc}
 * @see \Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface::getPosition()
 */
public function getPosition()
{
    return 'pre_auth';
}


/**
 * {@inheritdoc}
 * @see \Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface::getKey()
 */
public function getKey()
{
    return 'web_service';
}



}

You have to edit WebServiceUserProvider by adding this function

    public function createUser(array $attributes)
{
    $email = $attributes['email'];
    $password = $attributes['password'];
    $num = $attributes['num'];
    $salt = $attributes['salt'];

    $user = new WebServiceUser($email, $password, $num, $salt);

    return $user;
}

And remove $roles from you WebServiceUSer class:

public function __construct($email, $password, $num, $salt)
{
    $this->email = $email;
    $this->password = $password;
    $this->num = $num;
    $this->salt = $salt;
    $this->roles = array();
}

Ok, now you have all you security classes done. Let's configure this....

In the class MonBundle

<?php 

namespace MonApp\Bundle\MonBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use MonApp\Bundle\MonBundle\Security\Factory\WebServiceFactory;


class MonBundle extends Bundle
{
/**
 * {@inheritdoc}
 * @see \Symfony\Component\HttpKernel\Bundle\Bundle::build()
 */
public function build(ContainerBuilder $container)
{
    parent::build($container);

    // Ajout de la clef 'web_service' à l'extension security
    $extension = $container->getExtension('security');
    $extension->addSecurityListenerFactory(new WebServiceFactory());

}
}

In the config of MonBundle

services:
    web_service.security.user.provider:
        class:  MonApp\Bundle\MonBundle\Security\User\WebServiceUserProvider

web_service.security.authentication.listener:
    class:  MonApp\Bundle\MonBundle\Security\Authentication\WebServiceAuthenticationListener
    arguments: ['@security.context', '@web_service.security.authentication.provider','@?logger']

web_service.security.authentication.provider:
    class:  MonApp\Bundle\MonBundle\Security\Authentication\WebServiceAuthenticationProvider
    arguments: ['@web_service.security.user.provider']

And last, in your app config:

security:
    area_secured:
        pattern:    ^/
        web_service:  ~
        form_login:
            login_path:  /login
            check_path:  /login_check
            default_target_path: /test
        logout:
            path:   /logout
            target: /