Spring Security 5.7に対応したJPAベースのログイン認証とユーザ登録 (+バリデーションチェック)

# 目次
[1. はじめに](#1-はじめに)
[2. プロジェクトの作成と準備](#2-プロジェクトの作成と準備)
[3. ユーザ登録機能](#3-ユーザ登録機能)
[4. ログイン認証機能](#4-ログイン認証機能)
[5. まとめ](#5-まとめ)

# 1. はじめに
Spring Secutiry 5.7でセキュリティ設定の記述方法が大幅に変わりましたが、5.7かつJPAをベースとしたログイン認証のサンプルが見当たらなかったので、紹介したいと思います。

**※本記事の内容は、ある程度Springやデータベースの知識がある前提です。**

また、この記事を書くにあたって下記のサイトを参考にさせていただきました。
特に2つ目のサイトは、Spring初学者の方でも一からアプリケーションを作成できるような内容となっていますので、ぜひご覧ください。

参考サイト1:&emsp;[最新の5.7で学ぶ！初めてのひとのためのSpring Security](https://www.docswell.com/s/MasatoshiTada/KGVY9K-spring-security-intro#p1)
参考サイト2:&emsp;[新人社員がSpring Securityで認証・認可機能を一から作ってみた](https://qiita.com/y-yamagata/items/7232b9969fcb7ce85b54#4-%E5%AE%9F%E6%A5%AD%E5%8B%99%E5%90%91%E3%81%91%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF%E6%A9%9F%E8%83%BD%E3%81%AE%E5%AE%9F%E8%A3%85)

# 2. プロジェクトの作成と準備
本章では、プロジェクトに必要なパッケージの導入とデータベースの作成を行います。

## 2-1. 実行環境
作成したプロジェクトの実行環境は次の通りです。
- Java 17
- Spring Boot 2.7.2
- Spring Security 5.7.2
- Maven 3.8.4
- PostgreSQL 14.4

また、プロジェクトで使用したパッケージは次の通りです。
- Spring Data JPA
- Thymeleaf
- Spring Web
- Spring Boot DevTools
- PostgreSQL
- Spring Security
- Spring Validation

pom.xmlにこれらの依存関係を追加します。

```xml:pom.xml (抜粋)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</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.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>
```

## 2-2. データベースの作成と接続設定

今回はsampleという名前でデータベースを作成しました。
このデータベースにはusersというテーブルを作成します。直接データを登録する必要はありません。

```:schema.sql
DROP DATABASE IF EXISTS sample;
CREATE DATABASE sample;
\c sample;

DROP TABLE IF EXISTS users;
CREATE TABLE users(
    id SERIAL PRIMARY KEY,
    username VARCHAR(20),
    password VARCHAR(60),
    rolename VARCHAR(10)
);
```

**※パスワードはハッシュ化して保存するため、VARCHAR(60)としています。**
**今回使用したBCryptPasswordEncoderでは60文字のハッシュ文字列が生成されます。**

次に、接続設定の記述をapplication.propertiesに行います。

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

今回はrootユーザのpostgresでsampleデータベースを作成しました。

## 2-3. セキュリティ設定
本記事では、まずユーザ登録機能から作成しますが、Configurationクラスで認可設定をしないと登録画面にアクセスすることができないため、まずConfigurationクラスを作成します。

```java:SecurityConfig.java (Configuration)
package com.example.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests(authz -> authz
				.mvcMatchers("/css/**").permitAll()
				.mvcMatchers("/user/registration").permitAll()
				.anyRequest().authenticated()
		);
        return http.build();
	}
}
```

Spring Security 5.5からAuthorizationManagerベースのAuthorizationFilterが導入されました。
AuthorizationFilterは、従来使用されていたFilterSecurityInterceptorというAccessDecisionManagerベースのフィルターに代わる新たな認可設定のフィルターであり、公式のドキュメントでも今後はAuthorizationFilterを用いることが推奨されています。

AuthorizationFilterを用いた認可を行うためのメソッドが、`http.authorizeHttpRequests()`となります。
(従来は`http.authorizeRequests()`というメソッドが使われていました)

参考サイト1:&emsp;[最新の5.7で学ぶ！初めてのひとのためのSpring Security](https://www.docswell.com/s/MasatoshiTada/KGVY9K-spring-security-intro#p1)
参考サイト2:&emsp;[AuthorizationFilter で HttpServletRequests を認証する (公式)](https://spring.pleiades.io/spring-security/reference/servlet/authorization/authorize-http-requests.html)

この認可設定によってcssファイルとユーザ登録ページのアクセスは誰でも可能となり、それ以外のURLはログインが必要になります。

# 3. ユーザ登録機能

本章では、ユーザ登録機能を作成します。
バリデーションによって登録情報を正しいものにし、パスワードをハッシュ化してusersテーブルに登録します。

## 3-1. 画面の作成とControllerクラスの設定
まず、画面を作成します。htmlとcssは以下の通りです。

```html:user-registration.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" th:href="@{/css/style.css}">
    <title>User Registration</title>
</head>

<body>
    <main class="main">
    	<form class="form" th:action="@{/user/registration}" method="post">
    		<h1 class="form__title">User Registration</h1>
    		<p class="form__item">
    			<label for="username">Username</label>
    			<input type="text" placeholder="Type Username" name="username" id="username">
    		</p>
    		<p class="form__item">
    			<label for="password">Password</label>
    			<input type="password" placeholder="Type Password" name="password" id="password">
    		</p>
    		<p class="form__item">
    			<label for="confirm-password">Confirm Password</label>
    			<input type="password" placeholder="Type Confirm Password" name="confirmPassword" id="confirm-password">
    		</p>
    		<p class="form__item">
    			<input type="submit" value="User Registration">
    		</p>
    	</form>
    </main>
</body>
</html>
```

**※formの送信先リンクは`th:action="@{}"`となることに注意してください。**
**この記述によってCSRFトークンが自動で生成され、POSTリクエストが認可されることになります。**

```css:style.css
/*------ base ------*/

body{
	font-size: 20px;
}

*{
    box-sizing: border-box;
}

a{
	text-decoration: none;
}

input{
	margin: 0;
	outline: none;
	border: none;
    font-size: 20px;
}

input[type="submit"]{
	line-height: 40px;
	cursor: pointer;
}

.main{
	width: 600px;
	padding-top: 60px;
	margin: 0 auto;
	text-align: center;
}

/*------ form ------*/

.form__title{
	margin-bottom: 45px;
	font-size: 30px;
}

.form__item{
	margin-bottom: 45px;
	text-align: left;
}

.form__item > label{
	display: block;
	margin-bottom: 15px;
	font-weight: bold;
}

.form__item > input[type="text"], .form__item > input[type="password"]{
	width: 100%;
	border-bottom: 1px solid rgba(0, 0, 0, 0.2);
	line-height: 40px;
}

.form__item > input[type="submit"]{
	width: 100%;
}
```

次に、Controllerクラスを作成しURLを設定します。

```java:AccountController.java (Controller)
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AccountController {
	@GetMapping("/user/registration")
	public String showUserRegistration() {
		return "user-registration";
	}
}
```

Controllerクラスで設定した`http://localhost:8080/user/registration`にアクセスすると、以下のようにユーザ登録画面が表示されます。
![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745690/cad92c46-4b2f-6bcb-1986-29e67d4204fa.png)
## 3-2. Formクラスの作成とバリデーション設定
次に、フォームに入力された値を受け取るためのFormクラスを作成します。

```java:UserRegistrationForm.java (Form)
package com.example.demo.form;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class UserRegistrationForm {
	@NotBlank
	@Size(min = 4, max = 20)
	private String username;
	
	@NotBlank
	@Size(min = 8, max = 20)
	private String password;
	
	@NotBlank
	@Size(min = 8, max = 20)
	private String confirmPassword;
	
	@AssertTrue
	public boolean isPasswordValid() {
		if(password == null || confirmPassword == null) {
			return false;
		}
		return password.equals(confirmPassword);
	}
	
	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 String getConfirmPassword() {
		return confirmPassword;
	}
	
	public void setConfirmPassword(String confirmPassword) {
		this.confirmPassword = confirmPassword;
	}
}
```

Formクラスでは、バリデーションの設定も行います。
今回設定した、各パラメータの制約は以下の通りです。

|パラメータ|NotBlank|Size|その他の制約|
|:-:|:-:|:-:|:-:|
|username|〇|[4, 20]||
|password|〇|[8, 20]|confirmPasswordと一致すること|
|confirmPassword|〇|[8, 20]|Passwordと一致すること|

バリデーションに関しては下記のサイトを参考にさせていただきました。
参考サイト1:&emsp;[【Spring Boot】バリデーション](https://b1san-blog.com/post/spring/spring-validation/)
参考サイト2:&emsp;[Spring Bootで作るWebアプリケーション⑦ - 相関バリデーション](https://penguinlabo.hatenablog.com/entry/springboot/web/7_correlationvalidation)

また、バリデーション設定に伴ってhtml, cssファイルも変更します。

```html:user-registration.html (変更箇所)
<form class="form" th:action="@{/user/registration}" method="post" th:object="${form}">
    <h1 class="form__title">User Registration</h1>
    <p th:if="${#fields.hasErrors('passwordValid')}" th:errors="*{passwordValid}" class="error-message"></p>
    <p class="form__item">
        <label for="username">Username</label>
        <span th:if="${#fields.hasErrors('username')}" th:errors="*{username}" class="error-message"></span>
        <input type="text" placeholder="Type Username" name="username" id="username">
    </p>
    <p class="form__item">
        <label for="password">Password</label>
        <span th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="error-message"></span>
        <input type="password" placeholder="Type Password" name="password" id="password">
    </p>
    <p class="form__item">
        <label for="confirm-password">Confirm Password</label>
        <span th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}" class="error-message"></span>
        <input type="password" placeholder="Type Confirm Password" name="confirmPassword" id="confirm-password">
    </p>
    <p class="form__item">
        <input type="submit" value="User Registration">
    </p>
</form>
```

```css:style.css (追記部分)
.error-message{
	color: red;
	font-size: 16px;
}
```

そして、エラーメッセージをカスタマイズするためにValidationMessages.propertiesを新たに作成し、エラーメッセージを記述します。

```:ValidationMessages.properties
javax.validation.constraints.NotBlank.message=		値が入力されていません。
javax.validation.constraints.Size.message=			{min}文字から{max}文字で入力してください。
javax.validation.constraints.AssertTrue.message=	パスワードが確認用パスワードと一致しません。
```

最後に、ControllerクラスにPOSTリクエストが送信されたときの処理を追記します。

```java:AccountController.java (追記)
package com.example.demo.controller;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.form.UserRegistrationForm;

@Controller
public class AccountController {
	@GetMapping("/user/registration")
	// 追記部分
	public String showUserRegistration(@ModelAttribute("form") UserRegistrationForm form) {
		return "user-registration";
	}
	
	// 追記部分
	@PostMapping("/user/registration")
	public String userRegistration(@Valid @ModelAttribute("form") UserRegistrationForm form, BindingResult result) {
		if(result.hasErrors()) {
			return "user-registration";
		}
		return "redirect:/login";
	}
}
```

GETリクエストの引数にも<span>@</span>ModelAttributeを追加します。
<span>@</span>ModelAttributeを利用することでhtml上のformタグを、Formクラスのオブジェクトにバインドすることができます。

<span>@</span>ValidをつけることでFormクラスで定義したバリデーションが機能します。
また、BindingResultクラスにはFormクラスで入力された値とバリデーションの結果が格納されています。
そのため、hasErrorメソッドでバリデーションエラーの有無を確認することができます。

参考サイト:&emsp;[Spring Bootで作るWebアプリケーション⑥ - 単項目入力バリデーション](https://penguinlabo.hatenablog.com/entry/springboot/web/6_beanvalidation)

また、今回はユーザ登録後にログイン画面 (`http://localhost:8080/user/registration`) へ遷移するように設定します。
※現時点ではページを作成していないのでアクセスすることができません。

ここで再度`http://localhost:8080/user/registration`にアクセスし、何も入力せずにユーザ登録ボタンをクリックすると、以下のようにエラーメッセージが出力されることを確認できます。
![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745690/f22c33fe-f111-0a86-5515-0f8f22ea96b9.png)
## 3-3. データベースへのユーザ登録

バリデーションの準備が整ったのでデータベースに情報を登録します。
まず、データベースのusersテーブルと対応したEntityクラスと、データベースを操作するためのRepositoryインターフェースを作成します。

```java:User.java (Entity)
package com.example.demo.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "users")
public class User {
	@Id
	@Column(name = "id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	@Column(name = "username")
	private String username;
	
	@Column(name = "password")
	private String password;
	
	@Column(name = "rolename")
	private String rolename;
	
	public User() {
		
	}
	
	public User(String username, String password, String rolename) {
		this.username = username;
		this.password = password;
		this.rolename = rolename;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer 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 String getRolename() {
		return rolename;
	}

	public void setRolename(String rolename) {
		this.rolename = rolename;
	}
}
```

```java:UserRepository.java (Repository)
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}
```

次に、Configurationクラスにパスワードをハッシュ化するためのpasswordEncoderを追加します。

```java:SecurityConfig.java (追記)
package com.example.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
public class SecurityConfig {
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeHttpRequests(authz -> authz
				.mvcMatchers("/css/**").permitAll()
				.mvcMatchers("/user/registration").permitAll()
				.anyRequest().authenticated()
		);
        return http.build();
	}

	// 追記箇所
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}
```

今回はBCryptPasswordEncoderを用いてパスワードのハッシュ化を行いました。
前述したように、BCryptPasswordEncoderでは60文字のハッシュ文字列が生成されます。

これでユーザ登録を行う準備は整ったので、ユーザ登録を行うためのServiceクラスを作成します。

```java:UserRegistrationService.java (Service)
package com.example.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;

@Service
public class UserRegistrationService {
	@Autowired
	private PasswordEncoder passwordEncoder;
	
	@Autowired
	private UserRepository userRepository;
	
	public void userRegistration(String username, String password) {
		String hashedPassword = passwordEncoder.encode(password);
		userRepository.saveAndFlush(new User(username, hashedPassword, "GENERAL"));
	}
}
```

userRegistrationメソッドは、Configurationクラスで定義したpasswordEncoderを用いてパスワードをハッシュ化し、usersテーブルにユーザ登録するメソッドとなっています。

ユーザ登録フォームから情報がPOSTリクエストされたときに、このuserRegistrationを呼び出してデータベースに情報を登録します。
したがって、ControllerクラスにuserRegistrationを呼び出す処理を追記します。

```java:AccountController.java (追記)
package com.example.demo.controller;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.form.UserRegistrationForm;
import com.example.demo.service.UserRegistrationService;

@Controller
public class AccountController {
	// 追記部分
	@Autowired
	private UserRegistrationService userRegistrationService;

	@GetMapping("/user/registration")
	public String showUserRegistration(@ModelAttribute("form") UserRegistrationForm form) {
		return "user-registration";
	}
	
	@PostMapping("/user/registration")
	public String userRegistration(@Valid @ModelAttribute("form") UserRegistrationForm form, BindingResult result) {
		if(result.hasErrors()) {
			return "user-registration";
		}
		// 追記部分
		userRegistrationService.userRegistration(form.getUsername(), form.getPassword());
		return "redirect:/login";
	}
}
```

では、実際に以下の情報でユーザ登録を行います。

|パラメータ|値|
|:-:|:-:|
|username|user1|
|password|password|

`http://localhost:8080/user/registration`にアクセスし、情報を入力して登録ボタンをクリックすると以下のようにusersテーブルに情報が追加されます。
※まだログイン画面の設定をしていないので、ログイン画面に遷移することはできません。
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745690/2a9f3c87-3654-a772-0f63-1a0579cb9f1f.png)
ハッシュ化済みのパスワードが保存されていることが確認できます。

# 4. ログイン認証機能
本章では、ログイン認証機能を作成します。
**※本記事では、ユーザロールごとの認可設定は行いません。**

## 4-1. 画面の作成とControllerクラスの設定
ユーザ登録と同様にまず画面を作成します。
ここでは、ログイン画面とログイン成功時の遷移先画面を作成します。

htmlは以下の通りです (cssファイルに変更はありません)。

```html:login.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" th:href="@{/css/style.css}">
    <title>Login</title>
</head>

<body>
    <main class="main">
    	<form class="form" th:action="@{/login}" method="post">
    		<h1 class="form__title">Login</h1>
    		<p class="form__item">
    			<label for="username">Username</label>
    			<input type="text" placeholder="Type Your Username" name="username" id="username">
    		</p>
    		<p class="form__item">
    			<label for="password">Password</label>
    			<input type="password" placeholder="Type Your Password" name="password" id="password">
    		</p>
    		<p class="form__item">
    			<input type="submit" value="Login">
    		</p>
    		<a href="/user/registration" class="to-user-registration">User Registration</a>
    	</form>
    </main>
</body>
</html>
```

```html:index.html
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" th:href="@{/css/style.css}">
    <title>Top</title>
</head>

<body>
    <main class="main">
    	<p>ログイン成功</p>
    </main>
</body>
</html>
```

**※ユーザ登録と同様に、formの送信先リンクは`th:action="@{}"`となることに注意してください。**

次に、Controllerクラスにそれぞれの画面への遷移処理を追記します。

```java:AccountController.java (追記)
package com.example.demo.controller;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.example.demo.form.UserRegistrationForm;
import com.example.demo.service.UserRegistrationService;

@Controller
public class AccountController {
	@Autowired
	private UserRegistrationService userRegistrationService;

	// 追記部分
	@GetMapping("/")
	public String showTop() {
		return "index";
	}
	
	// 追記部分
	@GetMapping("/login")
	public String showLoginPage() {
		return "login";
	}
	
	@GetMapping("/user/registration")
	public String showUserRegistration(@ModelAttribute("form") UserRegistrationForm form) {
		return "user-registration";
	}
	
	@PostMapping("/user/registration")
	public String userRegistration(@Valid @ModelAttribute("form") UserRegistrationForm form, BindingResult result) {
		if(result.hasErrors()) {
			return "user-registration";
		}
		userRegistrationService.userRegistration(form.getUsername(), form.getPassword());
		return "redirect:/login";
	}
}
```

## 4-2. 認証設定

次に、Configurationクラスにログイン認証処理を追記します。

```java:SecurityConfig.java
package com.example.demo.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
public class SecurityConfig {
	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		// 追記部分
		http.formLogin(login -> login
				.loginPage("/login")
				.loginProcessingUrl("/login")
				.usernameParameter("username")
				.passwordParameter("password")
				.defaultSuccessUrl("/")
				.failureUrl("/login?error")
				.permitAll()
		).authorizeHttpRequests(authz -> authz
				.mvcMatchers("/css/**").permitAll()
				.mvcMatchers("/user/registration").permitAll()
				.anyRequest().authenticated()
		);
        return http.build();
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}
```

`http.login()`でログイン認証の設定を行うことができます。
ここで使用されるメソッドについて、下記にまとめます。

|メソッド名|説明|
|---|---|
|`loginPage()`|ログインページのURLを設定|
|`loginProcessingUrl()`|ユーザ名とパスワードの送信先を設定|
|`usernameParameter()`|ユーザ名のリクエストパラメータ名を設定<br>(デフォルトはusername) |
|`passwordParameter()`|パスワードのリクエストパラメータ名を設定<br>(デフォルトはpassword) |
|`defaultSuccessUrl()`|認証成功後の遷移先を設定|
|`failureUrl()`|認証失敗した場合の遷移先を設定|

今回は、formにおけるユーザ名とパスワードのname属性 (リクエストパラメータ名) はそれぞれusernameとpasswordです。
したがって、デフォルト値なのであえて記述する必要もありませんが、今回は説明のために記述しました。

ここで、`http://localhost:8080/login`にアクセスすると、以下のようにログイン画面が表示されます。
![image4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745690/13f6a4a5-2e3d-651d-7a5f-ecbb78c84cc4.png)

また、ログインに失敗した際、画面にエラーを出力するための処理をformに追記します。

```html:login.html (追記)
<form class="form" th:action="@{/login}" method="post">
    <h1 class="form__title">Login</h1>
    <!-- 追記部分 -->
    <div th:if="${param.error}">
	    <p th:if="${session[SPRING_SECURITY_LAST_EXCEPTION] != null}">
	        <span th:text="${session[SPRING_SECURITY_LAST_EXCEPTION].message}"></span>
	    </p>
	</div>
    <p class="form__item">
    	<label for="username">Username</label>
    	<input type="text" placeholder="Type Your Username" name="username" id="username">
    </p>
    <p class="form__item">
    	<label for="password">Password</label>
    	<input type="password" placeholder="Type Your Password" name="password" id="password">
    </p>
    <p class="form__item">
    	<input type="submit" value="Login">
    </p>
    <a href="/user/registration" class="to-user-registration">User Registration</a>
</form>
```

param.errorは、errorという名前のリクエストパラメータを返します。
先ほどConfigurationクラスで、認証に失敗した際にerrorというリクエストパラメータをURLに含めるように設定したので、これによってparam.errorを持つdivが表示されます。

認証失敗時に発生した例外オブジェクトは、リダイレクトされた際にはセッションに格納されます。
認証が失敗した時はログイン画面にリダイレクトするため、セッションに格納されたSPRING_SECURITY_LAST_EXCEPTION属性のオブジェクトを呼び出し、エラーメッセージを出力しています。

参考サイト1:&emsp;[11.4. Spring Securityチュートリアル](https://macchinetta.github.io/server-guideline-thymeleaf/current/ja/Tutorial/TutorialSecurity.html)
参考サイト2:&emsp;[Tutorial: Using Thymeleaf (ja) (公式)](https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html)

次に、データベースからデータを取得するために、UserDetailsImplクラスとUserDetailsServiceImplクラスを作成します。

UserDetailsImplクラスは、ユーザの情報を提供するためのUserDetailsインタフェースを実装したクラスです。
UserDetailsServiceImplクラスは、ユーザの情報 (UserDetails) を取得するためのUserDetailsServiceインターフェースを実装したクラスです。

Spring Securityでは、このUserDetailsを媒介としてデータベースと情報をやり取りします。

まず、UserDetailsImplクラスを作成します。
```java:UserDetailsImpl.java
package com.example.demo.userDetails;

import java.util.Collection;

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

import com.example.demo.entity.User;

public class UserDetailsImpl implements UserDetails {
	private User user;
	
	public UserDetailsImpl(User user) {
		this.user = user;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRolename());
	}
	
	@Override
	public String getUsername() {
		return user.getUsername();
	}

	@Override
	public String getPassword() {
		return user.getPassword();
	}
	
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

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

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

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

このクラスは、usersテーブルのEntityであるUserクラスをベースに作成します。

UserDetailsインターフェースを実装したクラスでは、上記のコードで示しているように、いくつかのメソッドをオーバーライドする必要があります。
各メソッドについて、下記にまとめます。

|メソッド名|説明|
|---|---|
|`getAuthorities()`|ユーザの権限を返す|
|`getUsername()`|ユーザのユーザ名を返す|
|`getPassword()`|ユーザのパスワードを返す|
|`isAccountNonExpired()`|アカウントが有効期限切れかを判定する|
|`isAccountNonLocked()`|アカウントのロック状態を判定する|
|`isCredentialsNonExpired()`|アカウントの資格情報（パスワード）が有効期限切れかを判定する|
|`isEnabled()`|有効なユーザかを判定する|

今回は実装しませんが、Spring Securityにおける認可処理では「ROLE_」プレフィックスをつけた文字列をロールとして扱います。
したがって、`getAuthorities()`ではrolenameに「ROLE_」をつけた文字列を返しています。

参考サイト:&emsp;[新人社員がSpring Securityで認証・認可機能を一から作ってみた](https://qiita.com/y-yamagata/items/7232b9969fcb7ce85b54#3-5-5-service%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E8%BF%BD%E5%8A%A0)

次に、UserDetailsServiceImplクラスを作成します。
また、UserRepositoryインターフェースにもユーザ情報を取得するメソッドを追加します。

```java:UserDetailsServiceImpl.java
package com.example.demo.userDetailsService;

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.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.userDetails.UserDetailsImpl;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	@Autowired
	private UserRepository userRepository;
    
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
		User user = userRepository.findByUsername(username);
		if(user == null) {
			throw new UsernameNotFoundException("Not Found");
		}
		return new UserDetailsImpl(user);
	}
}
```

```java:UserRepository.java (追記)
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
	// 追記部分
	public User findByUsername(String username);
}
```

ここで重要なのが`loadUserByUsername()`です。

このメソッドは、usernameに一致したユーザ情報をデータベースから検索し、そのユーザをもとに作られたUserDetailsを返します。
もし見つからなかった場合は、UsernameNotFoundExceptionという例外をスローします。

UserDetailsServiceを実装したUserDetailsServiceImplを<span>@</span>Serviceを付けてApplicationContextに登録することで、ログイン認証が行われる際Springが自動的にloadUserByUsernameを呼び出します。

ここで、再度`http://localhost:8080/login`にアクセスし、ログインをします。
ログインには前章で登録したユーザアカウントを利用します。

|パラメータ|値|
|:-:|:-:|
|username|user1|
|password|password|

ログインに成功すると、以下のようにログイン成功と画面に表示されます。

![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2745690/0cc639da-a878-392a-c685-3a12dfa9ad4b.png)

# 5. まとめ

今回、JPAベースのユーザ登録とログイン認証を行いました。
Qiitaに記事を投稿するのは初めてで拙い点も多くあるかと思いますが、Spring Securityを学習している人の力になれたら幸いです。