2017-07-17 225 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