2017-07-17 246 views
0

其实,我有一个在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)

为此,我需要在登录中添加自定义验证规则。我发现它在文档中:

但是,在这两个文件,我需要重写很多东西,但我希望将所有的实际行为:在前一页面(引用者或默认页面)上重定向用户,记住我(在gurad中,我不得不回复成功的回应,否则记得我不工作,但我不知道是哪个响应返回....因为如果我返回null,重定向工作正常),消息等...

我已经搜索过但没有找到Symfony默认使用的守卫来复制/粘贴它,只需添加一条规则即可。

有人知道另一种方式,那只是重写checkCredential

非常感谢

EDIT(请参照最后的答案): 我发现了一个先进的卫兵抽象类:Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator。于是,像Symfony的认证工作,现在,我只需要添加我自己在checkCredentials(我在的getUser()情况下的测试,我宁愿返回错误之前检索用户

+0

您可以编写针对该事件的监听器。请参阅https://symfony.com/doc/current/components/security/authentication.html#authentication-success-and-failure-events。写你的监听器为'security.authentication.failure'事件 –

+0

谢谢,但我已经有一个存储失败的侦听器,现在我想添加一个验证检查登录,与存储的失败。 – mpiot

回答

1

最后,我发现了一个简单的方法来扩展这个抽象类: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; 
    } 
} 
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

+0

谢谢,但我已经有一个存储失败的侦听器,现在我想在登录时添加一个验证检查,并存储失败。 – mpiot

+0

更新了我的答案。 –

+0

谢谢,我也编辑了我的帖子,最后我找到了一种通过实现'AbstractFormLoginAuthenticator'轻松完成CustomAuthenticator的方法。 – mpiot