SpringSecurity:データベース認証を実装する SpringBoot/thymeleaf/STS3
SpringBootの高機能なセキュリティ機構「SpringSecurity」を使います。
今回は、データベース認証に切り替える・・をやります。
STS3(3.9.6)+SpringBoot2.0+Tymeleaf3.0迄動作確認しています。
まずは仕組みの理解から
SpringSecurityは、もともと、データベース認証の仕組みをもってます。
Interfaceの「userDetails」「UserDetailsService」がそうです。
データベース認証をするには、それらの実装クラスを作成します。
「UserDetailsService」の実装クラスで、データベースのアカウント情報にアクセスして、アカウント情報が見つからない場合は、UsernameNotfoundExceptionをスローし、見つかった場合(認証成功)の場合は、その情報でUserDetailsを生成するわけです。
ロール(権限)情報も「UserDetailsService」内で生成します。
SpringSecurityの「ROLE_」で始まる始まる権限情報をロールとして使うという暗黙の
ルールに従う必要があります。
そして、実装した「UserDetailsService」を、Javaコンフィグクラスで有効化してやれば、データベース認証できるようになる。
・・とまあ、こんな感じなわけです。
まず、アカウントを保存するテーブルとエンティティを用意する
最低限必要な項目のみを持つアカウントテーブルです。(MySQLです)
create table account ( id INT not null comment 'id'AUTO_INCREMENT , username VARCHAR(50) not null comment 'username' , password VARCHAR(100) not null comment 'password' , enabled TINYINT not null comment 'enabled' , admin TINYINT not null comment 'admin' , constraint account_PKC primary key (id) ) comment 'account' ; create unique index account_IX1 on account(username); create unique index account_IX2 on account(id);
対応 するエンティティクラスです。
@Entity public class Account { protected Account(){} public Account(String username,String password,boolean isAdmin){ setId(0L); setUsername(username); setPassword(password); setEnabled(true); setAdmin(isAdmin); } @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(nullable = false, unique = true) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private boolean enabled; @Column(nullable = false) private boolean admin; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean isAdmin() { return admin; } public void setAdmin(boolean isAdmin) { this.admin = isAdmin; } }
Account エンティティを操作するRepositoryインタフェースを定義します。
public interface AccountRepository extends JpaRepository<Account, Integer> { public Account findByUsername(String username); }
Accountクラスを使って、UserDetailsインタフェースを実装する
まず、ソースから。
public class UserAccount implements UserDetails { private static final long serialVersionUID = -256740067874995659L; private Account user; private Collection<GrantedAuthority> authorities; protected UserAccount(){} public UserAccount(Account account,Collection<GrantedAuthority> authorities){ this.user = account; this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return user.isEnabled(); } }
上記で若干手抜きしている部分があります。
isAccountNonExpired()、 isAccountNonLocked()、isCredentialsNonExpired() の3つはAccountテーブルにも持たず、単にTrueを返すだけにしてます。
これらは「アカウントの期限きれ」「アカウントのロック」「資格情報の期限切れ」などを管理する場合に実装する部分ですが・・今回のサンプルでは必要ないので。
ほとんど、見ればわかる・・ようなソースですが、1点補足します。
この部分ですが・・
private Account user;
private Collection<GrantedAuthority> authorities;public UserAccount(Account account,Collection<;GrantedAuthority>; authorities){
this.user = account;
this.authorities = authorities;
}
authoritiesは「ROLE_ADMIN」や「ROLE_USER」などのロール情報を保持するリストです。
1アカウントに対して、N個のロールを持つことになるので、Accountテーブルとは別に管理して、コンストラクタでも別々に引数として受け取るようになってます。
こうしている理由は管理しやすいからです。
UserDetailsServiceインタフェースを実装する
UserDetailsインタフェースを実装したら、それを操作するUserDetailsServiceを実装してやる必要があります。
こちらもまずはソースです。
@Service public class UserAccountService implements UserDetailsService { @Autowired private AccountRepository repository; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (username == null || "".equals(username)) { throw new UsernameNotFoundException("Username is empty"); } Account ac = repository.findByUsername(username); if (ac == null) { throw new UsernameNotFoundException("User not found: " + username); } if (!ac.isEnabled()) { throw new UsernameNotFoundException("User not found: " + username); } UserAccount user = new UserAccount(ac,getAuthorities(ac)); return user; } private Collection<GrantedAuthority> getAuthorities(Account account){ if(account.isAdmin()){ return AuthorityUtils.createAuthorityList("ROLE_ADMIN","ROLE_USER"); }else{ return AuthorityUtils.createAuthorityList("ROLE_USER"); } } @Transactional public void registerAdmin(String username, String password) { Account user = new Account(username, passwordEncoder.encode(password),true); repository.save(user); } @Transactional public void registerUser(String username, String password) { Account user = new Account(username, passwordEncoder.encode(password),false); repository.save(user); } }
usernameを受け取って、UserDetailsオブジェクトを返すメソッド( loadUserByUsername)と、ユーザをDBに登録する(registerAdmin、registerUser)、および、ロール(権限のリスト)を返す(getAuthorities)を実装しています。
パスワードは「passwordEncoder.encode」でエンコードしてます。
passwordEncoderの実態は、あとで設定クラスで定義します。
権限は、今回はサンプルで2つ(ADIMINとUSER)しか扱わないので、DBにADMINか否かの情報だけもって処理しています。
ここで得に注目すべきところは、 loadUserByUsernameです。
よく見ると、usernameを受け取って、UserDetailsオブジェクトを返すだけで、passwordのチェックをしている部分がありません。
でも、これで良いのです。
パスワードのチェックはSpringSecurityにまかせる・・でOKです。
JAVAコンフィグクラスでSpringSecurityを有効にする
テーブルが用意できて、エンティティとUserDetailsとUserDetailsServiceの各実装クラスを用意できただけでは、機能しません。
JAVAの設定クラスで有効にします。
今回追加する部分のみのソースです。
@Autowired private UserDetailsService userDetailsService; @Autowired void configureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception{ auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
passwordEncoder()で利用するパスワードエンコードを行うオブジェクトを返すようにして、 AuthenticationManagerに、UserDetailsServiceの実装クラスと、passwordEncoder()を登録しているのがわかると思います。
これで使えるようになります。
ソース全体はこうです。
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity web)throws Exception{ web.formLogin().loginPage("/login").defaultSuccessUrl("/inpg01").failureUrl("/login-error").permitAll(); web.authorizeRequests().antMatchers("/css/**", "/images/**", "/js/**").permitAll().anyRequest().authenticated(); } @Autowired void configureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception{ auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
さて、実行してみましょう
まず、テストクラスかなんかで、DBにユーザ登録します。
@Autowired
private UserAccountService sv;・・
sv.registerAdmin("dbadmin","dbadpass");
sv.registerUser("dbuser","dbusrp");
ログイン画面を表示させて、さきほどデータベースに登録したユーザとパスワードでログインしてみます。
いけました。
これでデータベース認証はOKです。