如何创建自定义的表单密码Authenticator

3.4 版本
维护中的版本

参考 如何使用Guard创建一个自定义验证系统 以一个更简单、更灵活的方式来完成类似的自定义身份认证的任务。

假设你希望仅在下午2点至下午4点(UTC)才能访问自己的网站。在本文中,你将学习到对于登录表单((即,你的用户提交他们的用户名和密码的地方))来说应如何去做。

Password Authenticator 

首先创建一个新类去实现 SimpleFormAuthenticatorInterface 接口。最终,它会允许你创建自定义的逻辑来认证用户:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/Acme/HelloBundle/Security/TimeAuthenticator.php
namespace Acme\HelloBundle\Security;
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface;
 
class TimeAuthenticator implements SimpleFormAuthenticatorInterface
{
    private $encoder;
 
    public function __construct(UserPasswordEncoderInterface $encoder)
    {
        $this->encoder = $encoder;
    }
 
    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        try {
            $user = $userProvider->loadUserByUsername($token->getUsername());
        } catch (UsernameNotFoundException $e) {
            // CAUTION: this message will be returned to the client
            // (so don't put any un-trusted messages / error strings here)
            // 警告: 此信息将返回给客户端(不要在这里放置任何敏感信息/错误字符串)
            throw new CustomUserMessageAuthenticationException('Invalid username or password');
        }
 
        $passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());
 
        if ($passwordValid) {
            $currentHour = date('G');
            if ($currentHour < 14 || $currentHour > 16) {
                // CAUTION: this message will be returned to the client
                // (so don't put any un-trusted messages / error strings here)
                // 警告: 此信息将返回给客户端(不要在这里放置任何敏感信息/错误字符串)
                throw new CustomUserMessageAuthenticationException(
                    'You can only log in between 2 and 4!',
                    100
                );
            }
 
            return new UsernamePasswordToken(
                $user,
                $user->getPassword(),
                $providerKey,
                $user->getRoles()
            );
        }
 
        // CAUTION: this message will be returned to the client
        // (so don't put any un-trusted messages / error strings here)
        throw new CustomUserMessageAuthenticationException('Invalid username or password');
    }
 
    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof UsernamePasswordToken
            && $token->getProviderKey() === $providerKey;
    }
 
    public function createToken(Request $request, $username, $password, $providerKey)
    {
        return new UsernamePasswordToken($username, $password, $providerKey);
    }
}

它是如何工作的 

真太好!现在你只需设置一些 配置即可。但首先,不妨看看这个类中的每个方法都做了些什么。

1)createToken 

当Symfony开始处理一个请求时,createToken() 被调用,这是你创建了TokenInterface 对象之处,token包含了你在 authenticateToken() 方法中所需之任何信息,用于认证用户(如,对用户名和密码认证)。

无论你创建了何种Token对象,都会被传入 authenticateToken()

2)supportsToken 

Symfony 调用 createToken() 之后,将调用你的类中的 supportsToken() 方法(以及任何其它的authentication listeners)来计算出应该由谁来处理(当前的)token。这只是一种 “允许多个身份认证机制用于同一防火墙” 的方式(通过这种方式,你可以先尝试使用certificate或者API key来认证用户,[未通过的话]然后再回滚到表单登录)。

多数情况下,对于由 createToken() 方法所创建的token,只需要去让方法返回true。你的逻辑应与本例颇为相似。

3)authenticateToken 

如果 supportsToken() 返回 true,Symfony即调用 authenticateToken()。此时你要做的,是检查token是否被允许登录进来,首先通过user provider来获得 User 对象,然后再检查密码和当前时间。

关于如何获取 User 对象的“流程”,以及确定令牌是否有效(如,检查密码),可能会随着你的需求而变。

最后,你要返回一个新的token对象,它是“authenticated”的(已认证。即,它被设置了至少一个role),同时它还包含了 User 对象。

在这个方法中,需要使用password encoder来检查密码的有效性:

1
$passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());

这是Symfony中一个可用的服务,它使用了你在security配置信息(security.yml)中配置的算法(encoders 键)。下一小节中,你将看到如何把它注入到 TimeAuthenticator 中。

配置 

现在,配置 TimeAuthenticator 令其成为一个服务:

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

    time_authenticator:
        class:     Acme\HelloBundle\Security\TimeAuthenticator
        arguments: ["@security.password_encoder"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- app/config/config.xml -->
<?xml version="1.0" ?>
<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="time_authenticator"
            class="Acme\HelloBundle\Security\TimeAuthenticator"
        >
            <argument type="service" id="security.password_encoder" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
// app/config/config.php
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
// ...
 
$container->setDefinition('time_authenticator', new Definition(
    'Acme\HelloBundle\Security\TimeAuthenticator',
    array(new Reference('security.password_encoder'))
));

然后,在安全配置的 firewalls 区块中通过 simple_form 键来激活它:

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

    firewalls:
        secured_area:
            pattern: ^/admin
            # ...
            simple_form:
                authenticator: time_authenticator
                check_path:    login_check
                login_path:    login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 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="secured_area"
            pattern="^/admin"
            >
            <simple-form authenticator="time_authenticator"
                check-path="login_check"
                login-path="login"
            />
        </firewall>
    </config>
</srv:container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app/config/security.php
 
// ..
 
$container->loadFromExtension('security', array(
    'firewalls' => array(
        'secured_area'    => array(
            'pattern'     => '^/admin',
            'simple_form' => array(
                'provider'      => ...,
                'authenticator' => 'time_authenticator',
                'check_path'    => 'login_check',
                'login_path'    => 'login',
            ),
        ),
    ),
));

simple_form 键和通常的 form_login 选项有相同的配置子项,但多了一个 authenticator 键用于指向这个新的服务。更多细节,参考 表单登录配置

如果你对创建登录表单还是个新手,或者不理解 check_pathlogin_path 选项,参考 如何自定义你的表单登录

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

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