Spring-Boot ～Spring-Securityを使ったDB認証～

#はじめに
最近SpringBootを学び始めたプログラミング初学者です。
SpringSecurityを使用したDB認証を実装しようとしたのですが、思った以上に詰まってしまったので備忘録も兼ねて記事にしました。
本当に必要最小限の機能しか実装していないので、とりあえずSpringSecurityを使ったDB認証がしたい！という人向けの記事になるかと思います。
# 環境
* Eclipse 4.19.0
* SpringBoot 2.5.3
* Java11
* ビルドツール：Maven
* テンプレートエンジン：Thymeleaf
* O/Rマッパー：MyBatis
* DB：MySQL

##SpringInitializrの設定![Spring_initializr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593491/b5e8580f-39d8-99c5-b6be-b5c4a5cbfd88.png)

**出来上がったpom.xml**

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>Login6</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Login6</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.0</version>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
```

###プロジェクトのフォルダ構成
<img width="300" alt="パッケージ・エクスプローラー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593491/112a5791-d444-5613-9973-ddff08e2fd37.png">

* **UserModel.java**<br>
DBから取得したユーザ―情報を格納するEntityクラス<br>
UserDetailsをimplementsする
* **UserListMapper.java**<br>
Mapperインターフェース (MyBatisで使用するため詳細説明は省略)<br>
* **UserListMapper.xml**<br>
Mapperインターフェースに対応するXMLファイル(MyBatisで使用するため詳細説明は省略)<br>
* **UserService.java**<br>
サービスクラス<br>
 UserDetailsServiceをimplementsして実装する<br>
* **MainController.java**<br>
 コントローラークラス<br>
* **Login6Application.java**<br>
 SpringBootの起動クラス<br>
* **WebSecurityConfig.java**<br>
SpringSecurityの設定を行うクラス<br>
WebSecurityConfigurerAdapterをextendsして実装する<br>
* **loginForm.html**<br>
ログイン認証を行う画面<br>
* **success.html**<br>
ログイン認証に成功した時に遷移する画面<br>
* **application.properties**<br>
DBへの接続情報を記述しているファイル<br>
* **mybatis-config.xml**<br>
MyBatisの設定ファイル<br>
* **pom.xml**<br>
プロジェクトのビルドに関する情報やライブラリの依存関係などが記述されているファイル<br>

##Login6Application.java
SpringBootの起動クラスです。
MyBatis用の設定を追記していること以外はデフォルトのままです。

```java:Login6Application.java
package com.example;

import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;

@SpringBootApplication
@MapperScan(basePackages="com.example.persistence")
public class Login6Application {

	public static void main(String[] args) {
		SpringApplication.run(Login6Application.class, args);
	}

	
    // MyBatisの設定
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        // コンフィグファイルの読み込み
        sessionFactory.setConfigLocation(new ClassPathResource("/mybatis-config.xml"));
        return sessionFactory.getObject();
    }
}
```

##MainController.java
コントローラークラスです。

```java:MainController.java
package com.example.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MainController {

	@RequestMapping("/loginForm")
	public String loginForm() {
		return "loginForm";
	}

	@RequestMapping(value = "/loginForm", params = "error")
	public String error(Model model) {
		model.addAttribute("errorMsg", "ログイン認証に失敗しました");
		return "loginForm";
	}

	@RequestMapping("/success")
	public String success() {
		return "success";
	}
}
```
##WebSecurityConfig.java
SpringSecurityの設定用クラスです。
詳細はコード内のコメントに記載していますが、このクラスでログイン認証の具体的な設定を行っています。


```java:WebSecurityConfig.java
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.example.service.UserService;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserService userService;
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //アクセスポリシーの設定
        http.authorizeRequests()
            //指定したパターンごとに制限をかける
            .antMatchers("/js/**", "/css/**").permitAll()//制限なし
            .anyRequest().authenticated();//上記以外は制限あり
        //フォーム認証の設定
        http.formLogin()
            /*ログインページの指定（指定なしの場合デフォルトのものが用意される）
              コントローラークラスでこのURLをマッピングしておく*/
            .loginPage("/loginForm")  
            /*ログイン処理のパス（このURLへリクエストが送られると認証処理が実行される）
              ログインページのformタグのth:action属性にこのURLを指定しておく*/
            .loginProcessingUrl("/login")
            /*ログインページのログイン情報入力欄のname属性に以下の名称を指定する*/
            .usernameParameter("user")
            .passwordParameter("pass")
            /*ログイン成功時に遷移するページ(trueで成功時は常にここに飛ぶ）
             コントローラークラスでこのURLをマッピングしておく*/
            .defaultSuccessUrl("/success", true)
            /*失敗時の遷移先、アクセス制限は解除する
              コントローラークラスでこのURLをマッピングしておく*/
            .failureUrl("/loginForm?error").permitAll();
    }
    
    /**
     * 認証するユーザー情報をデータベースからロードする処理
     * @param auth　認証マネージャー生成ツール
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ///認証するユーザー情報をデータベースからロードする
        //その際、パスワードはBCryptでハッシュ化した値を利用する
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }
 
    /**
     * パスワードをBCryptでハッシュ化するクラス
     * ハッシュ化するクラスも幾つか種類があるみたいです
     * @return パスワードをBCryptで暗号化するクラスオブジェクト
     */
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
```

##UserService.java
UserDetailsServiceをimplementsしているサービスクラスです。
実装する必要があるのはloadUserByUsernameメソッドだけで、ユーザーネーム（今回はid）を引数にユーザ情報を返す処理を記述します。
今回はO/RマッパーにMyBatisを使用しており、マッパーインタフェースのcertificateメソッドを呼んでいます。

```java:UserService.java
package com.example.service;

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.persistence.UserListMapper;

@Service
public class UserService implements UserDetailsService{
	@Autowired
	private UserListMapper userListMapper;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userListMapper.certificate(username);
	}
}
```


##UserModel.java
DBから取得したユーザー情報を格納するEntityクラスです。
UserDetailsをImplementsして、UserDetailsに記述してあるメソッドを実装します。

```java:UserModel.java
package com.example.domain;

import java.util.Collection;

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

public class UserModel implements UserDetails{
	private String id;
	private String pass;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		//1つのユーザーがROLEを複数持つときに使うみたい
		return null;
	}
	@Override
	public String getPassword() {
               //パスワードを返す
		return pass;
	}
	@Override
	public String getUsername() {
               //ユーザー名（今回はid）を返す。
		return id;
	}
	@Override
	public boolean isAccountNonExpired() {
		//ユーザアカウントが認証期限切れしていないかどうかを返す
                //認証期限切れしている場合falseを返すが、今回は特に考慮していないので
                //固定でtrueを返しておく（以下メソッドでも同様の理由でtrueを返している）
		return true;
	}
	@Override
	public boolean isAccountNonLocked() {
		//ユーザアカウントがロックしていないかどうか
		return true;
	}
	@Override
	public boolean isCredentialsNonExpired() {
		//ユーザアカウントの資格が認証期限切れしていないかどうか
		return true;
	}
	@Override
	public boolean isEnabled() {
		//ユーザアカウントが無効になっていないか
		return true;
	}
}
```
##UserListMapper.java
マッパーインターフェースクラスです。
MyBatisに関することなので説明は省略します。

```java:UserListMapper.java
package com.example.persistence;

import org.apache.ibatis.annotations.Param;
import com.example.domain.UserModel;

public interface UserListMapper {
	public UserModel certificate(@Param("id") String id);
}
```

##UserListMapper.xml
マッパーインターフェースクラスに対応するxmlファイルです。
同様に説明は省略します。

```xml:UserListMapper.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.persistence.UserListMapper">
    <select id="certificate" resultType="com.example.domain.UserModel">
    	SELECT id, pass FROM userlist WHERE id = #{id}
    </select>
</mapper>
```


##loginForm.html
ログイン認証を行う画面です。
UserIDとPasswordの入力欄のname属性と、formタグのth:action属性はWebSecurityConfig.javaで設定しているものを記述します。

```html:loginForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ログイン認証</title>
</head>
<body>
	<form th:action="@{/login}" method="post">
	
		<span th:if="${errorMsg}" th:text="${errorMsg}"></span><br>
	
		UserID：<input type="text" name="user" /><br> 
		Password：<input type="password" name="pass" /><br>
		
		<input type="submit" value="認証"/>
	</form>
</body>
</html>
```

##success.html
ログイン認証が成功した時に遷移する画面です。

```html:success.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>認証成功</title>
</head>
<body>
認証成功！！！
</body>
</html>
```

##application.properties
DBへの接続情報を記述しています。

```properties:application.properties
#DBのドライバ設定
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#接続用URL
spring.datasource.url=jdbc:mysql://XXX/XXX
#ユーザ名
spring.datasource.username=XXX
#パスワード
spring.datasource.password=XXX
```

##mybatis-config.xml
MyBatisの設定情報を記述しています。

```xml:mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC
        "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
</configuration>
```
##テーブル定義
参考までにテーブル情報を載せておきます。
ちなみに、画面で入力されたパスワードはハッシュ化され、予めDBに登録してあるパスワード（のハッシュ値）と比較されます。
そのため、DBに格納するパスワードは予めハッシュ化しておく必要があります。
今回ハッシュ化に使用した[BCryptPasswordEncoderクラス](https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html)を利用して予めハッシュ化したパスワードをDBに登録しておくといいでしょう。

```sql:create.sql
CREATE TABLE userlist (
  id int NOT NULL,
  pass varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) 
```
##参考にした記事
[SpringSecurity Reference](https://docs.spring.io/spring-security/site/docs/current/reference/html5/)
[spring boot security + DB認証を試した時のポイント](https://qiita.com/teradatk/items/1e09f0ed4a29d2699504)
[最初のSpring Security - フォーム認証＆画面遷移](https://qiita.com/huge-book-storage/items/56b86d2986cf04632706)
[Spring Security 使い方メモ　認証・認可](https://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)
[Spring-Bootでログイン機能を実装してみる](https://qiita.com/a-pompom/items/80b3f4bb6414e8678829)