如何添加“记住我”登录功能

3.4 版本
维护中的版本

一旦用户被认证,他们的凭证通常存储在session中。这意味着当session结束时,他们会被注销,下次访问程序时会被再次要求输入登录信息。使用带有 remember_me 防火墙选项的cookie,你可以让用户选择“登录进来”的停留时间比session的(周期)更长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app/config/security.yml
security:
    # ...

    firewalls:
        main:
            # ...
            remember_me:
                secret:   '%secret%'
                lifetime: 604800 # 1 week in seconds
                path:     /
                # by default, the feature is enabled by checking a
                # checkbox in the login form (see below), uncomment the
                # following line to always enable it.
                # 默认时,此功能通过在登录表单中选中一个checkbox来启用(见下文)
                # 如果取消下面这行的注释,则将始终开启。
                #always_remember_me: true
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
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="utf-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:srv="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">
 
    <config>
        <!-- ... -->
 
        <firewall name="main">
            <!-- ... -->
 
            <!-- 604800 is 1 week in seconds -->
            <remember-me
                secret="%secret%"
                lifetime="604800"
                path="/" />
            <!-- by default, the feature is enabled by checking a checkbox
                 in the login form (see below), add always-remember-me="true"
                 to always enable it. -->
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'firewalls' => array(
        'main' => array(
            // ...
            'remember_me' => array(
                'secret'   => '%secret%',
                'lifetime' => 604800, // 1 week in seconds
                'path'     => '/',
                // by default, the feature is enabled by checking a
                // checkbox in the login form (see below), uncomment
                // the following line to always enable it.
                //'always_remember_me' => true,
            ),
        ),
    ),
));

firewall的 remember_me 定义了如下配置选项:

secret (必填项)
这个值用于加密cookie的内容。它一般使用定义在 app/config/parameters.yml 文件中的 secret 值。
name (默认值: REMEMBERME)
用于保存用户登录的cookie之名称。如果你在同一程序的多个防火墙中开启 remember_me 功能,确保为每个防火墙的cookie选择不同的名称。否则,你将面临很多安全相关问题。
lifetime (默认值: 31536000)
用户保持登录状态的秒数。默认时,用户登录状态保存一年。
path (默认值: /)
与此功能相关的cookie所使用的路径。默认时,这个cookie将用于整个网站,但你可以把它限定在一个特殊区域(如 /forum, /admin)。
domain (默认值: null)
与此功能相关的cookie所使用的域。默认时,cookie使用的是从 $_SERVER 中获得的当前域。
secure (默认值: false)
若设为 true,与此功能相关的cookie,将通过HTTPS安全连接发送至用户。
httponly (默认值: true)
若设为 true,与此功能相关的cookie仅能通过HTTP协议被访问到。这意味着cookie不能通过脚本语言访问,比如JavaScript。
remember_me_parameter (默认值: _remember_me)
检查表单字段的name,以决定是否启用“Remember Me”功能。继续阅读本文来了解如何有条件的开启此功能。
always_remember_me (默认值: false)
若设为 trueremember_me_parameter 的值会被忽略,不管末级用户是否想,"Remember Me" 功能将始终开启。
token_provider (默认值: null)
定义一个要用到的token provider的服务id。默认时,token存储在cookie中。例如,你可能想把token存储在数据库中,以不在cookie中持有一个(hashed)版本的密码。DoctrineBridge 自带了一个 Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider 供你使用。

强制用户不使用Remember Me功能 

让用户自由选择使用或者不使用自动登陆功能是个好主意,因为此功能并不总是恰如其分。实现它的常规办法在从登录表单中添加一个复选框(checkbox)。然后,把复选框的 name 设成 _remember_me(或者使用你在 remember_me_parameter 中所配置的),当复选框被勾选时cookie将被自动设置,同时用户会成功登陆。所以,你的定制表单登录代码最终看上去像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{# app/Resources/views/security/login.html.twig #}
{% if error %}
    <div>{{ error.message }}</div>
{% endif %}
 
<form action="{{ path('login') }}" 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" />
 
    <input type="checkbox" id="remember_me" name="_remember_me" checked />
    <label for="remember_me">Keep me logged in</label>
 
    <input type="submit" name="login" />
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- app/Resources/views/security/login.html.php -->
<?php if ($error): ?>
    <div><?php echo $error->getMessage() ?></div>
<?php endif ?>
 
<form action="<?php echo $view['router']->path('login') ?>" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username"
           name="_username" value="<?php echo $last_username ?>" />
 
    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />
 
    <input type="checkbox" id="remember_me" name="_remember_me" checked />
    <label for="remember_me">Keep me logged in</label>
 
    <input type="submit" name="login" />
</form>

只要 cookie 仍然有效,在之后的访问中,用户将自动登录。   

强制用户在访问特定资源之前再次认证 

当用户回到你的网站时,他们将基于remember me cookie中的信息被自动认证。这可让用户访问受保护的资源,如同用户在访问过程中真的已被认证过了。

然而在某些场合,你可能想要用户在访问某资源之前再次接受认证。例如,你可能想让 “remember me” 自动登录的用户去查看账户基本信息,但如果他们想要修改信息的话就要再次经过真正意义的认证。

Security组件提供了一个很好的方法来完成此事。除了显式地赋予用户role之外,根据其认证方式,用户们会被自动分配以下roles之一:

IS_AUTHENTICATED_ANONYMOUSLY
自动地赋给那些“处在网站防火墙内受保护部分”的用户,这些用户未真实登录进来。仅在匿名访问时才会发生。
IS_AUTHENTICATED_REMEMBERED
自动地赋给那些“通过remember mecookie被认证”的用户。
IS_AUTHENTICATED_FULLY
自动地赋给那些“在当前session中已经提交过登录细节”的用户。

除了根据“显式分配出去的roles”(来进行访问控制),你可以使用上面这些(自动分配的)roles来进行访问控制。

如果你有 IS_AUTHENTICATED_REMEMBERED role,那么你也会有 IS_AUTHENTICATED_ANONYMOUSLY role。如果你有 IS_AUTHENTICATED_FULLY role,你会有另外两个角色。换言之,这些角色代表了三个“强度递增”的认证级别。 

你可以使用这些附加的roles来对网站的受访部分进行精细控制。例如,你可能希望用户在cookie被验证后能够查看他们在 /account 下的的账户,但必须提供登录细节才能够编辑其账户信息。为此,你可以对控制器的特定action使用这些角色来强化安全性。控制器的editAction,使用service context以获得保护。

下例中,这个Action仅在用户持有 IS_AUTHENTICATED_FULLY 角色时可以访问。

1
2
3
4
5
6
7
8
9
10
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException
 
// ...
public function editAction()
{
    $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
 
    // ...
}

如果你的程序基于Symfony标准版,你也可以使用annotations方式来保护你的控制器:

1
2
3
4
5
6
7
8
9
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
 
/**
 * @Security("has_role('IS_AUTHENTICATED_FULLY')")
 */
public function editAction($name)
{
    // ...
}

如果你同时在security配置信息中也安排了一个访问控制(access control),要求用户持有一个 ROLE_USER role才能访问其账号功能的任意页面,那么你可能会遇到以下情况:

  • 如果未认证(non-authenticated。或anonymously authenticated匿名认证)尝试访问账户区域,该用户将被要求进行身份认证。

  • 一旦用户输入自己的用户名和密码,假设用户获得到了你配置的 ROLE_USER role,这个用户将有IS_AUTHENTICATED_FULLY role,并能够访问账户部分的任意页面,包括 editAction 控制器。

  • 当用户返回网站时,如果其session已结束,他们将能够访问每个账户页面 — 除了编辑页面之外 — 其他页面未要求强制认证。然而,当他们试图访问 editAction 控制器时,将被迫进行“再认证”,因为他们都没有进行fully authenticated(译注:可理解为“提供登录细节”的完全认证)。

以这种方式来保护service(服务)或method(服务中的方法)之信息,参考 如何保护程序中的服务和方法

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

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