身份验证添加constrainst
其实,我有一个在Redis的高速缓存存储failedLogin
,像AuthenticationEvents::AUTHENTICATION_FAILURE
监听器:身份验证添加constrainst
[
'ip' => [
'xxx.xxx.xxx.xxx' => [
'nbAttempts' => 5,
'lastAttempd' => \DateTime
],
],
'username' => [
'my_login' => [
'nbAttempts' => 3,
'lastAttempd' => \DateTime
],
'my_other_login' => [
'nbAttempts' => 2,
'lastAttempd' => \DateTime
],
]
]
但现在,我需要的不使用此列表,以防止登录当用户尝试与用户名连接在n分钟内尝试超过x次,对于IP(与另一个比率)相同。 (稍后,可能会在块之前添加ReCaptcha)
为此,我需要在登录中添加自定义验证规则。我发现它在文档中:
- http://symfony.com/doc/current/security/custom_password_authenticator.html
- https://symfony.com/doc/current/security/guard_authentication.html
但是,在这两个文件,我需要重写很多东西,但我希望将所有的实际行为:在前一页面(引用者或默认页面)上重定向用户,记住我(在gurad中,我不得不回复成功的回应,否则记得我不工作,但我不知道是哪个响应返回....因为如果我返回null,重定向工作正常),消息等...
我已经搜索过但没有找到Symfony默认使用的守卫来复制/粘贴它,只需添加一条规则即可。
有人知道另一种方式,那只是重写checkCredential
?
非常感谢
EDIT(请参照最后的答案): 我发现了一个先进的卫兵抽象类:Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator
。于是,像Symfony的认证工作,现在,我只需要添加我自己在checkCredentials(我在的getUser()情况下的测试,我宁愿返回错误之前检索用户
最后,我发现了一个简单的方法来扩展这个抽象类:Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator
。该验证替换默认FormLoginAuthenticator由Symfony的使用,但很简单,我们只是重写一些方法。
也许只是找到一种方式来获得config.yml值,确定的路线(避免把它写在这个文件中,因为我们在配置声明它)。
我的服务宣言:
app.security.form_login_authenticator:
class: AppBundle\Security\FormLoginAuthenticator
arguments: ["@router", "@security.password_encoder", "@app.login_brute_force"]
我FormLoginAuthenticator:
<?php
namespace AppBundle\Security;
use AppBundle\Utils\LoginBruteForce;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
private $router;
private $encoder;
private $loginBruteForce;
public function __construct(Router $router, UserPasswordEncoderInterface $encoder, LoginBruteForce $loginBruteForce)
{
$this->router = $router;
$this->encoder = $encoder;
$this->loginBruteForce = $loginBruteForce;
}
protected function getLoginUrl()
{
return $this->router->generate('login');
}
protected function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('homepage');
}
public function getCredentials(Request $request)
{
if ($request->request->has('_username')) {
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
return;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
// Check if the asked username is under bruteforce attack, or if client process to a bruteforce attack
$this->loginBruteForce->isBruteForce($username);
// Catch the UserNotFound execption, to avoid gie informations about users in database
try {
$user = $userProvider->loadUserByUsername($username);
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Bad credentials.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
$passwordValid = $this->encoder->isPasswordValid($user, $credentials['password']);
if (!$passwordValid) {
throw new AuthenticationException('Bad credentials.');
}
return true;
}
}
而且,如果它是有趣的人,我LoginBruteForce:
<?php
namespace AppBundle\Utils;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class LoginBruteForce
{
// Define constants used to define how many tries we allow per IP and login
// Here: 20/10 mins (IP); 5/10 mins (username)
const MAX_IP_ATTEMPTS = 20;
const MAX_USERNAME_ATTEMPTS = 5;
const TIME_RANGE = 10; // In minutes
private $cacheAdapter;
private $requestStack;
public function __construct(AdapterInterface $cacheAdapter, RequestStack $requestStack)
{
$this->cacheAdapter = $cacheAdapter;
$this->requestStack = $requestStack;
}
private function getFailedLogins()
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLogins = $failedLoginsItem->get();
// If the failedLogins is not an array, contruct it
if (!is_array($failedLogins)) {
$failedLogins = [
'ip' => [],
'username' => [],
];
}
return $failedLogins;
}
private function saveFailedLogins($failedLogins)
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLoginsItem->set($failedLogins);
$this->cacheAdapter->save($failedLoginsItem);
}
private function cleanFailedLogins($failedLogins, $save = true)
{
$actualTime = new \DateTime('now');
foreach ($failedLogins as &$failedLoginsCategory) {
foreach ($failedLoginsCategory as $key => $failedLogin) {
$lastAttempt = clone $failedLogin['lastAttempt'];
$lastAttempt = $lastAttempt->modify('+'.self::TIME_RANGE.' minute');
// If the datetime difference is greatest than 15 mins, delete entry
if ($lastAttempt <= $actualTime) {
unset($failedLoginsCategory[$key]);
}
}
}
if ($save) {
$this->saveFailedLogins($failedLogins);
}
return $failedLogins;
}
public function addFailedLogin(AuthenticationFailureEvent $event)
{
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
$username = $event->getAuthenticationToken()->getCredentials()['username'];
$failedLogins = $this->getFailedLogins();
// Add clientIP
if (array_key_exists($clientIp, $failedLogins['ip'])) {
$failedLogins['ip'][$clientIp]['nbAttempts'] += 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['ip'][$clientIp]['nbAttempts'] = 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
}
// Add username
if (array_key_exists($username, $failedLogins['username'])) {
$failedLogins['username'][$username]['nbAttempts'] += 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['username'][$username]['nbAttempts'] = 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
}
$this->saveFailedLogins($failedLogins);
}
// This function can be use, when the user reset his password, or when he is successfully logged
public function resetUsername($username)
{
$failedLogins = $this->getFailedLogins();
if (array_key_exists($username, $failedLogins['username'])) {
unset($failedLogins['username'][$username]);
$this->saveFailedLogins($failedLogins);
}
}
public function isBruteForce($username)
{
$failedLogins = $this->getFailedLogins();
$failedLogins = $this->cleanFailedLogins($failedLogins, true);
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
// If the IP is in the list
if (array_key_exists($clientIp, $failedLogins['ip'])) {
if ($failedLogins['ip'][$clientIp]['nbAttempts'] >= self::MAX_IP_ATTEMPTS) {
throw new AuthenticationException('Too many login attempts. Please try again in '.self::TIME_RANGE.' minutes.');
}
}
// If the username is in the list
if (array_key_exists($username, $failedLogins['username'])) {
if ($failedLogins['username'][$username]['nbAttempts'] >= self::MAX_USERNAME_ATTEMPTS) {
throw new AuthenticationException('Maximum number of login attempts exceeded for user: "'.$username.'". Please try again in '.self::TIME_RANGE.' minutes.');
}
}
return;
}
}
你可以听上失败的登录尝试的情况下创建一个服务:
services:
app.failed_login_listener:
class: AppBundle\EventListener\AuthenticationFailureListener
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
然后创建收听者:
<?php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
class AuthenticationFailureListener implements AuthenticationFailureHandlerInterface
{
public function onAuthenticationFailure(
Request $request,
AuthenticationException $exception
) {
// do whatever
}
}
修改您的服务定义以注入您可能需要的任何其他服务。
如果要在用户登录后执行操作,可以使用security.interactive_login
事件来完成此操作。只要抛出异常,如果你遇到了你想让用户的登录无效的情况,并且可能删除他们的安全令牌或你需要的任何东西。您甚至可以在Controller的登录操作中执行此操作。
例如:
services:
app.security_listener:
class: AppBundle\EventListener\InteractiveLoginListener
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
然后让你的听众:
<?php
namespace App\EventListener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class InteractiveLoginListener
{
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
// do whatever
}
}
根据需要再注入依赖。另请看Symfony的文档creating a custom authentication provider。
您可以编写针对该事件的监听器。请参阅https://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events。写你的监听器为'security.authentication.failure'事件 –
谢谢,但我已经有一个存储失败的侦听器,现在我想添加一个验证检查登录,与存储的失败。 – mpiot