如何设置前后过滤程序
在Web应用程序开发中,有种常见的操作,就是需要一些逻辑作为过滤程序或钩子,在你的控制器动作之前或之后执行。
有些web框架定义了像preExecute()
和 postExecute()
的方法,但在symfony中完全没有。好消息是有一个更好的方法,就是你可以使用EventDispatcher component来干预Request -> Response 的进程。
Token验证示例 ¶
假设你需要开发一个API,其中一些控制器是公开的,但还是有一些其他的控制器会限制某些客户端的访问。对于这样私有的特性,您可以为客户端提供一个token来进行自我验证。
所以,在执行你的控制器动作之前,你需要去检查这个动作(action)是否受到限制。如果他被限制,你需要去验证他提供的token。
请记住本教程为了简洁,token被定义在了配置中,并且不管是把token设置在数据库还是认证中,都必须通过 Security component才能够被使用。
在kernel.controller事件之前过滤 ¶
首先,使用config.yml
存储一些基础的token配置并使用parameters键:
1 2 3 4 5 |
# app/config/config.yml
parameters:
tokens:
client1: pass1
client2: pass2 |
1 2 3 4 5 6 7 |
<!-- app/config/config.xml -->
<parameters>
<parameter key="tokens" type="collection">
<parameter key="client1">pass1</parameter>
<parameter key="client2">pass2</parameter>
</parameter>
</parameters> |
1 2 3 4 5 |
// app/config/config.php
$container->setParameter('tokens', array(
'client1' => 'pass1',
'client2' => 'pass2',
)); |
标记要检查的控制器 ¶
kernel.controller
监听器会获取每个请求上的通知,恰好在控制器执行之前获取到。所以,首先,你需要一些方法来确定这个控制器是否需要匹配请求的token验证。
一个简洁容易的方法是创建一个空接口并让控制器去实现它:
1 2 3 4 5 6 |
namespace AppBundle\Controller;
interface TokenAuthenticatedController
{
// ...
} |
一个控制器要实现这个接口,简单看起来就是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
namespace AppBundle\Controller;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller implements TokenAuthenticatedController
{
// An action that needs authentication
public function barAction()
{
// ...
}
} |
创建一个事件监听器 ¶
接下来,您将需要创建一个事件监听器,它保存了在您控制器之前要执行的逻辑。如果你不熟悉事件监听器,你可以在Events 和Event Listeners这里学到更多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// src/AppBundle/EventListener/TokenListener.php
namespace AppBundle\EventListener;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class TokenListener
{
private $tokens;
public function __construct($tokens)
{
$this->tokens = $tokens;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof TokenAuthenticatedController) {
$token = $event->getRequest()->query->get('token');
if (!in_array($token, $this->tokens)) {
throw new AccessDeniedHttpException('This action needs a valid token!');
}
}
}
} |
注册监听器 ¶
最后,注册你的监听器作为一个服务并且标记他为一个事件监听器。通过监听kernel.controller
,您就可以告知 Symfony 您想要在任何控制器执行前调用监听器。
1 2 3 4 5 6 7 |
# app/config/services.yml
services:
app.tokens.action_listener:
class: AppBundle\EventListener\TokenListener
arguments: ['%tokens%']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController } |
1 2 3 4 5 |
<!-- app/config/services.xml -->
<service id="app.tokens.action_listener" class="AppBundle\EventListener\TokenListener">
<argument>%tokens%</argument>
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
</service> |
1 2 3 4 5 6 7 8 9 |
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%'));
$listener->addTag('kernel.event_listener', array(
'event' => 'kernel.controller',
'method' => 'onKernelController'
));
$container->setDefinition('app.tokens.action_listener', $listener); |
有了这个配置,你的TokenListener
的onKernelController
方法将会在每个请求中都被执行。如果即将执行的控制器实现了TokenAuthenticatedController
,token认证将被启用。它让你可以在像要的任何控制器“前”加过滤器。
在kernel.response事件之后过滤 ¶
除了在你的控制器之前有一个“hook”被执行,你也可以加入一个钩子“hook”在你的控制器之后执行。在这个例子中,假设你想要添加一个sha1哈希(使用token的salt)到所有通过token认证的响应中。
另一个symfony核心事件 - 叫做kernel.response
- 在每一项请求都会通知它,但是,是在控制器会返回一个响应对象之后。创建一个“后”监听器如同创建一个监听器类一样容易,并把他注册为这个事件的服务。
例如,从之前的例子中获取TokenListener
并第一时间记录认证token到请求属性中。这就是一个标记(flag ),表示此请求经过了token认证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function onKernelController(FilterControllerEvent $event)
{
// ...
if ($controller[0] instanceof TokenAuthenticatedController) {
$token = $event->getRequest()->query->get('token');
if (!in_array($token, $this->tokens)) {
throw new AccessDeniedHttpException('This action needs a valid token!');
}
// mark the request as having passed token authentication
$event->getRequest()->attributes->set('auth_token', $token);
}
} |
现在,为这个类添加另一个方法 - onKernelResponse
- 寻找请求属性上的标记(flag),如果找到了就设置一个自定义的响应头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// add the new use statement at the top of your file
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
public function onKernelResponse(FilterResponseEvent $event)
{
// check to see if onKernelController marked this as a token "auth'ed" request
if (!$token = $event->getRequest()->attributes->get('auth_token')) {
return;
}
$response = $event->getResponse();
// create a hash and set it as a response header
$hash = sha1($response->getContent().$token);
$response->headers->set('X-CONTENT-HASH', $hash);
} |
最终,第二个“tag”也需要在服务中定义,来通知 Symfony的onKernelResponse
事件应该被 kernel.response
通知:
1 2 3 4 5 6 7 8 |
# app/config/services.yml
services:
app.tokens.action_listener:
class: AppBundle\EventListener\TokenListener
arguments: ['%tokens%']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } |
1 2 3 4 5 6 |
<!-- app/config/services.xml -->
<service id="app.tokens.action_listener" class="AppBundle\EventListener\TokenListener">
<argument>%tokens%</argument>
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
<tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" />
</service> |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$listener = new Definition('AppBundle\EventListener\TokenListener', array('%tokens%'));
$listener->addTag('kernel.event_listener', array(
'event' => 'kernel.controller',
'method' => 'onKernelController'
));
$listener->addTag('kernel.event_listener', array(
'event' => 'kernel.response',
'method' => 'onKernelResponse'
));
$container->setDefinition('app.tokens.action_listener', $listener); |
就这样了!TokenListener
现在在每个控制器执行之前会被通知(onKernelController
),每个控制器执行之后会返回一个响应(onKernelResponse
)。通过让具体的控制器去实现TokenAuthenticatedController
接口,让您的监听器知道哪个控制器该采取行动的。并通过在请求“attributes”包里存储一个值,让onKernelResponse
方法知道添加的额外头。玩的高兴!