如果自定义安全捆绑包中的身份验证错误,如何重定向到注销页面

时间:2012-09-14 09:34:07

标签: symfony

我正在尝试创建自定义安全捆绑包。

我可以设法让事情变得有效(长而硬:-))。

除非我在某处遇到身份验证错误,否则我想将其重新路由到 “注销”页面而不是登录页面。

我认为如果抛出异常,以这种方式更改security.yml会发送到失败路径

security:
    firewalls:
        logout:
            pattern:  ^/logout$
            security: false

        secured_area:
            pattern: ^/
            my_user_factory: true
            form_login:
                login_path: /login
                check_path: /login_check
                failure_path: /logout

由于我的基本代码始终有效(没有真正的控制),我添加了一个异常抛出来测试身份验证错误。

class AuthProvider implements AuthenticationProviderInterface
{
//...
    public function authenticate(TokenInterface $token)
    {
        throw new BadCredentialsException('Bad credentials :)');
    }
//...

但我回到了登录页面。

这是跟踪:

// I arrive on the site
UserFactory.getPosition
UserFactory.getKey
UserFactory.getKey
UserFactory.create
CnamtsSecurityExtension.load
Configuration.getConfigTreeBuilder
AcmeSecurityExtension.load
Configuration.getConfigTreeBuilder
AuthProvider.__construct
AuthListener.__construct
AuthProvider.__construct

// Let's login
SecurityController.loginAction
AuthProvider.__construct
AuthListener.__construct
AuthListener.attemptAuthentication
UserToken.__construct
AuthProvider.supports
AuthProvider.authenticate
// The exception is thrown

UserToken.serialize
UserToken.unserialize
UserToken.serialize
AuthProvider.__construct
SecurityController.loginAction
// Back to the login page. I'd like to be on the logout one.

这是整个代码。如果删除“抛出新的BadCredentialsException('Bad credentials :)');”线 它应该工作。

捆绑树:

|~src/
| |~Acme/
| | `~SecurityBundle/
| |   |~Controller/
| |   | |-DefaultController.php
| |   | `-SecurityController.php
| |   |~DependencyInjection/
| |   | |~Security/
| |   | | `~Factory/
| |   | |   `-UserFactory.php
| |   | |-AcmeSecurityExtension.php
| |   | `-Configuration.php
| |   |~Resources/
| |   | |~config/
| |   | | |-routing.yml
| |   | | |-security_factories.yml
| |   | | `-services.yml
| |   | `~views/
| |   |   |~Default/
| |   |   | `-index.html.twig
| |   |   | `-logout.html.twig
| |   |   `~Login/
| |   |     `-login.html.twig
| |   |~Security/
| |   | |~Authentication/
| |   | | |~Firewall/
| |   | | | `-AuthListener.php
| |   | | |~Provider/
| |   | | | `-AuthProvider.php
| |   | | `~Token/
| |   | |   `-UserToken.php
| |   | `~User/
| |   |   |-User.php
| |   |   `-UserProvider.php
| |   |+Tests/
| |   `-AcmeSecurityBundle.php

工厂:

<?php
// src/Acme/SecurityBundle/DependencyInjection/Security/Factory/UserFactory.php

namespace Acme\SecurityBundle\DependencyInjection\Security\Factory;

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

class UserFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.user.'.$id;
        $container
            ->setDefinition($providerId, new DefinitionDecorator('user.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
        ;
        $listenerId = 'security.authentication.listener.user.'.$id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('user.security.authentication.listener'));

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

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'my_user_factory';
    }

    public function addConfiguration(NodeDefinition $node)
    {}
}

听众:

<?php
// Acme/SecurityBundle/Security/Firewall/AuthListener.php

namespace Acme\SecurityBundle\Security\Authentication\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;


use Acme\SecurityBundle\Security\Authentication\Token\UserToken;

class AuthListener extends AbstractAuthenticationListener
{
    protected $securityContext;
    protected $authenticationManager;
    protected $httpUtils;

    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager,
                                SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $options = array())
    {
        parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, "user", array_merge(array(
            'username_parameter' => '_username',
            'password_parameter' => '_password',
            'intention' => 'authenticate',
            'post_only' => true,
        ), $options));
    }

    /**
     * Performs authentication.
     *
     * @param  Request $request A Request instance
     *
     * @return TokenInterface The authenticated token, or null if full authentication is not possible
     *
     * @throws AuthenticationException if the authentication fails
     */
    protected function attemptAuthentication(Request $request)
    {

        $username = trim($request->get($this->options['username_parameter'], null, true));
        $password = $request->get($this->options['password_parameter'], null, true);

        //$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);

        return $this->authenticationManager->authenticate(new UserToken($username, $password, $this->providerKey));

    }

    public function getHttpUtils()
    {
        return $this->httpUtils;
    }

    public function setHttpUtils($httpUtils)
    {
        $this->httpUtils = $httpUtils;
    }
}

authenticationProvider:

<?php
// Acme/SecurityBundle/Security/Authentication/Provider/AuthProvider.php

namespace Acme\SecurityBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;

use Acme\SecurityBundle\Security\Authentication\Token\UserToken;

class AuthProvider implements AuthenticationProviderInterface
{
    private $userProvider;
    private $cacheDir;

    public function __construct(UserProviderInterface $userProvider, $cacheDir)
    {
        $this->userProvider = $userProvider;
        $this->cacheDir     = $cacheDir;
    }

    public function authenticate(TokenInterface $token)
    {
        // EXCEPTION TO MAKE AN AUTHENTICATION ERROR
        throw new BadCredentialsException('Bad credentials :)');
        $user = $this->userProvider->loadUserByUsername($token->getUsername());
        // $userToken = new UserToken();
        // $userToken->setUser($user);
        // echo "it worked"; exit;
        $newToken = new UserToken($token->getUser(), $token->getCredentials(), "user", array("ROLE_ADMIN"));
        $username = $newToken->getUser();
        if (empty($username)) {
            throw new BadCredentialsException('Bad credentials :)');
        }
        //return $newToken;

        if ($user && $this->validate()) {
            $authenticatedToken = new UserToken($token->getUser(), $token->getCredentials(), "user", $user->getRoles());
            $authenticatedToken->setUser($user);

            return $authenticatedToken;
        }
    }

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

    public function validate()
    {
        return true;
    }

}

UserToken:

<?php
// Acme/SecurityBundle/Security/Authenticaion/Token/UserToken.php

namespace Acme\SecurityBundle\Security\Authentication\Token;

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

/**
 * UsernamePasswordToken implements a username and password token.
 *
 */

class UserToken extends AbstractToken
{
    private $credentials;
    private $providerKey;

    /**
     * Constructor.
     *
     * @param string $user        The username (like a nickname, email address, etc.)
     * @param string $credentials This usually is the password of the user
     * @param string $providerKey The provider key
     * @param array  $roles       An array of roles
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($user, $credentials, $providerKey, array $roles = array())
    {
        parent::__construct($roles);

        if (empty($providerKey)) {
            throw new \InvalidArgumentException('$providerKey must not be empty.');
        }

        $this->setUser($user);
        $this->credentials = $credentials;
        $this->providerKey = $providerKey;

        parent::setAuthenticated(count($roles) > 0);
    }

    /**
     * {@inheritdoc}
     */
    public function setAuthenticated($isAuthenticated)
    {
        if ($isAuthenticated) {
            throw new \LogicException('Cannot set this token to trusted after instantiation.');
        }

        parent::setAuthenticated(false);
    }

    public function getCredentials()
    {
        return $this->credentials;
    }

    public function getProviderKey()
    {
        return $this->providerKey;
    }

    /**
     * {@inheritdoc}
     */
    public function eraseCredentials()
    {
        parent::eraseCredentials();

        $this->credentials = null;
    }

    public function serialize()
    {
        return serialize(array($this->credentials, $this->providerKey, parent::serialize()));
    }

    public function unserialize($str)
    {
        list($this->credentials, $this->providerKey, $parentStr) = unserialize($str);
        parent::unserialize($parentStr);
    }
}

用户提供商:

<?php
// src/Acme/SecurityBundle/Security/User/UserProvider.php

namespace Acme\SecurityBundle\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 Acme\SecurityBundle\Security\User\User;

class UserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        // make a call to your webservice here
        // $userData = ...
        // pretend it returns an array on success, false if there is no user
        $user = new User();
        $user->setUsername($username);
        $user->setPassword("1234");
        $user->setRoles(array("ROLE_ADMIN"));

        return $user;

    }

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

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

    public function supportsClass($class)
    {
        return $class === 'Acme\SecurityBundle\Security\User';
    }
}

用户:

<?php
// src/Acme/SecurityBundle/Security/User/User.php

namespace Acme\SecurityBundle\Security\User;

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

class User implements UserInterface
{
    private $username;
    private $password;
    private $salt;
    private $roles;

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

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

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

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

    public function setRoles($roles)
    {
        $this->roles = $roles;
    }

    public function setPassword($password)
    {
        $this->password = $password;
    }

    public function setSalt($salt)
    {
        $this->salt = $salt;
    }

    public function setUsername($username)
    {
        $this->username = $username;
    }

    public function eraseCredentials()
    {
    }

    public function equals(UserInterface $user)
    {
        if (!$user instanceof User) {
            return false;
        }
        if ($this->password !== $user->getPassword()) {
            return false;
        }
        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }
        if ($this->username !== $user->getUsername()) {
            return false;
        }
        return true;
    }
}

参数

捆绑路由:

# Acme/SecurityBundle/Resources/config/routing.yml
AcmeSecurityBundle_homepage:
    pattern:  /
    defaults: { _controller: AcmeSecurityBundle:Default:index }

AcmeSecurityBundle_logout:
    pattern:  /logout
    defaults: { _controller: AcmeSecurityBundle:Default:logout }

login:
    pattern: /login
    defaults: { _controller: AcmeSecurityBundle:Security:login }

login_check:
    pattern: /login_check

工厂声明

# Acme/SecurityBundle/Resources/config/security_factories.yml

services:
    security.authentication.factory.user:
        class:  Acme\SecurityBundle\DependencyInjection\Security\Factory\UserFactory
        tags:
            - { name: security.listener.factory }

服务:

# Acme/SecurityBundle/Resources/config/services.yml

services:
    user.security.authentication.provider:
        class:  Acme\SecurityBundle\Security\Authentication\Provider\AuthProvider
        arguments: ["", %kernel.cache_dir%/security/nonces]

    user.security.authentication.listener:
        class:  Acme\SecurityBundle\Security\Authentication\Firewall\AuthListener
        arguments: [@security.context, @security.authentication.manager, @security.authentication.session_strategy, @security.http_utils]
        tags:
            - { name: monolog.logger, channel: security }

    user_provider_service:
        class: Acme\SecurityBundle\Security\User\UserProvider

申请参数

常规路由

# /app/config/routing.yml

AcmeSecurityBundle:
    resource: "@AcmeSecurityBundle/Resources/config/routing.yml"
    prefix:   /

安全...

# /app/config/security.yml

security:
    factories:
        - "%kernel.root_dir%/../src/Acme/SecurityBundle/Resources/config/security_factories.yml"

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
         user_provider:
             id: user_provider_service

    firewalls:
        login:
            pattern:  ^/login$
            security: false

        logout:
            pattern:  ^/logout$
            security: false

        secured_area:
            pattern: ^/
            my_user_factory: true
            form_login:
                login_path: /login
                check_path: /login_check
                failure_path: /logout


    access_control:
        #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
        #- { path: ^/_internal, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }

控制器和模板

控制器,主页和注销

<?php
// Acme/SecurityBundle/Controller/DefaultController.php

namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;


class DefaultController extends Controller
{

    public function indexAction()
    {
        return $this->render('AcmeSecurityBundle:Default:index.html.twig', 
                  array('parametres' => print_r($this->container->getParameterBag()->all(), true),
                        'request' => print_r($this->getRequest(),true)));
    }

    public function logoutAction()
    {
        $this->get('security.context')->setToken(null);
        $this->getRequest()->getSession()->invalidate();

        return $this->render('CnamtsSecurityBundle:Default:logout.html.twig', 
                  array('parametres' => print_r($this->container->getParameterBag()->all(), true),
                    'request' => print_r($this->getRequest(),true)));
    }
}

主页模板

{# Acme/SecurityBundle/Resources/views/Default/index.html.twig #}
Hello ! <a href = "{{ path('AcmeSecurityBundle_logout') }}">logout</a>

<pre>
Parametres
{{ parametres }}
Request
{{ request }}
</pre>

退出模板

Goodbye !

<pre>
Parametres
{{ parametres }}
Request
{{ request }}
</pre>

控制器和登录页面

<?php
// src/Acme/SecurityBundle/Controller/SecurityController.php

namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // get the login error if there is one
        $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        $session->remove(SecurityContext::AUTHENTICATION_ERROR);

        return $this->render('AcmeSecurityBundle:Login:login.html.twig', array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }

}

相关模板

{# Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
    <div>{{ error.message }}</div>
{% endif %}
<form name="loginForm" action="{{ path('login_check') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" />
    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />
    {#
        If you want to control the URL the user is redirected to on success (more details
        below)
        <input type="hidden" name="_target_path" value="/account" />
    #}
    <button type="submit">login</button>
</form>

1 个答案:

答案 0 :(得分:1)

当抛出AuthenticationException时,Symfony为该身份验证提供程序调用入口点类的start方法(如果设置了入口点),最终将重定向响应返回到login_path。您必须在抛出异常之前发出$token->setAuthenticated(false);

要添加提供者的入口点,UserFactory应该扩展AbstractFactory

有关工厂和入口点的实现,请参阅herehere