如何设置前后过滤程序

3.4 版本
维护中的版本

在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);

有了这个配置,你的TokenListeneronKernelController方法将会在每个请求中都被执行。如果即将执行的控制器实现了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 方法知道添加的额外头。玩的高兴!

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)