Spring Securityによるブルートフォース認証の試行を防止する
1. 概要
このクイックチュートリアルでは、Spring Securityを使用してpreventing brute force authentication attemptsの基本的なソリューションを実装します。
簡単に言えば、単一のIPアドレスから発生した失敗した試行の数を記録します。 その特定のIPが設定された要求数を超えると、24時間ブロックされます。
参考文献:
リアクティブアプリケーション用の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実装にチェックを追加しましょう。 UserDetails、we 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
以上です。この新しい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. 結論
これはdealing with brute-force password attemptsの良い最初のステップであるだけでなく、改善の余地があることを理解することが重要です。 生産グレードのブルートフォース防止戦略には、IPがブロックする要素以上のものが含まれる場合があります。
このチュートリアルのfull implementationは、the github projectにあります。これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。