Symfony 2.6 登录随机失败


Symfony 2.6 login randomly fails

我正在使用它最新的稳定版本(2.6.1(启动一个新的Symfony2项目,我已经设置了一个最小的登录和安全组件配置。但是,当用户被重定向回主页时,处理/login_check 请求后,一半的登录尝试似乎失败。

这是登录失败后日志显示的内容(箭头强调(:

request.INFO: Matched route "login_check" (parameters: "_route": "login_check") [] []
doctrine.DEBUG: SELECT t0.id AS id1, t0.username AS username2, t0.password AS password3, t0.nombre AS nombre4, t0.apellidos AS apellidos5, t0.email AS email6, t0.movil AS movil7, t0.ciudad AS ciudad8, t0.hospital AS hospital9, t0.cargo AS cargo10, t0.roles AS roles11, t0.created_at AS created_at12 FROM usuarios t0 WHERE t0.username = ? LIMIT 1 ["admin"] []
--> security.INFO: User "admin" has been authenticated successfully [] []
security.DEBUG: Write SecurityContext in the session [] []
request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundle'Controller'DefaultController::indexAction", "_route": "homepage") [] []
--> security.INFO: Populated SecurityContext with an anonymous Token [] []
security.DEBUG: Write SecurityContext in the session [] []

如您所见,login_check过程实际上成功地对用户进行身份验证,但是当它被重定向到另一个页面时,系统会以某种方式"忘记"它,就好像它实际上并没有将结果存储在会话中一样。当整个过程工作时,日志如下所示:

request.INFO: Matched route "login_check" (parameters: "_route": "login_check") [] []
doctrine.DEBUG: SELECT t0.id AS id1, t0.username AS username2, t0.password AS password3, t0.nombre AS nombre4, t0.apellidos AS apellidos5, t0.email AS email6, t0.movil AS movil7, t0.ciudad AS ciudad8, t0.hospital AS hospital9, t0.cargo AS cargo10, t0.roles AS roles11, t0.created_at AS created_at12 FROM usuarios t0 WHERE t0.username = ? LIMIT 1 ["admin"] []
--> security.INFO: User "admin" has been authenticated successfully [] []
security.DEBUG: Write SecurityContext in the session [] []
request.INFO: Matched route "homepage" (parameters: "_controller": "AppBundle'Controller'DefaultController::indexAction", "_route": "homepage") [] []
--> security.DEBUG: Read SecurityContext from the session [] []
security.DEBUG: Reloading user from user provider. [] []
doctrine.DEBUG: SELECT t0.id AS id1, t0.username AS username2, t0.password AS password3, t0.nombre AS nombre4, t0.apellidos AS apellidos5, t0.email AS email6, t0.movil AS movil7, t0.ciudad AS ciudad8, t0.hospital AS hospital9, t0.cargo AS cargo10, t0.roles AS roles11, t0.created_at AS created_at12 FROM usuarios t0 WHERE t0.id = ? [1] []
--> security.DEBUG: Username "admin" was reloaded from user provider. [] []
security.DEBUG: Write SecurityContext in the session [] []

这是我目前拥有的整个设置。我敢打赌,要么是security.yml文件中的错误配置,要么是错误(?(,但我不知道还能去哪里看。

安全.yml

security:
    encoders:
        AppBundle'Entity'Usuario: bcrypt
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    providers:
        usuarios:
            entity: { class: AppBundle'Entity'Usuario, property: username }
    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        default:
            pattern: ^/
            anonymous: ~
            form_login:
                check_path: login_check
                login_path: login
            logout:
                path:   logout
                target: homepage
    access_control:
        - { path: ^/$,      roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/.*,     roles: ROLE_USER }

路由.yml

app:
    resource: @AppBundle/Controller/
    type:     annotation
login_check:
    path: /login_check
logout:
    path: /logout

登录.html.twig

{% extends 'base.html.twig' %}
{% block body %}
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}
    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Nombre de usuario:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
        <label for="password">Contraseña:</label>
        <input type="password" id="password" name="_password" />
        <input type="submit" name="login" value="Acceder" />
    </form>
{% endblock %}

默认控制器.php

<?php
namespace AppBundle'Controller;
use Sensio'Bundle'FrameworkExtraBundle'Configuration'Route;
use Symfony'Bundle'FrameworkBundle'Controller'Controller;
class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        // Symfony dummy template that just shows 'Homepage.'
        return $this->render('default/index.html.twig');
    }
    /**
     * @Route("/login", name="login")
     */
    public function loginAction()
    {
        $authUtils = $this->get('security.authentication_utils');
        return $this->render('login/index.html.twig', array(
            'last_username' => $authUtils->getLastUsername(),
            'error'         => $authUtils->getLastAuthenticationError(),
        ));
    }
}

乌苏阿里奥.php

<?php
namespace AppBundle'Entity;
use Doctrine'ORM'Mapping as ORM;
use Symfony'Component'Security'Core'User'AdvancedUserInterface;
/**
 * @ORM'Table(name="usuarios")
 * @ORM'Entity
 */
class Usuario implements AdvancedUserInterface
{
    const ROLE_USER = 'ROLE_USER';
    const ROLE_ADMIN = 'ROLE_ADMIN';
    /**
     * @var integer
     *
     * @ORM'Column(name="id", type="integer")
     * @ORM'Id
     * @ORM'GeneratedValue(strategy="AUTO")
     */
    private $id;
    /**
     * @var string
     *
     * @ORM'Column(name="username", type="string", length=255, unique=true)
     */
    private $username;
    /**
     * @var string
     *
     * @ORM'Column(name="password", type="string", length=60)
     */
    private $password;
    /**
     * @var string
     *
     * @ORM'Column(name="nombre", type="string", length=255)
     */
    private $nombre;
    /**
     * @var string
     *
     * @ORM'Column(name="apellidos", type="string", length=255)
     */
    private $apellidos;
    /**
     * @var string
     *
     * @ORM'Column(name="email", type="string", length=255, unique=true)
     */
    private $email;
    /**
     * @var string
     *
     * @ORM'Column(name="movil", type="string", length=255)
     */
    private $movil;
    /**
     * @var string
     *
     * @ORM'Column(name="ciudad", type="string", length=255)
     */
    private $ciudad;
    /**
     * @var string
     *
     * @ORM'Column(name="hospital", type="string", length=255)
     */
    private $hospital;
    /**
     * @var string
     *
     * @ORM'Column(name="cargo", type="string", length=255)
     */
    private $cargo;
    /**
     * @var string
     *
     * @ORM'Column(name="roles", type="simple_array", length=255)
     */
    private $roles;
    /**
     * @var 'DateTime
     *
     * @ORM'Column(name="created_at", type="datetime")
     */
    private $createdAt;
    public function __construct()
    {
        $this->roles = [self::ROLE_USER];
        $this->createdAt = new 'DateTime();
    }
    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    // ... An awful lot of generic getters and setters
    // Plus the mandatory AdvancedUserInterface methods:
    /**
     * @inheritdoc
     */
    public function getUsername()
    {
        return $this->username;
    }
    /**
     * @inheritdoc
     */
    public function getPassword()
    {
        return $this->password;
    }
    /**
     * @inheritdoc
     */
    public function isAccountNonExpired()
    {
        return true;
    }
    /**
     * @inheritdoc
     */
    public function isAccountNonLocked()
    {
        return true;
    }
    /**
     * @inheritdoc
     */
    public function isCredentialsNonExpired()
    {
        return true;
    }
    /**
     * @inheritdoc
     */
    public function isEnabled()
    {
        return true;
    }
    /**
     * @inheritdoc
     */
    public function getSalt()
    {
        return null;    // I'm using the bcrypt encoder
    }
    /**
     * @inheritdoc
     */
    public function eraseCredentials()
    {
        $this->password = null;
    }
}

嗯,这是一个微妙的问题。我没有提到我也在使用PDOSessionHandler将会话存储在数据库中,这被证明是问题的根源。

到目前为止,我曾经将自定义会话类添加到主捆绑包的 Entity 命名空间中,以便在我运行 console doctrine:schema:create 时与其他表一起生成会话表。显然,此表的强制架构已从 2.6 版本开始修改,我的实体不再匹配它。

https://github.com/symfony/symfony/issues/12833

幸运的是,我还了解到 PDOSessionHandler 类有一个名为 createTable() 的帮助程序方法,它正是这样做的,并且由于无论如何都必须将其定义为服务,因此我决定放弃实体方法并开始使用 Command 来利用此方法。使用此新架构,登录过程现在始终有效。以下是所有相关代码和配置:

配置.yml

framework:
    session:
        handler_id:  session.handler.pdo
services:
    pdo:
        class: PDO
        arguments:
            - "mysql:host=%database_host%;port=%database_port%;dbname=%database_name%"
            - "%database_user%"
            - "%database_password%"
        calls:
            - [setAttribute, [3, 2]] # 'PDO::ATTR_ERRMODE, 'PDO::ERRMODE_EXCEPTION
    session.handler.pdo:
        class:     Symfony'Component'HttpFoundation'Session'Storage'Handler'PdoSessionHandler
        arguments: ["@pdo"]

SetupSessionsTableCommand.php

<?php
namespace AppBundle'Command;
use Symfony'Bundle'FrameworkBundle'Command'ContainerAwareCommand;
use Symfony'Component'Console'Input'InputInterface;
use Symfony'Component'Console'Output'OutputInterface;
/**
 * This command leverages the PdoSessionHandler::createTable()
 * method from the session.handler.pdo service.
 */
class SetupSessionsTableCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('setup:sessions')
            ->setDescription('Creates the sessions table');
    }
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $sessionHandler = $this->getContainer()->get('session.handler.pdo');
        try {
            $sessionHandler->createTable();
        } catch ('Exception $e) {
            $output->writeln("<error>Exception thrown while attempting to create the 'sessions' table</error>");
            $output->writeln($e->getMessage());
            $output->writeln($e->getTraceAsString());
            return -1;
        }
        $output->writeln("<info>'sessions' table created successfully</info>");
        return 0;
    }
}