Spring Security でアカウントロック機構を実現する

アカウントロック機構は、パスワード認証をともなうWebアプリケーションの開発ではしばしばセキュリティ要件として盛り込まれることがあります。また、Webサービスのユーザーとしても馴染みのある機能のひとつでしょう。今日は、この機能を Spring Security の認証の枠組みの中で実現する方法について紹介します。なお、本記事は Spring Security 3.0.4 をベースに書かれています。
いきなりですが、 Spring Security では、アカウントロック機構は提供されていません。アカウントをロックするのは認証サービスが行うべきことであり、認証サービスとアプリケーションをつなぐためのフレームワークである Spring Security には、そのような義務はありません。また、アカウントロックの実現方式についても、認証サービスに強く依存します。ですので、アプリケーションへの要求としてアカウントロック機構が求められており、認証サービスがアカウントロック機構を提供していない場合は、自作する必要が出てきます*1
といっても、実装のための手がかりがまったくないというわけではありません。Spring Security では、認証に失敗した場合に AuthenticationFailureBadCredentialsEvent というイベントが発生します。このイベントを受け取る Listener を実装し Spring Bean として登録しておけば、認証失敗時に対応する Listener が呼び出されます。したがって、この Listener にアカウントロックの機能を実装すれば事足りる、というわけです。一般的には、認証失敗の情報をデータベース等に記録し、規定回数を超えた場合はユーザーアカウントをロックする、といった内容になるでしょう。
以下は、Spring Security が標準で送出するイベントの一覧になります。表の右列にある「対応する例外」が発生したときに、イベントがトリガーされます。認証に成功した場合は、AuthenticationSuccessEvent が送出されます。つまり、認証成功時に何か特別な処理をしたい場合*2にも、今回紹介する手法と同様のやり方で実現できます。

発生するイベント 意味 対応する例外
AuthenticationFailureBadCredentialsEvent 認証情報が正しくない BadCredentialsException, UsernameNotFoundException
AuthenticationFailureCredentialsExpiredEvent 認証情報が失効している CredentialsExpiredException
AuthenticationFailureDisabledEvent アカウントが無効になっている DisabledException
AuthenticationFailureExpiredEvent アカウントが失効している AccountExpiredException
AuthenticationFailureLockedEvent アカウントがロックされている LockedException
AuthenticationFailureProviderNotFoundEvent 認証プロバイダーが見つからない ProviderNotFoundException
AuthenticationFailureProxyUntrustedEvent (CAS認証において)プロキシーが信頼できない ProxyUntrustedException
AuthenticationFailureServiceExceptionEvent AuthenticationManager 内で問題が発生した AuthenticationServiceException
AuthenticationSuccessEvent 認証に成功した なし


イベントの伝播は Spring Framework の標準的な機構を用いて行われます。すなわち、イベントオブジェクトは ApplicationEvent を基底としたクラスとなり、通知は ApplicationListener で受け取ることができます。Spring Framework のイベント伝播の仕組みについては、すでに blog や書籍等で数多くの解説記事がありますので、ここでは触れません。不明な点があれば、それらの解説記事も参照してください。

最後に、実装サンプルを載せておきます。
注意点としては、AuthenticationFailureBadCredentialsEvent に対応する例外は BadCredentialsException と UsernameNotFoundException のふたつがあります。そのため、単純に AuthenticationFailureBadCredentialsEvent を捕捉するだけでは、上記の2つの Exception を区別できないため、存在しないユーザーもDBに記録してしまう可能性があります。これを嫌うのであれば、捕捉した Event から例外情報を取り出して、その型をチェックすることが必要になります。


なお、Spring 3.0 以降では ApplicationListener が Generics 対応しており、サンプルコードのように受け取るイベントの型を明示できるようになっています。キャストの手間がひとつ減りますので、利用側としてはうれしいですね。

*1:LDAPのように、認証サービス側がアカウントロック機構をすでに持っている場合は、その枠組みを利用すればよく、その場合は自作する必要はありません。

*2:たとえば、ログインの証跡を取りたいなど。