Spring Security その２〜DB認証

[前回の記事](https://qiita.com/daiki7010/items/fd4fdf8afd51c3ca9917)でログイン機能の作成を実施しましたが、ログインユーザは「User」で固定。パスワードはコンソールに出力と、実際の運用には程遠い内容。

今回はDBに登録されたユーザ名（ID）と、パスワードを照合していきます。

## テーブルの準備
今回使用するテーブルはこちら
spring securityの学習がメインなので詳細は割愛しますが、account-idはcharacterにすればよかった。
account_lock〜account_groupまでは今回は使用しませんが、いずれこの辺も使用する予定
telephone1と2はユーザID、パスワードの他に、もう１点（合計３点一致）で認証とする場合に使用する予定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555551/dd5917ed-ba70-884b-bd5e-7c26b4dfaf56.png)

## テーブルにデータを追加
id:12345
password:password
で登録しておきます。
※パスワードはハッシュ化した値を格納します。
ハッシュ化は[このサイト](http://www.tekboy.net/bcrypt-calculator)で実施できます。
ハッシュ化についての説明は[こちらの記事を読んでください](https://qiita.com/daiki7010/items/b15de9ef747f5b23c984)

```sql
select account_id,password from user_account;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555551/29d93565-ce72-db2d-cef4-8d7d8fee54e9.png)

## テーブルから取得した値を格納するentityを作成
```java
package com.example.entity;
public record UserAccount(int accountId,String password) {}
```

## テーブルからaccount_idとpasswordを取得するmapperを作成
- Java
```java
package com.example.mapper;

import org.apache.ibatis.annotations.Mapper;

import com.example.entity.UserAccount;

@Mapper
public interface DemoMapper {
	public UserAccount getAccount(int accountId);
}

```

- xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.DemoMapper">
    <select id="getAccount" resultType="com.example.entity.UserAccount">
        SELECT
            account_id,
            password
         FROM
            user_account
         WHERE
            account_id = #{accountId}   
    </select>
</mapper>
```

## 今回のメインのひとつ UserDetailsの実装
UserDetailsインターフェースを実装します。
ポイント１：コンストラクタを作成し、UserAccountを設定します。
ポイント２：getUsernameとgetPasswordに上記で設定したUserAccountから取得した値を返すよう実装します。
その他は今回は使用しないのでis〜のメソッドは<font color="Red">「true」</font>を返却する。
注意：自動生成された時点ではFalseを返却するようになってます。Falseではアカウントロック状態等になってしまうので、Trueを返却するように変更します。

```java
package com.example.auth;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.example.entity.UserAccount;

public class DemoUserDetails implements UserDetails {
	private final UserAccount user ;
	
	public DemoUserDetails(UserAccount user) {
		this.user =user;		
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// TODO 自動生成されたメソッド・スタブ
		return null;
	}

	@Override
	public String getPassword() {
		return this.user.password();
	}

	@Override
	public String getUsername() {
		return Integer.valueOf(user.accountId()).toString();
	}

	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@Override
	public boolean isEnabled() {
		return true;
	}
}
```

## もう一つのメイン UserDetailsServiceの実装
ポイント１：mapperを使用し、Dbからデータを取得し、UserAccountに設定する。
※アカウントIDをintで作成してしまったため、型変換が必要。ん〜やっぱりcharacterで作成すべきだった・・・
ポイント２：取得結果が０件（ユーザが存在しない）の場合、「UsernameNotFoundException」をthrowする
ポイント３：取得したUserAccountを指定して、UserDetailsを作成する

```java
package com.example.auth;

import org.springframework.beans.factory.annotation.Autowired;
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.Service;

import com.example.entity.UserAccount;
import com.example.mapper.DemoMapper;


@Service
public class DemoUserDetailsService implements UserDetailsService {

	@Autowired
	DemoMapper mapper;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserAccount user = mapper.getAccount(Integer.valueOf(username));
		if (user == null) {
			throw new UsernameNotFoundException("not found : " + username);
		}
		return (new DemoUserDetails(user));
	}
}
```
## 最後にSecurityConfigを修正

ポイント：passwordEncoderを追加
```java
package com.example.conf;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        .formLogin()
        .and()
        .logout()
        .and()
        .authorizeHttpRequests(authz -> authz
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .permitAll()
                .anyRequest().authenticated()
        );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```

## 実行してログインしてみる
http://localhost:8080/toppage にアクセスするとログイン画面が表示されるので
UserNameに12345
Passwordにpassword を入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555551/2b6ae8ff-7d77-81fe-0c00-a47ae7c284d5.png)

ログイン成功し、トップページへ遷移できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555551/42acd3cb-a415-f68e-b710-7a863199669a.png)