Symfony 自定义身份验证提供程序在请求重叠时注销

Posted

技术标签:

【中文标题】Symfony 自定义身份验证提供程序在请求重叠时注销【英文标题】:Symfony custom authentication provider log out on request overlap 【发布时间】:2018-11-23 16:43:21 【问题描述】:

这个问题已经在 Symfony 3.3.17 和 3.4.9 中重现

我有一个自定义身份验证提供程序,它将旧应用程序和 Symfony 应用程序联系在一起:

app/config/security.yml

security:
    providers:
        zog:
            id: app.zog_user_provider


    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:

            anonymous: ~
            logout:
                path:   /logout
                target: /
            guard:
                authenticators:
                    - app.legacy_token_authenticator
                    - app.token_authenticator
                entry_point: app.legacy_token_authenticator

src/AppBundle/Security/LegacyTokenAuthenticator:

class LegacyTokenAuthenticator extends AbstractGuardAuthenticator

    private $session;

    private $router;

    public function __construct(
        RouterInterface $router,
        SessionInterface $session,
        $environment
    ) 
        if (session_status() != php_SESSION_ACTIVE) 
            if ($environment != 'test')
                session_start();
            
            $session->start();
            $this->setSession($session);
        
        //if (!$session->isStarted()) 

        //


        $this->router = $router;
    


    /**
     * @return mixed
     */
    public function getSession()
    
        return $this->session;
    


    /**
     * @param mixed $session
     */
    public function setSession($session)
    
        $this->session = $session;
    


    /**
     * Called on every request. Return whatever credentials you want,
     * or null to stop authentication.
     */
    public function getCredentials(Request $request)
    
        $session = $this->getSession();

        if (isset($_SESSION['ADMIN_logged_in']) && intval($_SESSION['ADMIN_logged_in']))
            return $_SESSION['ADMIN_logged_in'];
        
        return;
    

    public function getUser($credentials, UserProviderInterface $userProvider)
    
        return $userProvider->loadUserByUserId($credentials);
    

    public function checkCredentials($credentials, UserInterface $user)
    
        return $user->getUsername() == $credentials;
    

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    
        return null;
    

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    
        return null;
    

    /**
     * Called when authentication is needed, but it's not sent
     */
    public function start(Request $request, AuthenticationException $authException = null)
    
        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    

    public function supportsRememberMe()
    
        return false;
    

src/AppBundle/Security/TokenAuthenticator:

class TokenAuthenticator extends AbstractGuardAuthenticator


    /**
     * @var \Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
     * Default message for authentication failure.
     *
     * @var string
     */
    private $failMessage = 'Invalid credentials';

    /**
     * @var UserPasswordEncoderInterface
     */
    private $passwordEncoder;

    /**
     * Creates a new instance of FormAuthenticator
     */
    public function __construct(
        RouterInterface $router,
        SessionInterface $session,
        $environment,
        UserPasswordEncoderInterface $passwordEncoder
    ) 
        $this->passwordEncoder = $passwordEncoder;
        $this->router = $router;

        if (session_status() != PHP_SESSION_ACTIVE) 
            if ($environment != 'test') 
                session_start();
            
            $session->start();
        

    

    /**
     * @inheritdoc
     */
    public function getCredentials(Request $request)
    
        if ($request->getPathInfo() != '/security/login' || !$request->isMethod('POST')) 
            return;
        

        return ['username' => $request->request->get('username'), 'password' => $request->request->get('password')];
    

    /**
     * @inheritdoc
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    
        try 
            return $userProvider->loadUserByUsername($credentials['username']);
         catch (UsernameNotFoundException $e) 
            throw new CustomUserMessageAuthenticationException(
                $e->getMessage() != '' ?$e->getMessage():$this->failMessage
            );
        
    

    /**
     * @inheritdoc
     */
    public function checkCredentials($credentials, UserInterface $user)
    
        if ($this->passwordEncoder->isPasswordValid($user, $credentials['password'])) 
            return true;
        
        throw new CustomUserMessageAuthenticationException($this->failMessage);
    

    /**
     * @inheritdoc
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    
        $_SESSION['ADMIN_logged_in'] = $token->getUser()->getUsername();
        if ($_SESSION['legacy_page_requested'] ?? '/')
            $url = $_SESSION['legacy_page_requested'] ?? '/';
        else
            $url = '/workflow_detailv2view.php';
        
        unset($_SESSION['legacy_page_requested']);
        return new RedirectResponse($url);
    

    /**
     * @inheritdoc
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    
        $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);

        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    

    /**
     * @inheritdoc
     */
    public function start(Request $request, AuthenticationException $authException = null)
    
        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    

    /**
     * @inheritdoc
     */
    public function supportsRememberMe()
    
        return false;
    

我发现这个系统运行良好。然而,旧页面中的一项新功能正在运行 2 个重叠的 Symfony 异步应用程序请求。

在这种情况下,第一个请求显示 2 个会话 Cookie

Request URL: https://somedomain.com/system/staff_meeting/edit/1
Request Method: GET
Status Code: 200 OK
Remote Address: 222.154.225.22:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: max-age=0, must-revalidate, private
Cache-Control: no-store, no-cache, must-revalidate
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Date: Wed, 13 Jun 2018 21:57:19 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=15, max=90
Server: Apache/2.4.6 (CentOS) mpm-itk/2.4.7-04 OpenSSL/1.0.2k-fips PHP/7.0.20
Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
Set-Cookie: PHPSESSID=tn7jhi5n2iu16le1os971vn024; path=/
Transfer-Encoding: chunked
X-Powered-By: PHP/7.0.20

第二个请求正在注销:

Request URL: https://somedomain.com/system/staff_meeting/edit/1
Request Method: GET
Status Code: 302 Found
Remote Address: 222.154.225.22:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: no-store, no-cache, must-revalidate
Cache-Control: max-age=0, must-revalidate, private
Connection: Keep-Alive
Content-Length: 332
Content-Type: text/html; charset=UTF-8
Date: Thu, 14 Jun 2018 01:26:43 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=15, max=90
Location: /system/security/login
Server: Apache/2.4.6 (CentOS) mpm-itk/2.4.7-04 OpenSSL/1.0.2k-fips PHP/7.0.20
Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
X-Powered-By: PHP/7.0.20

我相信这一定是在访问我们用来将遗留系统和 Symfony 应用程序结合在一起的 $_SESSION 变量时的某种竞争条件。

任何想法如何解决这个问题?

【问题讨论】:

对于 Symfony 中的会话管理,最好使用 $request->getSession() 或使用依赖注入并注入 SessionInterface。 【参考方案1】:

对您的身份验证器进行以下更改,以使会话管理保持在 symfony 中:

src/AppBundle/Security/LegacyTokenAuthenticator:

public function __construct(RouterInterface $router) 
    $this->router = $router;


public function getUser($credentials, UserProviderInterface $userProvider)

    return $userProvider->loadUserByUsername($credentials);


public function getCredentials(Request $request)

    $session = $request->getSession();
    if ($session->has('ADMIN_logged_in') && intval($session->get('ADMIN_logged_in')))
        return $session->get('ADMIN_logged_in');
    
    return null;

src/AppBundle/Security/TokenAuthenticator:

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)

    $session = $request->getSession();
    $session->set('ADMIN_logged_in', $token->getUser()->getUsername());
    if ($session->has('legacy_page_requested')) 
        $url = $session->get('legacy_page_requested') ?? '/';
        $session->remove('legacy_page_requested');
     else 
        $url = '/workflow_detailv2view.php';
    
    return new RedirectResponse($url);

【讨论】:

因此,使用此解决方案,我看不到旧应用程序如何检查某人是否已在 Symfony 端登录,因为 $_SESSION 中没有任何内容。登录页面只存在于 Symfony 中 检查此解决方案中提出的跨平台共享会话变量的侦听器:***.com/questions/35601629/…【参考方案2】:

我可以通过设置来规避问题

security.yml

security:
    session_fixation_strategy: none

但我不确定如何修复会话的重新生成和 Cookie 值的相应重命名。

我仍然很想听听对此的其他想法。

【讨论】:

【参考方案3】:

我不完全了解您的架构,但我认为最好在单个防火墙上没有两个 Guard Authenticator。

您可以像这样使您的 Symfony 会话成为旧会话:

# config.yml
framework:
    session:
        handler_id: session.handler.native_file
        save_path: "/tmp/sessions/%kernel.environment%"

从那里,您拥有的任何遗留会话都应该可以使用 $_SESSION 超全局(遗留代码经常使用)进行管理。

如果您想为旧系统和新系统设置不同的登录规则,我的建议是设置两个不同的防火墙。您可以分配一个shared context,这样用户就不需要重新进行身份验证,只需在子类中添加一些额外的规则。

【讨论】:

【参考方案4】:

避免调用 session_start() 或 $session->start();! Symfony 会自己做。

【讨论】:

这是一个答案吗?身份验证成功后会话重新启动。 额外的 session_starts 可能会引发问题。我从不在乎开始会话,值得一试。 可能是。但是当他使用guardAuthenticator 时,会话会自动管理。我之前在身份验证成功时设置的会话变量在这里丢失了问题。

以上是关于Symfony 自定义身份验证提供程序在请求重叠时注销的主要内容,如果未能解决你的问题,请参考以下文章

在 Symfony2 中使用自定义身份验证提供程序

Symfony2 - 安装了 FOS 用户包的自定义身份验证提供程序

Symfony2 实体用户提供者覆盖自定义身份验证提供者

Symfony2 自定义身份验证:用户登录但未通过身份验证

在 Symfony5.3 上自定义新的身份验证错误消息

Symfony:身份验证请求失败:无效的 CSRF 令牌