几乎所有内容都在标题中。
我之前来这里是为了确定这是一个错误,然后将其作为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。
我认为问题在于当您将AdminAuthenticationSuccessHandler
的onAuthenticationSuccess
方法订阅到事件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
,因为对API
的POST
请求应该包含一个带有身份验证/api密钥的头。
至于您的API实现,我觉得您的方法可能是错误的。不久前,我使用ApiKeyAuthenticator实现来创建RESTAPI。了解如何启动API实现是一个很好的"基础"。但是,对于较新版本的Symfony(3.0+),您可以使用Guard身份验证,这似乎更容易一些。