SpringSecurityでブルートフォース認証の試みを防ぐ
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>
これで終わりです。この新しいRequestContextListenerをweb.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プロジェクトにあります。