Spring Securityによるブルートフォース認証の試みの防止

Spring Securityによるブルートフォース認証の試行を防止する

1. 概要

このクイックチュートリアルでは、Spring Securityを使用してpreventing brute force authentication attemptsの基本的なソリューションを実装します。

簡単に言えば、単一のIPアドレスから発生した失敗した試行の数を記録します。 その特定のIPが設定された要求数を超えると、24時間ブロックされます。

参考文献:

Spring Method Securityの概要

Spring Securityフレームワークを使用したメソッドレベルセキュリティのガイド。

Spring Securityフィルターチェーンのカスタムフィルター

Spring Securityコンテキストでカスタムフィルターを追加する手順を示すクイックガイド。

リアクティブアプリケーション用のSpring Security 5

リアクティブアプリケーションを保護するためのSpringSecurity5フレームワークの機能の簡単で実用的な例。

2. AuthenticationFailureEventListener

AuthenticationFailureEventListenerを定義することから始めましょう–AuthenticationFailureBadCredentialsEventイベントをリッスンし、認証の失敗を通知します。

@Component
public class AuthenticationFailureListener
  implements ApplicationListener {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails)
          e.getAuthentication().getDetails();

        loginAttemptService.loginFailed(auth.getRemoteAddress());
    }
}

認証が失敗したときに、失敗した試行が発生したIPアドレスのLoginAttemptServiceに通知する方法に注意してください。

3. AuthenticationSuccessEventListener

また、AuthenticationSuccessEventListenerを定義しましょう。これはAuthenticationSuccessEventイベントをリッスンし、認証が成功したことを通知します。

@Component
public class AuthenticationSuccessEventListener
  implements ApplicationListener {

    @Autowired
    private LoginAttemptService loginAttemptService;

    public void onApplicationEvent(AuthenticationSuccessEvent e) {
        WebAuthenticationDetails auth = (WebAuthenticationDetails)
          e.getAuthentication().getDetails();

        loginAttemptService.loginSucceeded(auth.getRemoteAddress());
    }
}

失敗リスナーと同様に、認証要求の発信元のIPアドレスのLoginAttemptServiceに通知していることに注意してください。

4. LoginAttemptService

それでは、LoginAttemptServiceの実装について説明しましょう。簡単に言えば、IPアドレスごとの間違った試行の数を24時間保持します。

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader() {
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void loginFailed(String key) {
        int attempts = 0;
        try {
            attempts = attemptsCache.get(key);
        } catch (ExecutionException e) {
            attempts = 0;
        }
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_ATTEMPT;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

an unsuccessful authentication attempt increases the number of attempts for that IPと、認証が成功すると、そのカウンターがリセットされることに注意してください。

この時点から、それは単にchecking the counter when we authenticateの問題です。

5. UserDetailsService

それでは、カスタムUserDetailsService実装にチェックを追加しましょう。 UserDetailswe first need to check if this IP address is blockedをロードすると:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Autowired
    private HttpServletRequest request;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        String ip = getClientIP();
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("blocked");
        }

        try {
            User user = userRepository.findByEmail(email);
            if (user == null) {
                return new org.springframework.security.core.userdetails.User(
                  " ", " ", true, true, true, true,
                  getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
            }

            return new org.springframework.security.core.userdetails.User(
              user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true,
              getAuthorities(user.getRoles()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

そしてここにgetClientIP()メソッドがあります:

private String getClientIP() {
    String xfHeader = request.getHeader("X-Forwarded-For");
    if (xfHeader == null){
        return request.getRemoteAddr();
    }
    return xfHeader.split(",")[0];
}

identify the original IP address of the Clientにいくつかの追加ロジックがあることに注意してください。 ほとんどの場合、これは必要ありませんが、一部のネットワークシナリオでは必要です。

これらのまれなシナリオでは、X-Forwarded-Forヘッダーを使用して元のIPにアクセスしています。このヘッダーの構文は次のとおりです。

X-Forwarded-For: clientIpAddress, proxy1, proxy2

また、Springが持つもう1つの非常に興味深い機能–we need the HTTP request, so we’re simply wiring it in.にも注目してください。

さて、それはクールです。 これを機能させるには、web.xmlにクイックリスナーを追加する必要があります。これにより、作業が非常に簡単になります。


    
        org.springframework.web.context.request.RequestContextListener
    

以上です。この新しいRequestContextListenerweb.xmlに定義して、UserDetailsServiceからのリクエストにアクセスできるようにしました。

6. AuthenticationFailureHandlerを変更する

最後に、CustomAuthenticationFailureHandlerを変更して、新しいエラーメッセージをカスタマイズしましょう。

ユーザーが実際に24時間ブロックされる状況に対処しています。また、ユーザーが許可されている不正な認証の最大試行回数を超えたためにIPがブロックされていることをユーザーに通知しています。

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Override
    public void onAuthenticationFailure(...) {
        ...

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);
        if (exception.getMessage().equalsIgnoreCase("blocked")) {
            errorMessage = messages.getMessage("auth.message.blocked", null, locale);
        }

        ...
    }
}

7. 結論

これはdealing with brute-force password attemptsの良い最初のステップであるだけでなく、改善の余地があることを理解することが重要です。 生産グレードのブルートフォース防止戦略には、IPがブロックする要素以上のものが含まれる場合があります。

このチュートリアルのfull implementationは、the github projectにあります。これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。