实际上,我在AuthenticationEvents::AUTHENTICATION_FAILURE
上有一个监听器,可以将failedLogin
存储在Redis缓存中,例如:
[
'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)
为此,我需要在登录时添加自定义验证规则。我在文档中找到了它:
但是,在这两个文档中,我需要重写很多东西,但我想保留所有实际行为:在上一页重定向用户(使用referer或在默认页面上),记住我(在gurad中,我被迫返回成功的响应,否则记住我不工作,但我真的不知道哪个响应返回....因为如果我返回null,重定向工作正常),消息等。
我已经搜索但没有找到Symfony默认使用的防护,可以复制/粘贴它,只需添加一条规则。
有人知道另一种方式,只需重写checkCredential
?
非常感谢
编辑(见最后的答案):
我找到了一个高级防护抽象类:Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator
。然后,身份验证工作就像在Symfony中一样,现在,我只需要在checkCredentials中添加我自己的测试(在我的情况下在getUser()中,我更喜欢返回检索用户之前的错误。
答案 0 :(得分:1)
您可以侦听事件是否失败登录尝试。创建服务:
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文档。
答案 1 :(得分:1)
最后,我通过扩展这个抽象类找到了一个简单的方法:Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator
。这个Authenticator取代了Symfony使用的默认 FormLoginAuthenticator ,但非常简单,我们只是重写了几个方法。
也许只是找到了获取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;
}
}