认证(Authentication)

3.4 版本
维护中的版本

当一个请求指向一个受保护区域时,防火墙映射(firewall map)中的某个监听,将从当前 Request 对象中提取出用户的凭据,然后创建一个包含有这些凭据的token。下一步,监听器要请求authentication manager(认证管理器)来认证这个给定的token,并且在“(token中的)凭据被找到并且有效”时返回一个authenticated token 。然后,监听要利用 token storage 来存储这个authenticated token:

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
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
 
class SomeAuthenticationListener implements ListenerInterface
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;
 
    /**
     * @var AuthenticationManagerInterface
     */
    private $authenticationManager;
 
    /**
     * @var string Uniquely identifies the secured area
     * 用作唯一识别受保护区域的字符串
     */
    private $providerKey;
 
    // ...
 
    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();
 
        $username = ...;
        $password = ...;
 
        $unauthenticatedToken = new UsernamePasswordToken(
            $username,
            $password,
            $this->providerKey
        );
 
        $authenticatedToken = $this
            ->authenticationManager
            ->authenticate($unauthenticatedToken);
 
        $this->tokenStorage->setToken($authenticatedToken);

一个token,可以是任何类,只要它实现了 TokenInterface

Authentication Manager 

默认的authentication manager是 AuthenticationProviderManager 的一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
 
// instances of Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface
// 由AuthenticationProviderInterface的实例组成的数组
$providers = array(...);
 
$authenticationManager = new AuthenticationProviderManager($providers);
 
try {
    $authenticatedToken = $authenticationManager
        ->authenticate($unauthenticatedToken);
} catch (AuthenticationException $failed) {
    // authentication failed
}

AuthenticationManager 被实例化时,会接收若干个authentication provider,每一个都支持一个不同类型的token。

你可以写自己的authentication manager,它只要实现 AuthenticationManagerInterface 接口即可。

Authentication Providers 

每一个provider(因为它要实现 AuthenticationProviderInterface 接口)都有一个 supports()方法,通过该方法, AuthenticationProviderManager 可以知道自己是否支持给定的token。如果token受支持,则manager会调用provider的 authenticate方法。本方法返回一个authenticated token(已认证token)或者抛出一个 AuthenticationException 认证异常(或其他继承它的异常)。

通过用户名和密码认证用户 

一个authentication provider在尝试认证用户时,基于的是用户提供的凭据(credentials)。通常会是用户名和密码这些。多数web程序会把用户的用户名和“用一个随机生成的混淆值(salt)加密过”的密码存储起来。这意味着认证过程包括了从用户的token storage中提取出salt和加密的密码,用salt混淆来加密由用户提供(比如通过登陆表单)的密码,然后比较两边来判断给定的密码是否有效。

这个功能已被 DaoAuthenticationProvider 提供。它从 UserProviderInterface 中取出用户数据,使用 PasswordEncoderInterface 来创建加密后的密码,并且在密码有效时返回一个authenticated token:

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
use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
 
$userProvider = new InMemoryUserProvider(
    array(
        'admin' => array(
            // password is "foo"
            'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
            'roles'    => array('ROLE_ADMIN'),
        ),
    )
);
 
// for some extra checks: is account enabled, locked, expired, etc.?
// 做特殊检查:账号是否开启、锁定、过期?
$userChecker = new UserChecker();
 
// an array of password encoders (see below)
// password encoder的数组
$encoderFactory = new EncoderFactory(...);
 
$provider = new DaoAuthenticationProvider(
    $userProvider,
    $userChecker,
    'secured_area',
    $encoderFactory
);
 
$provider->authenticate($unauthenticatedToken);

上例演示了“in-memory”类型的user provider的用法,但是你可以使用其他任何user provider,只要它实现的是 UserProviderInterface 接口。还有一种可能,是让多个user provider来找出用户信息,这时要使用 ChainUserProvider

Password Encoder Factory/密码加密器工厂 

DaoAuthenticationProvider 使用了一个encoder factory来为一个给定类型的用户创建“密码加密器”(password encoder)。这能让你针对不同类型的用户使用不同的加密策略。默认的 EncoderFactory 接收一个加密器的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Security\Core\Encoder\EncoderFactory;
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
 
$defaultEncoder = new MessageDigestPasswordEncoder('sha512', true, 5000);
$weakEncoder = new MessageDigestPasswordEncoder('md5', true, 1);
 
$encoders = array(
    'Symfony\\Component\\Security\\Core\\User\\User' => $defaultEncoder,
    'Acme\\Entity\\LegacyUser'                       => $weakEncoder,
 
    // ...
);
 
$encoderFactory = new EncoderFactory($encoders);

每一个encoder都应该实现 PasswordEncoderInterface 接口,或者是一个由 class 键和 arguments 键组成的数组,该数组可以让encoder factory仅在“需要的时候”来构造encoder。

创建自定义的密码加密器 

有很多内置的password encoder。但如果你想创建自己的,只需遵循以下规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
 
class FoobarEncoder extends BasePasswordEncoder
{
    public function encodePassword($raw, $salt)
    {
        if ($this->isPasswordTooLong($raw)) {
            throw new BadCredentialsException('Invalid password.');
        }
 
        // ...
    }
 
    public function isPasswordValid($encoded, $raw, $salt)
    {
        if ($this->isPasswordTooLong($raw)) {
            return false;
        }
 
        // ...
}

使用Password Encoder 

password encoder的 getEncoder()方法被调用时,user对象是它的第一个参数,它返回一个 PasswordEncoderInterface 类型的encoder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a Acme\Entity\LegacyUser instance
// LegacyUser实例
$user = ...;
 
// the password that was submitted, e.g. when registering
// 提交过来的密码,比如在注册时
$plainPassword = ...;
 
$encoder = $encoderFactory->getEncoder($user);
 
// will return $weakEncoder (see above)
// 返回$weakEncoder
$encodedPassword = $encoder->encodePassword($plainPassword, $user->getSalt());
 
$user->setPassword($encodedPassword);
 
// ... save the user 存储user对象

现在,当你要检查提交的密码(比如在登陆时)是否正确,你可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
// fetch the Acme\Entity\LegacyUser
// 取出LegacyUser
$user = ...;
 
// the submitted password, e.g. from the login form
// 从登录表单提交的密码
$plainPassword = ...;
 
$validPassword = $encoder->isPasswordValid(
    $user->getPassword(), // the encoded password
    $plainPassword,       // the submitted password
    $user->getSalt()
);

Authentication Events/认证事件 

安全组件提供了4种相关的authentication events:

事件名称 事件名称常量 传给监听的事件
security.authentication.success AuthenticationEvents::AUTHENTICATION_SUCCESS AuthenticationEvent
security.authentication.failure AuthenticationEvents::AUTHENTICATION_FAILURE AuthenticationFailureEvent
security.interactive_login SecurityEvents::INTERACTIVE_LOGIN InteractiveLoginEvent
security.switch_user SecurityEvents::SWITCH_USER SwitchUserEvent

Authentication Success and Failure Events/认证成功和认证失败事件 

当一个provider对用户进行认证时,一个 security.authentication.success 事件将被派遣。但是注意——这个事件是会被释放的,例如,在基于session进行认证的每一次 的请求时。参考下面的 security.interactive_login 事件,在用户切实地 登录之后,你可以进行一些操作。

当provider尝试认证却失败时(比如,抛出一个 AuthenticationException 异常),一个 security.authentication.failure 事件将被派遣。你可以监听 security.authentication.failure 事件,例如,将每一次登陆失败的尝试写入日志。

Security Events/安全事件 

security.interactive_login 事件将在用户成功地互动(actively)登陆到你的网站之后被触发。从登陆动作中,区分非互动认证方法(non-interactive authentication methods)是十分重要的:

  • 基于“remember-me” cookie的认证

  • 基于你的session的认证

  • 基于一个HTTP basic或HTTP digest header的认证

你可以监听 security.interactive_login 事件,例如,在你的用户登录之后,给他们一个欢迎条子(a welcome flash)。

security.switch_user 事件在每次你激活 switch_user (安全选项所对应的)防火墙监听(firewall listener)时被触发。

要了解多关于“切换用户”的信息,请参考 如何假扮一个用户

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

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