两个防火墙,具有两个login_form和两个不同的AuthenticationSuccessHandler,但仅一个用


Two firewalls with two login_form and two different AuthenticationSuccessHandlers but only one used for both

几乎所有内容都在标题中。

我之前来这里是为了确定这是一个错误,然后将其作为symfony/symfony上的问题进行报告。

我有以下证券。yml:

security:
    encoders:
        Symfony'Component'Security'Core'User'User: plaintext
    providers:
        in_memory:
            memory:
                users:
                    chalasr: { password: chalasr, roles: [ 'ROLE_USER' ] }
    firewalls:
        admin:
            pattern:            ^/admin
            form_login:
                provider:       in_memory
                login_path:     /admin/login
                check_path:     /admin/login_check
                failure_path:   null
                success_handler: admin.authentication_success_handler
            logout:
                path:           /admin/logout
            anonymous:          true
        login_api:
            pattern:  ^/v1/login
            stateless: true
            anonymous: true
            form_login:
                provider: in_memory
                check_path: /v1/login_check
                require_previous_session: false
                username_parameter: username
                password_parameter: password
                success_handler: api.authentication_success_handler
        api:
            pattern:   ^/v1/
            stateless: true
            lexik_jwt: ~
    access_control:
        - { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_USER }
        - { path: ^/v1/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/v1/, role: ROLE_USER }

正如您所看到的,我有两个登录防火墙,一个用于与^/admin匹配的路由,另一个用于那些与^/v1匹配的路由
两个form_login具有一个相应的authentication_success_handler集合。

api处理程序:

use Symfony'Component'HttpFoundation'Request;
use Symfony'Component'Security'Core'Authentication'Token'TokenInterface;
 class ApiAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
        die('interecepted by api');
    }
}

管理员处理程序:

use Symfony'Component'HttpFoundation'Request;
use Symfony'Component'Security'Core'Authentication'Token'TokenInterface;
class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        die('interecepted by admin');
    }
}

服务:

admin.authentication_success_handler:
    class: AppBundle'EventListener'AdminAuthenticationSuccessHandler
    tags:
        - { name: kernel.event_listener, event: security.on_authentication_success, method: onAuthenticationSuccess }
api.authentication_success_handler:
    class: AppBundle'EventListener'ApiAuthenticationSuccessHandler
    tags:
        - { name: kernel.event_listener, event: security.on_authentication_success, method: onAuthenticationSuccess }

问题是admin.authentication_success_handler用于两个防火墙,它说"被管理员拦截",用于api登录路由和管理员登录路由(表单)。

我的第一个目标是制作完全依赖于所使用的防火墙的东西,由于admin/api的分离,这种行为在我的应用程序中导致了一个错误
一开始,当我发现这个错误时,我在Symfony 2.8上使用LexikJWTAuthenticationBundle和FOSUserBundle作为api,使用SonataAdminBundle(带FOSUB)作为管理员。
然后我升级到3.0以上,删除了任何第三方安全捆绑包,以避免对这些捆绑包产生任何疑问,并确保这是来自symfony的。(这就是为什么我使用一个简单的明文编码器,尽可能简单)。

当我认为这是一个bug时,我是对的吗?或者我只是做错了什么?

因为这件事显然还没有文档化,我们可以找到很多不同的(有效或无效)实现。

编辑

嗨@tftd,谢谢你的工作。

我真的很抱歉,但我只是缩短了代码,使问题变得可读,所以从我的角度来看,这两个代码中缺少的anonymous: true只是一个简单的拼写错误,我添加了它们。

事实上,在一个完成的项目中,实现工作得很好,它在不同的主机上被分隔为一个api和一个admin,admin防火墙具有FOSUserBundle提供的登录表单,api防火墙具有一个简单的/login_check端点,该端点提供JWT,然后用户将经过身份验证的请求发送到/v1/*路由,并将令牌作为头(承载)。

我升级到3.0以进入阶段,我使用了一个简单的in_memory来避免对第三方捆绑包的任何怀疑(FOSUserBundle成功处理、LexikJWT成功处理或任何其他可能导致错误的原因),我希望我真的避免对它们的任何怀疑。

你可以看到我的简单routing.yml(请记住,我用一个样本来展示这个问题,这个样本只是用来观察登录成功的表现,它绝对不是真实世界的用法,但我在真实世界的使用中也有同样的行为)。

// app/config/routing.yml
fos_user:
    prefix: /admin
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"
api_login_check:
    path: /v1/login_check

路由/admin/*由提供登录表单的FOSUserBundle管理,我的目标只是看看两次登录成功是否被拦截,事实上,它们确实被拦截了。

问题来自于我的两个听众正在收听security.interactive_login,它看起来像这样:

class ApiAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
        return $this->onAuthenticationSuccess($event->getRequest(), $event->getAuthenticationToken());
    }
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        die('Interecepted by api');
    }
}

这两个服务声明看起来像:

api.authentication_success_handler:
    class: AppBundle'EventListener'AuthenticationSuccessHandlerApi ]
    tags:
        - { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }

设置了标记后,只会激发一个侦听器
通过删除标记,可以在每个防火墙上正确调用相应的处理程序。

结论,不需要标记AuthenticationSuccessListener(除了抛出onAuthenticationSuc成功之外的任何其他原因),如果对其进行标记,请将其标记为security.on_authentication_success事件,而不是在security.interactive_login上,因为无论security.yml中定义了什么处理程序,第一个都将用于InteractiveLogin,因此第二个将被完全忽略。

感谢@Federico的精彩回答,再次感谢@tdtd。

我认为问题在于当您将AdminAuthenticationSuccessHandleronAuthenticationSuccess方法订阅到事件security.on_authentication_success时。每次用户通过一个提供者的身份验证时,它都会被触发,不管是哪一个。由于您首先注册了admin.authentication_success_handler,因此它将首先运行。

我不太确定,因为事件名称是security.authentication.success,但请尝试使用更改您的服务定义

admin.authentication_success_handler:
    class: AppBundle'EventListener'AdminAuthenticationSuccessHandler
api.authentication_success_handler:
    class: AppBundle'EventListener'ApiAuthenticationSuccessHandler

我不太清楚为什么您的配置没有显示"循环"错误。根据官方文档,匿名用户应该可以访问您的login路径,login_check应该在您的防火墙下。在当前情况下,/admin/下有login路径,需要ROLE_USER

您的配置应该是这样的:

安全:

encoders:
    Symfony'Component'Security'Core'User'User: plaintext
providers:
    in_memory:
        memory:
            users:
                user: { password: password, roles: [ 'ROLE_USER' ] }
firewalls:
    dev:
        pattern:  ^/(_(profiler|wdt)|css|images|js)/
        security: false
    admin_login:
        anonymous: ~
        pattern: ^/admin/login$
    admin:
        pattern:        ^/admin
        provider:       in_memory
        form_login:
            login_path:             /admin/login
            check_path:             /admin/login_check
            default_target_path:    /admin/
            success_handler:        admin.authentication_success_handler
        logout:
            path:       /admin/logout
            target:     /admin/login
    api:
        pattern:             ^/v1
        stateless:           true
        form_login:
            provider:        in_memory
            check_path:      /v1/login_check
            success_handler: api.authentication_success_handler
            require_previous_session: false

access_control:
    - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/admin, role: ROLE_USER }
    - { path: ^/v1/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/v1, role: ROLE_USER }

在您的routing.yml中,确保您已经更改了模板中的链接:

admin_logout:
    path: /admin/logout
admin_login_check:
    path: /admin/login_check
api_login_check:
    path: /v1/login_check

SecurityController:

<?php
namespace AppBundle'Controller;
use Sensio'Bundle'FrameworkExtraBundle'Configuration'Template;
use Symfony'Bundle'FrameworkBundle'Controller'Controller;
use Symfony'Component'HttpFoundation'Request;
use Symfony'Component'Routing'Annotation'Route;
class SecurityController  extends Controller
{
    /**
     * @Route("/login",       name="api_login")
     * @Route("/admin/login", name="admin_login")
     * @Template("::login.html.twig")
     */
    public function loginAction(Request $request)
    {
        $authenticationUtils = $this->get('security.authentication_utils');
        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();
        return array(
            // last username entered by the user
            'last_username' => $lastUsername,
            'error'         => $error,
            'login_path'    => $request->get('_route') == 'admin_login' ? '/admin/login_check' : '/v1/login_check'
        );
    }
}
?>

app/Resources/views/login.html.twig模板:

{% extends '::base.html.twig' %}
{% block body %}
    {% if error %}
        <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}
    <form action="{{ login_path }}" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
        <button type="submit">login</button>
    </form>
{% endblock %}

服务和成功处理程序的定义与您发布的内容完全相同。

上面的代码是一个概念验证,它向您展示了不同的防火墙在不同的success_handler中可以很好地工作。之所以如此,是因为在每个防火墙中,您都定义了在成功进行身份验证时要调用的success_handler。此外,在现实生活中,您不需要/v1/login,因为对APIPOST请求应该包含一个带有身份验证/api密钥的头。

至于您的API实现,我觉得您的方法可能是错误的。不久前,我使用ApiKeyAuthenticator实现来创建RESTAPI。了解如何启动API实现是一个很好的"基础"。但是,对于较新版本的Symfony(3.0+),您可以使用Guard身份验证,这似乎更容易一些。