如何假扮一个用户

3.4 版本
维护中的版本

有时,能够从一个用户切换到另一个用户却毋须登出和再次登入,是有必要的(例如,在你调试或者尝试搞清一个“某用户可见,你却不能重现”的bug时)。

Caution

模拟用户不可兼容 预认证防火墙。原因是,模拟过程的认证状态,需要被保持在服务器端,而预认证信息(SSL_CLIENT_S_DN_EmailREMOTE_USER 或其他)是在每一次请求时发送的。

激活 switch_user 防火墙监听,即轻松搞定模拟用户:

1
2
3
4
5
6
7
8
# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            # ...
            switch_user: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <config>
        <!-- ... -->
 
        <firewall name="main">
            <!-- ... -->
            <switch-user />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'firewalls' => array(
        'main'=> array(
            // ...
            'switch_user' => true,
        ),
    ),
));

要切到其他用户,只需为当前URL添加一个查询字符串(query string)即 _switch_user 参数,同时把用户名作为其值:

1
http://example.com/somewhere?_switch_user=thomas

要切回原来的用户,使用特殊的 _exit 用户名:

1
http://example.com/somewhere?_switch_user=_exit

在模拟期间,用户被赋予了一个特殊的role,叫做 ROLE_PREVIOUS_ADMIN。在模板中,比如,该角色可以用于显示一个“退出模拟用户”之连接:

1
2
3
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
    <a href="{{ path('homepage', {'_switch_user': '_exit'}) }}">Exit impersonation</a>
{% endif %}
1
2
3
4
5
6
7
<?php if ($view['security']->isGranted('ROLE_PREVIOUS_ADMIN')): ?>
    <a href="<?php echo $view['router']->path('homepage', array(
        '_switch_user' => '_exit',
    ) ?>">
        Exit impersonation
    </a>
<?php endif ?>

某些情况下,你可能需要去获取“正在模拟别人的”用户对象,而不是“正被别人模拟的”用户对象。使用以下码段去遍历用户的roles,直至找到一个(持有) SwitchUserRole 的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Security\Core\Role\SwitchUserRole;
 
$authChecker = $this->get('security.authorization_checker');
$tokenStorage = $this->get('security.token_storage');
 
if ($authChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {
    foreach ($tokenStorage->getToken()->getRoles() as $role) {
        if ($role instanceof SwitchUserRole) {
            $impersonatingUser = $role->getSource()->getUser();
            break;
        }
    }
}

当然,这个功能需要基于一个小型用户组才能使用。默认时,访问仅限于有 ROLE_ALLOWED_TO_SWITCH role的用户。这个角色的名称可以通过设置 role 来修改。为强化安全性,你还可以通过 parameter 来更改查询参数名称(query parameter name):

1
2
3
4
5
6
7
8
# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            # ...
            switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
    <config>
        <!-- ... -->
 
        <firewall name="main">
            <!-- ... -->
            <switch-user role="ROLE_ADMIN" parameter="_want_to_be_this_user" />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'firewalls' => array(
        'main'=> array(
            // ...
            'switch_user' => array(
                'role' => 'ROLE_ADMIN',
                'parameter' => '_want_to_be_this_user',
            ),
        ),
    ),
));

事件 

firewall在用户模拟完成之后,立即派遣 security.switch_user 事件。SwitchUserEvent 被传入监听(listener),你可以用该事件来获取现在正在模拟的用户(译注:被模拟的用户)。

当你在模拟一个用户时, 把locale信息“粘连”到用户的Session周期中 一文中的locale将不再更新。下面的代码示例将展示如何改变“被粘连的locale(the sticky locale)”:

1
2
3
4
5
6
# app/config/services.yml
services:
    app.switch_user_listener:
        class: AppBundle\EventListener\SwitchUserListener
        tags:
            - { name: kernel.event_listener, event: security.switch_user, method: onSwitchUser 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
>
    <services>
        <service id="app.switch_user_listener"
            class="AppBundle\EventListener\SwitchUserListener"
        >
            <tag name="kernel.event_listener"
                event="security.switch_user"
                method="onSwitchUser"
            />
        </service>
    </services>
</container>
1
2
3
4
5
// app/config/services.php
$container
    ->register('app.switch_user_listener', 'AppBundle\EventListener\SwitchUserListener')
    ->addTag('kernel.event_listener', array('event' => 'security.switch_user', 'method' => 'onSwitchUser'))
;

Caution

监听器的实现过程,已经假设你 User entity拥有一个 getLocale() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/EventListener/SwitchUserListener.php
namespace AppBundle\EventListener;
 
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
 
class SwitchUserListener
{
    public function onSwitchUser(SwitchUserEvent $event)
    {
        $event->getRequest()->getSession()->set(
            '_locale',
            $event->getTargetUser()->getLocale()
        );
    }
}

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

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