Spring(SpringBootも同じ)でログインユーザのアカウントをロック、有効期限切れ、認証資格有効期限切れなどを行う方法についてです。
結構簡単だったのですが、イマイチ基本を理解できていなかったので半日ほど手間取りました。
基本的にユーザ情報に持たせたbooleanのフィールドのture/falseを切り替えるだけで、ロックや期限切れの処理自体を記述する必要はありません。
このフィールドとは、org.springframework.security.core.userdetails.Userを継承したログインユーザのクラスのフィールドです。これに気付くのにしばらくかかりました。。。
元々、Userには以下のメソッドが用意されています。
・ユーザアカウントが認証期限切れしていないか
boolean isAccountNonExpired();
・ユーザアカウントの資格が認証期限切れしていないか
boolean isCredentialsNonExpired();
・ユーザアカウントがロックしていないか
boolean isAccountNonLocked();
・ユーザアカウントが無効
boolean isEnabled();
ですので、Userを継承したログインユーザのクラスで上記のメソッドを実装しておきます。
例)アカウントのロックを管理するUserクラス
public class LoginUserDetails extends User { private final ユーザクラス user; public LoginUserDetails(ユーザクラス userBean, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<GrantedAuthority> authorities) { super(userBean.getUsername(), userBean.getPassword(), true, true, true, userBean.isAccountNonLocked(), getAuthority(userBean)); this.user = userBean; } public boolean isAccountNonLocked() { return user.isAccountNonLocked(); } public boolean isEnabled() { return true; }
※無効、期限切れ、資格の期限切れには対応しないので、isEneabled()などは固定でtureを返し、コンストラクタのsuperに関する処理でも固定でtureを設定している
上記のフィールドのtrue/falseを切り替えることで、ログイン時にException(イベント)が発生することになります。
ログインに成功した場合、以下のイベントが発生します。
イベント | 内容 |
---|---|
AuthenticationSuccessEvent | 認証処理が成功(後続処理でエラー発生の場合あり) |
SessionFixationProtectionEvent | セッション固定攻撃対策の処理成功 |
InteractiveAuthenticationSuccessEvent | 認証処理がすべて成功 |
ログインに失敗した場合、以下のイベントが発生します。
イベント | イベントのトリガとなるException | 内容 |
---|---|---|
AuthenticationFailureBadCredentialsEvent | BadCredentialsException | 認証失敗 |
AuthenticationFailureDisabledEvent | DisabledException | アカウント無効 |
AuthenticationFailureLockedEvent | LockedException | アカウントロック |
AuthenticationFailureExpiredEvent | AccountExpiredException | アカウント有効期限切れ |
AuthenticationFailureCredentialsExpiredEvent | CredentialsExpiredException | 資格情報の有効期限切れ |
AuthenticationFailureServiceExceptionEvent | AuthenticationServiceException | 認証処理異常 |
※参考6. TERASOLUNA Server Framework for Java (5.x)によるセキュリティ対策
これらのイベントを制御するために、イベントのリスナクラスを作成します。
@Componentを付与したDI可能なクラスで@EventListenerを付与したメソッドを作成するだけです。
例)イベントのリスナクラス
@Component public class クラス名 { @EventListner public void メソッド名(イベント名 event) { } }
基本的にイベントごとに対応すれば上記で全て対応可能です。
存在するユーザでログインに失敗した場合、DBなどに記録したユーザのアカウント失敗回数をインクリメントします。一定回数に到達したら、ロックのフラグをtrueにします。
ただしログイン失敗時に存在するユーザと存在しないユーザのどちらでログインしようとしたかを判別するためにUsernameNotFoundExceptionの発生を識別する必要があります。
これはAuthenticationFailureBadCredentialsEventに分類されますが、認証プロバイダ(AbstractUserDetailsAuthenticationProvider)でhideUserNotFoundExceptionsをfalseに変更しないと
UsernameNotFoundExceptionはBadCredentialsExceptionに書き換えられるため、存在しないユーザでのログインを識別できません。
Configクラスに以下の記述をすることでUsernameNotFoundExceptionの発生を識別できるようになりましたが、AuthenticationProviderを正確に理解できていないので少し挙動の理解が怪しいです。
@Autowired UserDetailsService実装クラス service; @Bean public AuthenticationProvider daoAuhthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(service); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(new パスワードエンコーダクラス()); return daoAuthenticationProvider; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuhthenticationProvider()); }
※参考Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( 番外編 )( Spring Security でいろいろ試してみる )
上記の対応を実施した上でイベントのリスナクラスを以下のようにすると、存在するユーザと存在しないユーザの識別が可能になります。
@EventListener public void authFailureBadCredentialsEventHandler(AuthenticationFailureBadCredentialsEvent event) { if (event.getException().getClass().equals(UsernameNotFoundException.class)) { // 存在しないユーザ名でのログイン失敗 } else { // 存在するユーザ名でのログイン失敗 } }