SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

SpringSecurity:データベース認証を実装する  SpringBoot/thymeleaf/STS3

SpringBootの高機能なセキュリティ機構「SpringSecurity」を使います。

今回は、データベース認証に切り替える・・をやります。

STS3(3.9.6)+SpringBoot2.0+Tymeleaf3.0迄動作確認しています。

f:id:arakan_no_boku:20190222012501j:plain

 

まずは仕組みの理解から

 

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");

 

ログイン画面を表示させて、さきほどデータベースに登録したユーザとパスワードでログインしてみます。

f:id:arakan_no_boku:20180321225329j:plain

 

いけました。

f:id:arakan_no_boku:20180321225445j:plain

 

これでデータベース認証はOKです。

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com

arakan-pgm-ai.hatenablog.com