Symfony2 SecurityBundle and FOSUserBundle integration: How does it work?
OVERVIEW
A couple of days ago, I realized I needed to add some new functionality to the login process. Specifically, I needed to track all previous login attempts. Not knowing anything about the new Symfony2 SecurityBundle, I had to go through the underlying code to understand what was going on. In the process, I think got a basic idea about how the new SecurityBundle interacts with FOSUserBundle.CONFIGURATION
I have a basic security configuration as illustrated below.app/config/security.yml
Full security configuration reference can be found at here
app/config/routing.yml
As you can see above, we are importing all FOSUserBundle security routing rules with the /user prefix. Now, let's have a look at the security routing rules under FOSUserBundle.
vendor/bundles/FOS/UserBundle/Resources/config/routing/security.xml
It should be pretty self explanatory up to this point. We have a default security routing config file that we are importing into our application with the /user prefix and we have all the matching paths (login_path, check_path, etc.) defined in our app/config/config.yml for a working FOSUserBundle implementation. Now, let's have a look at FOSUserBundle SecurityController.
vendor/bundles/FOS/UserBundle/Controller/SecurityController.php
- namespace FOS\UserBundle\Controller;
- use Symfony\Component\DependencyInjection\ContainerAware;
- use Symfony\Component\Security\Core\SecurityContext;
- use Symfony\Component\Security\Core\Exception\AuthenticationException;
- class SecurityController extends ContainerAware
- {
- public function loginAction()
- {
- $request = $this->container->get('request');
- /* @var $request \Symfony\Component\HttpFoundation\Request */
- $session = $request->getSession();
- /* @var $session \Symfony\Component\HttpFoundation\Session */
- // get the error if any (works with forward and redirect -- see below)
- if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
- $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
- } elseif (null !== $session && $session->has(SecurityContext::AUTHENTICATION_ERROR)) {
- $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
- $session->remove(SecurityContext::AUTHENTICATION_ERROR);
- } else {
- $error = '';
- }
- if ($error) {
- // TODO: this is a potential security risk (see http://trac.symfony-project.org/ticket/9523)
- $error = $error->getMessage();
- }
- // last username entered by the user
- $lastUsername = (null === $session) ? '' : $session->get(SecurityContext::LAST_USERNAME);
- return $this->container->get('templating')->renderResponse('FOSUserBundle:Security:login.html.'.$this->container->getParameter('fos_user.template.engine'), array(
- 'last_username' => $lastUsername,
- 'error' => $error,
- ));
- }
- public function checkAction()
- {
- throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
- }
- public function logoutAction()
- {
- throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
- }
- }
HOW DOES IT WORK?
Well, a single word: listeners.- First, an appropriate listener factory class is identified based on the authentication type.
- Second, this listener factory class registers a user provider identified by the provider key under the firewall definition in security.yml. In our case, our provider is called fos_userbundle. This process basically links SecurityBundle to FOSUserBundle for authenticating submitted from values against a database.
- Next, the listener factory class registers an authentication listener identified by "security.authentication.listener.form". This code listens to login attempts at the /user/login path.
- Finally, FOSUserBundle registers a login listener for security.interactive_login event. This event indicates a successful login and the listener is used to execute post login code.
Let's go back to the beginning; everything starts when SecurityBundle is initiated and our security factory definitions are loaded from security_factories.xml.
Symfony/Bundle/SecurityBundle/Resources/config/security_factories.xml
- xml version="1.0" ?>
- <container xmlns="http://symfony.com/schema/dic/services"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- <services>
- <service id="security.authentication.factory.form" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory">
- <tag name="security.listener.factory" />
- service>
- <service id="security.authentication.factory.x509" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory">
- <tag name="security.listener.factory" />
- service>
- <service id="security.authentication.factory.basic" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory">
- <tag name="security.listener.factory" />
- service>
- <service id="security.authentication.factory.digest" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory">
- <tag name="security.listener.factory" />
- service>
- <service id="security.authentication.factory.remember_me" class="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory">
- <tag name="security.listener.factory" />
- service>
- services>
- container>
Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
- private function createFirewalls($config, ContainerBuilder $container)
- {
- // ...
- // create security listener factories
- $factories = $this->createListenerFactories($container, $config);
- // ...
- }
Once all listener factories are loaded, the code then loops over the firewall definitions in our app/config/security.yml file and calls SecurityExtension::createFirewall() method for each one found. In our app/config/config.yml file, we have three firewalls defined: main, dev, and login.
Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
- private function createFirewalls($config, ContainerBuilder $container)
- {
- // ...
- foreach ($firewalls as $name => $firewall) {
- list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $factories);
- $contextId = 'security.firewall.map.context.'.$name;
- $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
- $context
- ->replaceArgument(0, $listeners)
- ->replaceArgument(1, $exceptionListener)
- ;
- $map[$contextId] = $matcher;
- }
- // ...
- }
Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
- private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds, array $factories)
- {
- // ...
- // Authentication listeners
- list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $factories);
- // ...
- }
Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
- private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, array $factories)
- {
- // ...
- foreach ($this->listenerPositions as $position) {
- foreach ($factories[$position] as $factory) {
- $key = str_replace('-', '_', $factory->getKey());
- if (isset($firewall[$key])) {
- $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;
- list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
- $listeners[] = new Reference($listenerId);
- $authenticationProviders[] = $provider;
- $hasListeners = true;
- }
- }
- }
- // ...
- }
Once an appropriate factory class is identified, the FormLoginFactory::create() method is called. This registers a "security.authentication.listener.form" authentication listener that intercepts login attempts.
The FormLoginFactory::create() method also calls createAuthProvider() which is responsible for registering our custom user provider (fos_userbundle). This user provider class is used to verify login credentials against a database.
The FormLoginFactory class also declares security.authentication.listener.form as our listener id. (See FormLoginFactory::getListenerId())
Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml
- xml version="1.0" ?>
- <container xmlns="http://symfony.com/schema/dic/services"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- <parameters>
- <parameter key="security.authentication.listener.form.class">Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListenerparameter>
- parameters>
- <services>
- <service id="security.authentication.listener.form"
- class="%security.authentication.listener.form.class%"
- parent="security.authentication.listener.abstract"
- abstract="true">
- service>
- services>
- container>
First, the authenticate() method is called on our authentication provider, which then makes a call to our user provider. This is the UserManager class included in the FOSUserBundle package.
Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php
- protected function attemptAuthentication(Request $request)
- {
- // ...
- return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
- }
Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php
- private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token)
- {
- // ...
- if (null !== $this->dispatcher) {
- $loginEvent = new InteractiveLoginEvent($request, $token);
- $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
- }
- // ...
- }
vendor/bundles/FOS/UserBundle/Resources/config/security.xml
- xml version="1.0" encoding="UTF-8"?>
- <container xmlns="http://symfony.com/schema/dic/services"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
- <parameters>
- <parameter key="fos_user.encoder_factory.class">FOS\UserBundle\Security\Encoder\EncoderFactoryparameter>
- <parameter key="fos_user.security.interactive_login_listener.class">FOS\UserBundle\Security\InteractiveLoginListenerparameter>
- parameters>
- <services>
- <service id="fos_user.encoder_factory" class="%fos_user.encoder_factory.class%" public="false">
- <argument>%security.encoder.digest.class%argument>
- <argument>%fos_user.encoder.encode_as_base64%argument>
- <argument>%fos_user.encoder.iterations%argument>
- <argument type="service" id="fos_user.encoder_factory.parent" />
- service>
- <service id="fos_user.security.interactive_login_listener" class="%fos_user.security.interactive_login_listener.class%">
- <argument type="service" id="fos_user.user_manager" />
- <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
- service>
- services>
- container>
FOS/UserBundle/Security/InteractiveLoginListener.php
- namespace FOS\UserBundle\Security;
- use FOS\UserBundle\Model\UserManagerInterface;
- use FOS\UserBundle\Model\UserInterface;
- use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
- use DateTime;
- class InteractiveLoginListener
- {
- protected $userManager;
- public function __construct(UserManagerInterface $userManager)
- {
- $this->userManager = $userManager;
- }
- public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
- {
- $user = $event->getAuthenticationToken()->getUser();
- if ($user instanceof UserInterface) {
- $user->setLastLogin(new DateTime());
- $this->userManager->updateUser($user);
- }
- }
- }
More than 3 requests, I'll translate this to Chinese.
超过3个请求,我就会把这篇文章翻译成中文。
No comments:
Post a Comment