1. 概要

このクイックチュートリアルでは、Springセキュリティを使用してブルートフォース認証の試行を防止するための基本的なソリューションを実装します。

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

2. AuthenticationFailureListener

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

@Component
public class AuthenticationFailureListener implements 
  ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginFailed(request.getRemoteAddr());
        } else {
            loginAttemptService.loginFailed(xfHeader.split(",")[0]);
        }
    }
}

認証が失敗したときに、失敗した試行が発生したIPアドレスをLoginAttemptServiceに通知する方法に注意してください。 ここでは、 HttpServletRequest BeanからIPアドレスを取得します。これにより、たとえばによって転送されるリクエストのX-Forwarded-Forヘッダーの発信元アドレスも取得されます。 プロキシサーバー。

3. AuthenticationSuccessEventListener

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

@Component
public class AuthenticationSuccessEventListener implements 
  ApplicationListener<AuthenticationSuccessEvent> {
    
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(final AuthenticationSuccessEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginSucceeded(request.getRemoteAddr());
        } else {
            loginAttemptService.loginSucceeded(xfHeader.split(",")[0]);
        }
    }
}

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

4. LoginAttemptService

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

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Integer>() {
            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;
        }
    }
}

認証の試行が失敗すると、そのIP の試行回数が増加し、認証が成功すると、そのカウンターがリセットされることに注意してください。

この時点から、認証時にカウンターをチェックするだけです

5. UserDetailsService

次に、カスタムUserDetailsService実装に追加のチェックを追加しましょう。 UserDetails をロードするとき、最初にこのIPアドレスがブロックされているかどうかを確認する必要があります

@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];
}

クライアントの元のIPアドレスを識別するための追加のロジックがあることに注意してください。 ほとんどの場合、これは必要ありませんが、一部のネットワークシナリオでは必要です。

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

X-Forwarded-For: clientIpAddress, proxy1, proxy2

また、Springが持つもう1つの非常に興味深い機能に注目してください。 HTTPリクエストが必要なので、単に接続するだけです。

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

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

これで終わりです。この新しい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. 結論

これは、ブルートフォースパスワードの試行に対処するための優れた最初のステップであるだけでなく、改善の余地があることを理解することが重要です。 プロダクショングレードのブルートフォース防止戦略には、IPブロックよりも多くの要素が含まれる場合があります。

このチュートリアルの完全な実装は、githubプロジェクトにあります。