支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
当一个请求指向一个受保护区域时,防火墙映射(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是 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
接口即可。
每一个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
。
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。但如果你想创建自己的,只需遵循以下规则:
加密器类必须实现 PasswordEncoderInterface
接口;
实现 encoderPassword()
和 isPasswordValid()
方法时,必须首先保证密码不能超出长度限制,比如密码一般不能超过4096个字符。这是因为安全原因限制(CVE-2013-5750),你可以使用 isPasswordTooLong()
对此进行检查“:
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的 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()
); |
安全组件提供了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 |
当一个provider对用户进行认证时,一个 security.authentication.success
事件将被派遣。但是注意——这个事件是会被释放的,例如,在基于session进行认证的每一次 的请求时。参考下面的 security.interactive_login
事件,在用户切实地 登录之后,你可以进行一些操作。
当provider尝试认证却失败时(比如,抛出一个 AuthenticationException
异常),一个 security.authentication.failure
事件将被派遣。你可以监听 security.authentication.failure
事件,例如,将每一次登陆失败的尝试写入日志。
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 创作共用授权。