Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その9 )( ログイン画面の作成3 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その8 )( ログイン画面の作成2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- パスワードを5回間違えた場合には ID をロックする機能の作成
参照したサイト・書籍
Spring Security でアカウントロック機構を実現する
http://d.hatena.ne.jp/ocs/20111101/1320115388http://gotoanswer.com/?q=Use+custom+exceptions+in+Spring+Security
- UsernameNotFoundException が BadCredentialsException に置き換わらないようにするために、AbstractUserDetailsAuthenticationProvider 抽象クラスの setHideUserNotFoundExceptions メソッドを呼び出す方法を調査していた時に参照しました。
目次
- はじめに
- 1.0.x-make-accountlock ブランチの作成
- ログインエラーのイベントを ApplicationListener クラスで受け取ってエラーの原因を判別できるのか?
- 実装内容を決める
- user_info テーブルの変更
- パスワードを5回間違えた場合には ID をロックする機能の作成
- 動作確認
- commit、Push、Pull Request、マージ
- メモ書き
手順
はじめに
パスワードを5回間違えた場合には ID をロックする機能を作成します。
UserInfoUserDetails クラスに accountNonLocked というフィールドが用意されているので、アカウントロック時はこれが false になるようにすればいいはずです。
今回は Spring Security でアカウントロック機構を実現する というやりたいことがそのままタイトルになっている記事も以前見つけていますので、Spring Boot でもそのまま実装できるのか動作確認してから進めます。
1.0.x-make-accountlock ブランチの作成
- IntelliJ IDEA で 1.0.x-make-accountlock ブランチを作成します。
ログインエラーのイベントを ApplicationListener クラスで受け取ってエラーの原因を判別できるのか?
以前 Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その9 )( ログイン画面作成2 ) を書いていた時に見つけた Spring Security でアカウントロック機構を実現する の記事によると、認証情報が正しくない時に発生する AuthenticationFailureBadCredentialsEvent の ApplicationListener クラスを登録しておけばログインエラー時の処理を追加できるとのことでした。確認してみます。
src/main/java/ksbysample/webapp/lending/security の下に AuthenticationFailureBadCredentialsEventListener.java を作成します。作成後、リンク先のその1の内容 に変更します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動して http://localhost:8080/ にアクセスし、ログイン画面を表示します。
最初はログイン成功の場合にイベントが発生しないことを確認します。ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。「ログイン成功!」の画面が表示され、AuthenticationFailureBadCredentialsEventListener::onApplicationEvent に入れたログも出力されませんでした。
次に存在しない ID でログインしてみます。ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、ID に "xxxxxxxx"、Password に "taro" を入力して「ログイン」ボタンをクリックします。
画面に「入力された ID あるいはパスワードが正しくありません」のエラーが表示されました。
ログを見ると BadCredentialsException のログが出力されていました。ユーザが存在しないので UsernameNotFoundException が発生すると思っていたのですが違うようです。
次に ID は存在するがパスワードが間違っている場合を試してみます。再度ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、ID に "tanaka.taro@sample.com"、Password に "xxxxxxxx" を入力して「ログイン」ボタンをクリックします。
画面に「入力された ID あるいはパスワードが正しくありません」のエラーが表示されました。
ログを見ると先ほどと同じく BadCredentialsException のログが出力されていました。
作成した UserDetailsService インターフェースの実装クラス UserInfoUserDetailsService では入力された ID のユーザが user_info テーブルに存在しない場合には UsernameNotFoundException を throw するように実装したのですが、もしかして UsernameNotFoundException が throw されていないのでしょうか? 確認してみます。
UserInfoUserDetailsService クラスの loadUserByUsername メソッドの以下の画像の箇所にブレークポイントを設定して、存在しない ID でログインしてみます。
Ctrl+F2 を押して Tomcat を停止します。
メイン画面右上で Debug ボタンを押して Tomcat を起動します。
ブラウザで http://localhost:8080/ にアクセスしてログイン画面を表示した後、ID に "xxxxxxxx"、Password に "taro" を入力して「ログイン」ボタンをクリックします。
想定通り
throw new UsernameNotFoundException(...);
の処理が実行されていました。UsernameNotFoundException を throw しているのになぜログに BadCredentialsException が出力されるのか調べるために、このまま Debugger の Step Over ボタンを押して処理を実行し続けてみました。以下のことが分かりました。
- org.springframework.security.authentication.dao の AbstractUserDetailsAuthenticationProvider 抽象クラスの 136行目に
if (hideUserNotFoundExceptions) {
という記述があり、hideUserNotFoundExceptions が true の場合には UsernameNotFoundException ではなく BadCredentialsException に置き換わるようになっていました。 - hideUserNotFoundExceptions はクラスの上で
protected boolean hideUserNotFoundExceptions = true;
と定義されていました。 public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions)
というメソッドも用意されていました。setHideUserNotFoundExceptions を呼び出して true にすれば UsernameNotFoundException のまま throw されるようです。
- org.springframework.security.authentication.dao の AbstractUserDetailsAuthenticationProvider 抽象クラスの 136行目に
src/main/java/ksbysample/webapp/lending/config の下の WebSecurityConfig.java を リンク先の内容 に変更します。
Ctrl+F2 を押して Tomcat を停止します。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
最初にログイン可能な ID, Password でログインできるか確認します。ログイン画面で ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。「ログイン成功!」の画面が表示されました。
次に存在しない ID でログインしてみます。ログイン画面で ID に "xxxxxxxx"、Password に "taro" を入力して「ログイン」ボタンをクリックします。
画面に「username(xxxxxxxx) not found.」のエラーが表示されました。
ログを見ると今度は UsernameNotFoundException のログが出力されていました。
次に ID は存在するがパスワードが間違っている場合を試してみます。ログイン画面で ID に "tanaka.taro@sample.com"、Password に "xxxxxxxx" を入力して「ログイン」ボタンをクリックします。こちらはログに BadCredentialsException が出力されていました。
UsernameNotFoundException 発生時のエラーメッセージが BadCredentialsException 発生時と同じになるようにします。
src/main/resources/messages_ja_JP.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetailsService.java を リンク先のその1の内容 に変更します。
Ctrl+F5 を押して Tomcat を再起動します。
ログイン画面で ID に "xxxxxxxx"、Password に "taro" を入力して「ログイン」ボタンをクリックします。画面に「入力された ID あるいはパスワードが正しくありません」のエラーが表示されました。
Ctrl+F2 を押して Tomcat を停止します。
実装内容を決める
ApplicationListener クラスでログインエラー時のイベントを受け取って処理できることが確認できましたので、以下の内容で実装を進めます。
- user_info テーブルにログインエラー回数を保存するカラムを追加します。
- ログインエラー回数は初期値を 0 にします。BadCredentialsException が発生した場合のみログインエラー回数をインクリメントします。
- ログインエラー回数が 5 以上になったらアカウントをロックします。テーブルを直接操作してログインエラー回数のカラムの値を 4以下に変更しないとログインできないようにします。正しい ID、Password を入力してもログインできません。
- ログインできたらログインエラー回数を 0 に戻します。
user_info テーブルの変更
/sql の下の create_table.sql を リンク先の内容 に変更します。
コマンドプロンプトから以下のコマンドを実行して user_info テーブルに cnt_badcredentials カラムを追加します。
C:\project-springboot\ksbysample-webapp-lending>psql -U ksbylending_user ksbylending ユーザ ksbylending_user のパスワード: psql (9.4.1) "help" でヘルプを表示します. ksbylending=> alter table user_info add column cnt_badcredentials smallint not null default 0; ALTER TABLE ksbylending=> \d user_info user_id | bigint | not null default nextval('user_info_user_id_seq'::regclass) username | character varying(32) | not null password | character varying(256) | not null mail_address | character varying(256) | not null enabled | smallint | not null default 1 cnt_badcredentials | smallint | not null default 0 ksbylending=> \q C:\project-springboot\ksbysample-webapp-lending>
IntelliJ IDEA の Database Tools の「Shynchronize」ボタンをクリックして変更内容を反映します。
登録済の username = 'tanaka taro' のデータを見ると cnt_badcredentials のカラムの値は 0 になっています。
Gradle projects View から gen タスクを実行して user_info テーブルの Entity クラスを作成し直します。
パスワードを5回間違えた場合には ID をロックする機能の作成
UserInfoDao インターフェースに cnt_badcredentials 更新用のメソッドを追加します。src/main/java/ksbysample/webapp/dao の下の UserInfoDao.java を リンク先の内容 に変更します。
src/main/resources/META-INF/ksbysample/webapp/lending/dao/UserInfoDao の下に incCntBadcredentialsByMailAddress.sql を作成します。作成後、リンク先の内容 に変更します。
src/main/resources/META-INF/ksbysample/webapp/lending/dao/UserInfoDao の下に initCntBadcredentialsByMailAddress.sql を作成します。作成後、リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending の下に service パッケージを作成します。
src/main/java/ksbysample/webapp/lending/service の下に UserInfoService.java を作成します。作成後、リンク先の内容 に変更します。
ログインエラー時の処理を実装します。src/main/java/ksbysample/webapp/lending/security の下の AuthenticationFailureBadCredentialsEventListener.java を リンク先のその2の内容 に変更します。
ログイン成功時の処理を実装します。src/main/java/ksbysample/webapp/lending/security の下に AuthenticationSuccessEventListener.java を作成します。作成後、リンク先の内容 に変更します。
ログインエラーが5回以上になったらアカウントロックする処理を実装します。src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetails.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/security の下の UserInfoUserDetailsService.java を リンク先のその2の内容 に変更します。
動作確認
user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値を 0 に変更しておきます。
Gradle projects View から bootRun タスクを実行して Tomcat を起動します。
ブラウザを起動して http://localhost:8080/ にアクセスし、ログイン画面を表示します。
ID に "tanaka.taro@sample.com"、Password に "x" を入力して「ログイン」ボタンをクリックします。
「入力された ID あるいはパスワードが正しくありません」のメッセージが表示されます。
user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値が 1 に増えています。
あと4回 ID に "tanaka.taro@sample.com"、Password に "x" を入力して「ログイン」ボタンをクリックする操作を繰り返します。user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値が 5 になります。
ID に "tanaka.taro@sample.com"、Password に "x" を入力して「ログイン」ボタンをクリックします。
「入力された ID はロックされています」のメッセージが表示されます。
user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値は 5 のまま変わりません。
この後何回か ID に "tanaka.taro@sample.com"、Password に "x" を入力して「ログイン」ボタンをクリックしても「入力された ID はロックされています」のメッセージが表示され続けました。
今度はアカウントロックされている時に正しい ID、Password を入力してもログインできない ( アカウントロックが解除されない ) ことを確認します。ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。
ログインはできず「入力された ID はロックされています」のメッセージが表示されました。
user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値は 5 のままでした。
今度は cnt_badcredentials の値を 4 に変更してアカウントロックの状態を解除した後に、ログインできることとログインした後 cnt_badcredentials の値が 0 に初期化されることを確認します。まず user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値を 4 に変更します。
ID に "tanaka.taro@sample.com"、Password に "taro" を入力して「ログイン」ボタンをクリックします。
ログインして「ログイン成功!」の画面が表示されました。
user_info テーブルの username = 'tanaka taro' のデータの cnt_badcredentials の値は 0 になりました。
想定通り動作することが確認できました。Ctrl+F2 を押して Tomcat を停止します。
commit、Push、Pull Request、マージ
commit します。「Code Analysis」ダイアログが出ますが、無視して「Commit」ボタンをクリックします。
GitHub へ Push、1.0.x-make-accountlock -> 1.0.x へ Pull Request、1.0.x でマージ、1.0.x-make-accountlock ブランチを削除、をします。
メモ書き
パッケージの中のクラス一覧を確認する方法
今回の作業中に、package org.springframework.security.authentication.event;
の中に AuthenticationFailureBadCredentialsEvent 以外のどんなイベントクラスが用意されているか確認したいと思ったので、その方法を調べてみました。
一番シンプルで速い方法は、IntelliJ IDEA のメイン画面の上部に表示されているパッケージ名をクリックすることです。そのパッケージ内のクラス一覧が表示されます。
Ultimate Edition であれば UML のクラス図で表示させることができます。まず
package org.springframework.security.authentication.event;
のevent
の部分にカーソルを移動した後、コンテキストメニューを表示して「Diagrams」->「Show Diagram...」を選択します。UML のクラス図が表示されます。下の画像ではクラス名しか表示されていませんが、左上の f や m のボタンを押して選択状態にすればフィールドやメソッドも表示されます。
Spring Security のログイン処理時のイベント一覧は Spring Security のマニュアルに掲載されているのか?
Spring 関連のライブラリが用意しているイベント一覧を知るにはにはどうすればよいのか興味が湧いたので、調べてみました。
Spring Security Reference には記載がありませんでした。
パッケージの中のクラス一覧を確認する方法 に書いた通り、org.springframework.security.authentication.event にあるイベント一覧を表示させればネーミングも分かりやすいものばかりなので、何があるかは分かりそうではあります。
docs.spring.io の Spring Security 3.2.7.RELEASE API を表示して、"event" で検索するのが一番分かりやすそうです。
- Spring Security 3.2.7.RELEASE API で "event" で検索する。
- org.springframework.security.authentication.event のリンクをクリックして、Package org.springframework.security.authentication.event を表示する。
- 画面上部の Tree リンクをクリックすると Hierarchy For Package org.springframework.security.authentication.event が表示され、org.springframework.context.ApplicationEvent を継承したクラスであることが分かる。
おそらく Spring 関連のライブラリは docs.spring.io に同様にドキュメントが用意されていると思われるので、ここを見るのが一番調べやすそうな気がします。
IntelliJ IDEA に指定したパッケージ内の ApplicationEvent を継承したクラスだけをダイアグラムで表示してくれる機能があると便利なのですが。。。 と思って調べていたらそれらしいことができる機能を見つけたので番外編で書きます。
ソースコード
AuthenticationFailureBadCredentialsEventListener.java
■その1
package ksbysample.webapp.lending.security; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.stereotype.Component; @Component public class AuthenticationFailureBadCredentialsEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { @Override public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { System.out.println("★★★" + event.getException().getClass().getName()); } }
■その2
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class AuthenticationFailureBadCredentialsEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent> { @Autowired private UserInfoService userInfoService; @Override public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) { // UsernameNotFoundException は何もしない if (event.getException().getClass().equals(UsernameNotFoundException.class)) { return; } // 入力されたID(メールアドレス)の user_info.cnt_badcredentials を +1 する userInfoService.incCntBadcredentials(event.getAuthentication().getName()); } }
WebSecurityConfig.java
package ksbysample.webapp.lending.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します // .antMatchers("/country/**").permitAll() .antMatchers("/fonts/**").permitAll() .antMatchers("/html/**").permitAll() .antMatchers("/encode").permitAll() .anyRequest().authenticated(); http.formLogin() .loginPage("/") .loginProcessingUrl("/login") .defaultSuccessUrl("/loginsuccess") .failureUrl("/") .usernameParameter("id") .passwordParameter("password") .permitAll() .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/") .deleteCookies("JSESSIONID") .invalidateHttpSession(true) .permitAll(); } @Bean public AuthenticationProvider daoAuhthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); return daoAuthenticationProvider; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuhthenticationProvider()); } }
- daoAuhthenticationProvider Bean を追加します。このメソッド内で UserDetailService, PasswordEncoder を設定するようにします。
- configAuthentication メソッド内では auth.authenticationProvider を呼び出して、追加した daoAuhthenticationProvider Bean を設定するようにします。
messages_ja_JP.properties
AbstractUserDetailsAuthenticationProvider.locked=入力された ID はロックされています AbstractUserDetailsAuthenticationProvider.disabled=入力された ID は使用できません AbstractUserDetailsAuthenticationProvider.expired=入力された ID の有効期限が切れています AbstractUserDetailsAuthenticationProvider.credentialsExpired=入力された ID のパスワードの有効期限が切れています AbstractUserDetailsAuthenticationProvider.badCredentials=入力された ID あるいはパスワードが正しくありません UserInfoUserDetailsService.usernameNotFound=入力された ID あるいはパスワードが正しくありません typeMismatch.java.math.BigDecimal=数値を入力して下さい。 typeMismatch.java.lang.Long=数値を入力して下さい。
UserInfoUserDetailsService.usernameNotFound
を追加します。
UserInfoUserDetailsService.java
■その1
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.dao.UserInfoDao; import ksbysample.webapp.lending.dao.UserRoleDao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.entity.UserRole; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Component public class UserInfoUserDetailsService implements UserDetailsService { @Autowired private UserInfoDao userInfoDao; @Autowired private UserRoleDao userRoleDao; @Autowired private MessageSource messageSource; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo userInfo = userInfoDao.selectByMailAddress(username); if (userInfo == null) { throw new UsernameNotFoundException( messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , new Object[]{}, LocaleContextHolder.getLocale())); } Set<SimpleGrantedAuthority> authorities = new HashSet<>(); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); if (userRoleList != null) { authorities.addAll( userRoleList.stream().map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); } return new UserInfoUserDetails(userInfo, authorities, true, true, true); } }
private MessageSource messageSource;
を追加します。throw new UsernameNotFoundException(String.format("username(%s) not found.", username));
→throw new UsernameNotFoundException(messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound", new Object[]{}, LocaleContextHolder.getLocale()));
へ変更します。
■その2
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo userInfo = userInfoDao.selectByMailAddress(username); if (userInfo == null) { throw new UsernameNotFoundException( messageSource.getMessage("UserInfoUserDetailsService.usernameNotFound" , new Object[]{}, LocaleContextHolder.getLocale())); } Set<SimpleGrantedAuthority> authorities = new HashSet<>(); List<UserRole> userRoleList = userRoleDao.selectByUserId(userInfo.getUserId()); if (userRoleList != null) { authorities.addAll( userRoleList.stream().map(userRole -> new SimpleGrantedAuthority(userRole.getRole())) .collect(Collectors.toList())); } return new UserInfoUserDetails(userInfo, authorities, true, true); }
- loadUserByUsername メソッドの最後の
return new UserInfoUserDetails(userInfo, authorities, true, true);
から true を1つ削除します。
create_table.sql
create table user_info ( user_id bigserial primary key , username varchar(32) not null , password varchar(256) not null , mail_address varchar(256) not null , enabled smallint not null default 1 , cnt_badcredentials smallint not null default 0 ); create index user_info_idx_01 on user_info(mail_address);
- user_info テーブルに cnt_badcredentials カラムを追加します。
UserInfoDao.java
package ksbysample.webapp.lending.dao; import ksbysample.webapp.lending.entity.UserInfo; import ksbysample.webapp.lending.util.doma.ComponentAndAutowiredDomaConfig; import org.seasar.doma.Dao; import org.seasar.doma.Delete; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.Update; /** */ @Dao @ComponentAndAutowiredDomaConfig public interface UserInfoDao { /** * @param userId * @return the UserInfo entity */ @Select UserInfo selectById(Long userId); @Select UserInfo selectByMailAddress(String mailAddress); /** * @param entity * @return affected rows */ @Insert int insert(UserInfo entity); /** * @param entity * @return affected rows */ @Update int update(UserInfo entity); @Update(sqlFile = true) int incCntBadcredentialsByMailAddress(String mailAddress); @Update(sqlFile = true) int initCntBadcredentialsByUsername(String username); /** * @param entity * @return affected rows */ @Delete int delete(UserInfo entity); }
int incCntBadcredentialsByMailAddress(String mailAddress);
を追加します。int initCntBadcredentialsByUsername(String username);
を追加します。- どちらのメソッドも @Update アノテーションに
sqlFile = true
を指定します。引数が Entity クラスでなくてもエラーになりません。 - initCntBadcredentialsByUsername メソッドの引数が username なのは、この後に実装する AuthenticationSuccessEventListener クラスの onApplicationEvent メソッドの引数 AuthenticationSuccessEvent event の
event.getAuthentication().getName()
の戻り値が mailAddress ではなく username のためです。event.getAuthentication().getClass().getName()
を出力してみたらorg.springframework.security.authentication.UsernamePasswordAuthenticationToken
というクラスが返ってきたので、何か別の変更が必要なようです。今回はこのまま進めます。
incCntBadcredentialsByMailAddress.sql
update user_info set cnt_badcredentials = cnt_badcredentials + 1 where mail_address = /* mailAddress */'tanaka.taro@sample.com' and cnt_badcredentials < 5
initCntBadcredentialsByMailAddress.sql
update user_info set cnt_badcredentials = 0 where username = /* username */'tanaka taro'
UserInfoService.java
package ksbysample.webapp.lending.service; import ksbysample.webapp.lending.dao.UserInfoDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserInfoService { @Autowired private UserInfoDao userInfoDao; public void incCntBadcredentials(String mailAddress) { userInfoDao.incCntBadcredentialsByMailAddress(mailAddress); } public void initCntBadcredentials(String mailAddress) { userInfoDao.initCntBadcredentialsByMailAddress(mailAddress); } }
AuthenticationSuccessEventListener.java
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.service.UserInfoService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.stereotype.Component; @Component public class AuthenticationSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> { @Autowired private UserInfoService userInfoService; @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { // 入力されたID(メールアドレス)の user_info.cnt_badcredentials を 0 にする userInfoService.initCntBadcredentials(event.getAuthentication().getName()); } }
UserInfoUserDetails.java
package ksbysample.webapp.lending.security; import ksbysample.webapp.lending.entity.UserInfo; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Set; public class UserInfoUserDetails implements UserDetails { private UserInfo userInfo; private final Set<? extends GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public UserInfoUserDetails(UserInfo userInfo , Set<? extends GrantedAuthority> authorities , boolean accountNonExpired , boolean credentialsNonExpired) { this.userInfo = userInfo; this.authorities = authorities; this.accountNonExpired = accountNonExpired; this.accountNonLocked = (userInfo.getCntBadcredentials() < 5); this.credentialsNonExpired = credentialsNonExpired; this.enabled = (userInfo.getEnabled() == 1); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return userInfo.getPassword(); } @Override public String getUsername() { return userInfo.getUsername(); } public String getMailAddress() { return userInfo.getMailAddress(); } @Override public boolean isAccountNonExpired() { return accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; } }
- コンストラクタの以下の点を変更します。
- 引数から
boolean accountNonLocked
を削除します。 this.accountNonLocked = accountNonLocked;
→this.accountNonLocked = (userInfo.getCntBadcredentials() < 5);
へ変更します。
- 引数から
履歴
2015/07/18
初版発行。