> Symfony中文手册 > 如何建立一个传统的登录表单

如何建立一个传统的登录表单

如果你需要一个登录表单,并且把用户存到某种类型的数据库中,那么你应该考虑使用 FOSUserBundle,它帮助你建立 User 对象,还提供了常见任务所需的的路由和控制器,包括登录、注册、找回密码等。

在本文中,你将构建一个传统的登录表单。当然,在用户登录时,你可以从任何地方加载他们 - 比如数据库。参考 配置“如何加载用户”。

首先,在firewall中开启“表单登录”:

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

    firewalls:
        main:
            anonymous: ~
            form_login:
                login_path: login
                check_path: login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 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">
            <anonymous />
            <form-login login-path="login" check-path="login" />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
// app/config/security.php
$container->loadFromExtension('security', array(
    'firewalls' => array(
        'main' => array(
            'anonymous'  => null,
            'form_login' => array(
                'login_path' => 'login',
                'check_path' => 'login',
            ),
        ),
    ),
));

login_pathcheck_path 也可以是路由名称(但不能有任何通配符 - 如 /login/{foo}foo 并没有默认值)。

现在,当安全系统初始化认证过程时,它会把用户重定向到登录表单 /login。实现这个登录表单是你要做的事。首先,在bundle中创建一个新的 SecurityController

1
2
3
4
5
6
7
8
// src/AppBundle/Controller/SecurityController.php
namespace AppBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class SecurityController extends Controller
{
}

接下来,配置路由,也就是之前你用在 form_login 配置中的(login):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/AppBundle/Controller/SecurityController.php
 
// ...
use Symfony\Component\Httpfoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
 
class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function loginAction(Request $request)
    {
    }
}
1
2
3
4
# app/config/routing.yml
login:
    path:     /login
    defaults: { _controller: AppBundle:Security:login }
1
2
3
4
5
6
7
8
9
10
11
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing
        http://symfony.com/schema/routing/routing-1.0.xsd">
 
    <route id="login" path="/login">
        <default key="_controller">AppBundle:Security:login</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
 
$collection = new RouteCollection();
$collection->add('login', new Route('/login', array(
    '_controller' => 'AppBundle:Security:login',
)));
 
return $collection;

很好!下一步,添加 loginAction 的逻辑以显示登录表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/AppBundle/Controller/SecurityController.php
 
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 / 获取用户输入的username(用户名)
    $lastUsername = $authenticationUtils->getLastUsername();
 
    return $this->render(
        'security/login.HTML.twig',
        array(
            // last username entered by the user
            'last_username' => $lastUsername,
            'error'         => $error,
        )
    );
}

别被这个控制器迷惑。你即将看到,当用户提交表单时,security系统会自动帮你处理表单提交。如果用户提交了无效的用户名和密码,该控制器会从security系统中读取表单提交的错误,再显示给用户。

换句话说,你的任务是显示 登录表单以及任何可能发生的登录错误(login error),但是安全系统本身会负责检查“已提交的用户名和密码”并对用户进行认证。

最后,创建模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{# app/Resources/views/security/login.html.twig #}
{# ... you will probably extends your base template, like base.html.twig #}
{# ... 这里你也许会继承基础布局模板,如 base.html.twig #}
 
{% if error %}
    <div>{{ error.messageKey|trans(error.messageData, 'security') }}</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" />
 
    {#
        If you wAnt to control the URL the user
        is redirected to on success (more details below)
        如果你要控制“用户成功登录后被重定向的URL”的话(下文有更多细节)
        <input type="hidden" name="_target_path" value="/account" />
    #}
 
    <button type="submit">login</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- src/AppBundle/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" />
 
    <!--
        If you want to control the URL the user
        is redirected to on success (more details below)
        <input type="hidden" name="_target_path" value="/account" />
    -->
 
    <button type="submit">login</button>
</form>

传入到模版的 error 变量是 AuthenticationException 的实例。它可以包含很多信息- 甚至是敏感信息 - 即关于认证失败的,要明智地使用之。

表单看上去可以千奇百怪,但它通常遵循如下约定:

  • form 元素发送一个 POST 请求到 login 路由,因为你在 security.ymlform_login 键中是这样配置的;

  • 用户名字段的name必须是 _username 而密码字段的name则必须是 _password

其实所有这些都可以在 form_login 键下进行配置,参考 表单登录配置。

此登录表单目前不能对CSRF攻击进行防护。阅读 使用CSRF保护登录表单 以了解如何保护表单。

妥了!当你提交表单时,security系统会自动检查用户的凭证,要么通过认证,要么把用户送回“显示有错误信息”的登陆表单。

回顾整个过程:

  1. 用户试图访问受保护的资源;

  2. 防火墙通过“将用户重定向到登录表单(/login)”启用认证进程;

  3. 本例中,通过路由(route)和控制器(controller)来渲染出(译注:render,即输出之意)/login 页面的登录表单;

  4. 用户提交表单到 /login

  5. security系统拦截请求,检查用户提交的凭据,若正确则认证之,如果不正确,用户会被送回登录表单。

登录成功后的重定向 ¶

如果提交的凭证是正确的,用户会被重新定向到其所请求的原始页面(如 /admin/foo)。如果用户最初直接进入的登录页面,他们会被重定向到首页(homepage)。这些都是可以设定的,允许你,比如,将用户重定向到一个指定的url上。

关于此点的更多细节,以及如何从大体上自定义表单登录进程,请参考 如何自定义你的表单登录。

避免常见误区 ¶

在设置表单时应注意一些常见的陷阱。

1.创建正确的路由 ¶

首先,确保你定义了正确的 /login 路由,它和你的 login_pathcheck_path 的配置值是一样的。如果配置错误,有可能会导致重定向到一个404页面而不是登录页面,或者在表单提交后不执行任何操作(你会一遍又一遍的看到登录表单)。

2. 确保登录页面毋须认证(循环重定向!) ¶

此外,要确保登录页面可以被匿名用户访问。例如,下面的配置 - 所有URLs皆需要 ROLE_ADMIN role(包括 /login URL),将引发重定向循环:

1
2
3
4
5
# app/config/security.yml
 
# ...
access_control:
    - { path: ^/, roles: ROLE_ADMIN }
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 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>
        <!-- ... -->
        <rule path="^/" role="ROLE_ADMIN" />
    </config>
</srv:container>
1
2
3
4
5
6
// app/config/security.php
 
// ...
'access_control' => array(
    array('path' => '^/', 'role' => 'ROLE_ADMIN'),
),

向access_control中添加一个 /login/*,即毋须认证身份而修复问题:

1
2
3
4
5
6
# app/config/security.yml
 
# ...
access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: ROLE_ADMIN }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 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>
        <!-- ... -->
        <rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY" />
        <rule path="^/" role="ROLE_ADMIN" />
    </config>
</srv:container>
1
2
3
4
5
6
7
// app/config/security.php
 
// ...
'access_control' => array(
    array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
    array('path' => '^/', 'role' => 'ROLE_ADMIN'),
),

3. 确保check_path受防火墙保护 ¶

接着,要确保你的 check_path URL (如 /login) 处在表单登录的防火墙之内(本例中,单一防火墙匹配了所有 URL,包括 /login)。如果 /login 无法匹配到任何防火墙,你会收到一个 Unable to find the controller for path “/login” 异常。

4. 多个防火墙不能共享同一Security上下文 ¶

如果你使用多个防火墙并对其中的一个开展认证,其他的防火墙就不会 自动对你进行认证了。不同的防火墙,就像不同的安全系统。要实现这一点,你必须对不同的防火墙显式指定相同的 Firewall Context。然而一般来说,有一个主力防火墙就足够了。

5. 路由错误页不受防火墙限制 ¶

由于routing是在security之前 完成,404页不受防火墙限制。这意味着在这些页面你无法检查安全性,甚至访问到user对象。参考 如何自定义错误页。