今までSpring Security の認証処理を理解できていなかった。
単純なパスワード突合だけなら、UserDetailsService を実装してDB からselect するだけで良いので何とかそれで乗り切っていた。
しかしながら、現在のプロジェクトで複雑なログイン要件が追加されたため、それだけでは立ち行かなくなってしまった。
参考までに追加された要件
- パスワードを連続5回失敗失敗したらアカウントをロックする
- パスワード変更の有効期限を過ぎたらアカウントをロックする
いろいろ探しところ、DaoAuthenticationProvider のadditionalAuthenticationChecks() を継承すると非常に柔軟に実装できることが分かった。
コードはこちら。
@Component
@Slf4j
public class AppAuthenticationProvider extends DaoAuthenticationProvider {
public static final String DEFAULT_LOGIN_ERROR = "ユーザー名またはパスワードが違います";
public static final String ERROR_TOO_MANY_ATTEMPTS = "連続5回ログインに失敗しました";
public static final String ERROR_PASSWORD_EXPIRED = "パスワードの有効期限が過ぎています";
private final PlatformTransactionManager transactionManager;
private final AccountRepository accountRepository;
public AppAuthenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService, PlatformTransactionManager transactionManager, AccountRepository accountRepository) {
super(passwordEncoder);
// 親クラスで必要
setUserDetailsService(userDetailsService);
this.transactionManager = transactionManager;
this.accountRepository = accountRepository;
}
@Override
protected void additionalAuthenticationChecks(
UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 5回以上ログイン失敗している場合はログイン不可
if (LOGIN_FAIL_MAX_COUNT <= userDetails.getLoginFailCount()) {
throw new AppLoginException(ERROR_TOO_MANY_ATTEMPTS);
}
// パスワード変更期限を過ぎていたらログイン不可
if (userDetails.getPasswordExpiredDate() != null && now().isAfter(account.getPasswordExpiredDate())) {
throw new AppLoginException(ERROR_PASSWORD_EXPIRED);
}
// パスワードチェック
try {
// 親クラス(DaoAuthenticationProvider)のパスワードチェックを呼び出す
// パスワード相違時は例外がthrow される
super.additionalAuthenticationChecks(userDetails, authentication);
// パスワード一致時はカウンターをリセットする
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
// spring security から呼ばれるのでトランザクションを持っていない
accountRepository.resetFailCounter(userDetails.getAccountId());
});
} catch (AuthenticationException ae) {
// パスワード相違時は、カウンターをインクリメント
new TransactionTemplate(transactionManager).executeWithoutResult(status -> {
// spring security から呼ばれるのでトランザクションを持っていない
accountRepository.incrementFailCounter(userDetails.getAccountId());
});
// すでに4回失敗していたら、エラーメッセージを変更する
if ((LOGIN_FAIL_MAX_COUNT - 1) <= userDetails.getLoginFailCount()) {
throw new AppLoginException(ERROR_TOO_MANY_ATTEMPTS);
} else {
throw new AppLoginException(DEFAULT_LOGIN_ERROR);
}
}
}
}
こちらを@Bean 登録しすると有効になる。
@Bean
public AuthenticationManager authenticationManager(AppAuthenticationProvider provider) {
return new ProviderManager(provider);
}
ビルトインの機能を使って、UserDetails のisAccountNonExpired() で似たような実装を行うこともできる。その場合、すでに4回失敗した状態でパスワード相違した時にメッセージを変更することができないと思った。
throw された例外は、formLogin のfailureHandler に入ってくるので、それを一時的にセッションへ格納し、ログイン画面を表示するコントローラでセッションの値を取得して画面に表示すると良い。
セッションの値は一度限りで即時削除する。(redirectAttribute と同じだがここでは使えない)
また、failureHandler を使わず、additionalAuthenticationChecks() でセッションに例外を詰めることもできるが、ユーザ名不正の場合は、additionalAuthenticationChecks() が呼ばれないのでハンドリングできない。(1敗)



