spring boot security + DB認証を試した時のポイント

# はじめに
- 試したものは[spring security](https://spring.io/guides/gs/securing-web/)のサンプルにDBからのユーザ検索を追加した形になります
- 記載していないところ(htmlとか)はサンプルそのまま使用しています
- プロジェクトは[Spring Initializr](https://start.spring.io/)で作成します
- DBはPostgreSQLを使用します

## Spring securityの初期設定

```java:WebSecurityConfigurer.java
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

	@Bean
	PasswordEncoder passwordEncoder() {
		// テストのためパスワードの暗号化はしない
		return NoOpPasswordEncoder.getInstance();
	}

	@Override
	public void configure(WebSecurity web) throws Exception {
		// spring securityで無視するリクエストパスを設定
		web.ignoring().antMatchers("/css/**", "/resources/**");
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// ログイン処理の認証ルールを設定
		http.authorizeRequests()
				.antMatchers("/", "/login").permitAll() // 認証なしでアクセス可能なパス
				.anyRequest().authenticated() // それ以外は認証が必要
				.and()
			.formLogin()
				.loginPage("/login")
				.loginProcessingUrl("/login") // ログインフォームのアクションに指定したURL[action="@{/login}"]を設定
				.usernameParameter("username") // ログインフォームのユーザー欄のname属性を設定
				.passwordParameter("password") // ログインフォームのパスワード欄のname属性を設定
				.successForwardUrl("/home") // ログイン成功時に遷移するURL
				.failureUrl("/login?error") // ログイン失敗時に遷移するURL
				.permitAll()
				.and()
			.logout()
				.logoutUrl("/logout")
				.permitAll()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
	}
}
```

## ログインURLのマッピング
- WebMvcConfigurerインターフェースを実装したクラスに書く必要がある

```java:WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
	public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }
}
```

## 認証後のURLのマッピング
- 認証情報(UserModel)は引数のAuthenticationから取得可能

```java:DemoController.java
@Controller
public class DemoController {
	@RequestMapping(value = {"/", "/home"})
	public String home() {
		return "home";
	}

	@RequestMapping(value = "/hello")
	public String hello(Authentication authentication, Model model) {
		// 認証情報を取得
		UserModel userModel = (UserModel)authentication.getPrincipal();
		model.addAttribute("name", userModel.getName());

		return "hello";
	}
}
```

# ここからDB認証部分の実装になります
- やることはDB検索して結果を返すだけなので簡単です

## DB接続情報の設定

```properties:application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver
```

## ログインユーザの情報を格納するテーブルを作成
- データは適当に追加して下さい

```sql
create table t_user (
  id character varying(10) not null
  , name character varying(100) not null
  , password character varying(500) not null
  , enabled boolean DEFAULT true
  , primary key (user_id)
);
```

## 認証情報を格納するBeanの作成
- ユーザーネームに該当するものがIDになるので、メソッド[getUsername]にはidを返すようにしています

```java:UserModel.java
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UserModel implements UserDetails {

	private String id;

	private String name;

	private String password;

	private boolean enabled;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return null;
	}

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

	@Override
	public String getUsername() {
		return this.id;
	}

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

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

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

	@Override
	public boolean isEnabled() {
		return this.enabled;
	}

}
```

## ログインユーザを取得するSQL
```xml:UserRepository.xml
<mapper namespace="com.example.demo.UserRepository">
  <select id="selectByUser" parameterType="com.example.demo.UserRepository" resultType="com.example.demo.UserModel">
    SELECT id, name, password, enabled FROM t_user where id = #{id};
  </select>
</mapper>
```

## ログインユーザを取得するSQLのインターフェース
```java:UserRepository.java
@Mapper
public interface UserRepository {
	public UserModel selectByUser(String username);
}
```

## 認証情報を検索する
- UserDetailsServiceインターフェースのloadUserByUsernameでDB検索するだけ

```java:UserDetailsServiceImpl.java
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		if (StringUtils.isEmpty(username)) throw new UsernameNotFoundException("");

		UserModel userModel = userRepository.selectByUser(username);

		// ユーザが存在しない場合
		if (userModel == null) throw new UsernameNotFoundException("");
		// アカウントの有効期限切れ、アカウントのロック、パスワードの有効期限切れ、ユーザの無効を判定
		if (!userModel.isAccountNonExpired() || !userModel.isAccountNonLocked() ||
				!userModel.isCredentialsNonExpired() || !userModel.isEnabled())
			throw new UsernameNotFoundException("");
		return userModel;
	}

}
```

# ここまで出来れば、あとはアプリを実行するだけでDB認証が試せます

## その他 ハマったこと
- 画面を表示しようとしたら[Circular view path [login]]が発生
 - 原因：[Spring Initializr](https://start.spring.io/)でプロジェクトを作成していたため、pom.xmlにthymeleafを追加していなかった
 - 対処方法：pom.xmlに下記を追加

```xml:pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```