Spring Securityを使ったWebアプリケーションの基本③　〜ユーザー情報をデータベースで管理する実装〜


<h2>はじめに</h2>
<a href="https://qiita.com/homes31285/items/ecec7f8e288a40df9051">前回の記事</a>では、ログイン機能とログアウト機能を実装しました。今回は、ユーザー情報をデータベースに登録し、ユーザー情報テーブルから、ログインできるように実装をしていきたいと思います。

<details>
<summary>ファイル構成</summary>
<pre>
<code>
.
├── .gradle
├── .idea
├── build
├── gradle
└── src
     ├── main
     |    ├── java
     |    |    └── com
     |    |         └── example
     |    |              └── practice 
     |    |                  ├── config
     |    |                  |    └── SecurityConfig.java
     |    |                  ├── web 
     |    |                  |    ├── order
     |    |                  |    |    ├── OrderForm.java
     |    |                  |    |    └── OrderController.java
     |    |                  |    └── IndexController.java
     |    |                  └── domain
     |    |                      ├── authentication
     |    |                      |    ├── CustomUserDetails.java
     |    |                      |    ├── CustomUserDetailsService.java
     |    |                      |    ├── User.java
     |    |                      |    └── UserRepotitory
     |    |                      └── order 
     |    |                           ├── OrderEntity.java
     |    |                           ├── OrderService.java
     |    |                           └── OrderRepository.java
     |    |                        
     |    └── resources
     |         ├── static
     |         ├── templates
     |         |    ├── order
     |         |    |    ├── delete_confirmation.html
     |         |    |    ├── detail.html 
     |         |    |    ├── form.html 
     |         |    |    └── list.html
     |         |    ├── login.html
     |         |    └── index.html
     |         ├── schema.sql
     |         ├── data.sql 
     |         └── application.properties
     └── test
.gitignore
build.gradle
gradlew.bat
HELP.md
settings.gradle
</code>
</pre>
</details>

<h2>データベースのテーブルの作成</h2>
まずは、ユーザー情報を格納するUSERSテーブルを作成します。

以下の内容をもとにTABLEを作成しましょう。

<b>ユーザー情報テーブル</b>
| カラム名 | データ型 | NULL許容 | デフォルト値 | 主キー | 説明 |
|:-:|:-:|:-:|:-:|:-:|:-:|
|  username | VARCHAR(256)  |  NOT NULL |   |  ✔ | ユーザー名  |
|  password |  VARCHAR(256) |  NOT NULL |   |   |   |


```sql:schema.sql
--ユーザー情報テーブル
CREATE TABLE USERS (
username varchar(256) not null primary key,
password varchar(256) not null
);
```

<h2>データベース初期データの追加</h2>

次に、ユーザー情報をデータベースに初期データとして追加をします。
初期データは任意ですので、適当な値を追加しましょう。

```sql:data.sql
-- 初期データを追加
INSERT INTO USERS (username, password) values ('taro', 'password'); 
INSERT INTO USERS (username, password) values ('yamada', 'password'); 
```

<h2>Userクラス</h2>
次に、ユーザー情報を表すためのデータモデルとして使用するために、ユーザー名とパスワードを格納するためのプロパティを作成します。

```java:User
package com.example.practice.domain.authentication;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private String username;
    private String password;
}
```

<h2>UserRepositoryクラス</h2>
次に、MyBatisアノテーションを使用してデータベースからユーザー情報を取得するためのクエリを定義します。

簡単に説明をすると、引数としてユーザー名を受け取り、該当するユーザー情報をOptional<User>型で返します。Optionalは、値が存在するかどうかを表すラッパークラスで、値が存在しない場合にはnullではなくOptional.empty()が返されます。

findByUsernameメソッドを呼び出すことで、指定されたユーザー名に一致するユーザー情報を取得することができます。

```java:UserRepository
package com.example.practice.domain.authentication;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Optional;

@Mapper
public interface UserRepository {
        @Select("select * from users where username = #{username}")
        Optional<User> findByUsername(String username);
}
```

<h2>CustomUserDetailsクラス</h2>
次に、Spring Securityを使用して認証や認可を行う際に利用されるカスタムなユーザー詳細情報クラスを定義します。
CustomUserDetailsクラスは、Userクラスを継承しユーザー名、パスワード、権限情報（GrantedAuthorityのコレクション）を受け取ります。


```java:CustomUserDetails
package com.example.practice.domain.authentication;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;  // Spring SecurityのUserクラスをインポート
import java.util.Collection;

public class CustomUserDetails extends User {  // CustomUserDetailsクラスがUserクラスを拡張
    public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);  // 親クラスのコンストラクタを呼び出してインスタンスを初期化
    }
}

```

<h2>CustomUserDetailsService</h2>
CustomUserDetailsServiceは、Spring SecurityのUserDetailsServiceインターフェースを実装したクラスで、ユーザー名を受け取りデータベースからユーザー情報を取得する役割を担います。

以下のコードが詳細になります。

```java:CustomUserDetailsService
package com.example.practice.domain.authentication;

import lombok.RequiredArgsConstructor;  // lombokのRequiredArgsConstructorをインポート
import org.springframework.security.core.userdetails.UserDetails;  // Spring SecurityのUserDetailsをインポート
import org.springframework.security.core.userdetails.UserDetailsService;  // Spring SecurityのUserDetailsServiceをインポート
import org.springframework.security.core.userdetails.UsernameNotFoundException;  // ユーザーが見つからなかった場合の例外をインポート
import org.springframework.stereotype.Service;  // SpringのServiceアノテーションをインポート

import java.util.Collections;  // JavaのCollectionsクラスをインポート

@Service  // サービスクラスであることを示す@Serviceアノテーション
@RequiredArgsConstructor  // コンストラクタインジェクションのためのアノテーション
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;  // UserRepositoryの依存を注入

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username)  // ユーザー名でユーザー情報を検索
                .map(  // Optional型からカスタムなユーザー情報を作成
                        user -> new CustomUserDetails(
                                user.getUsername(),
                                user.getPassword(),
                                Collections.emptyList()
                        )
                )
                .orElseThrow(  // ユーザーが見つからない場合は例外をスロー
                        () -> new UsernameNotFoundException(
                                "ユーザー情報の認証に失敗しました。 (username = '" + username + "')"
                        )
                );
    }
}
```


<h2>SecurityConfig</h2>
次に、SecurityConfigの実装をしていきたいと思います。
SecurityConfigは、Webアプリケーションのセキュリティ設定を行うためのクラスです。
また、Webアプリケーションのアクセス権限やログイン機能を制御するためのものであり、セキュリティを強化してアプリケーションの安全性を向上させる役割を持ちます。


```java:SecurityConfig
package com.example.practice.config;

import lombok.RequiredArgsConstructor;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;

// Spring SecurityのWebセキュリティを有効化
@EnableWebSecurity
// コンストラクタの引数からフィールドを生成
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // UserDetailsServiceの実装を保持するフィールド
    private final UserDetailsService userDetailsService;

    // HTTPリクエストに対するセキュリティ設定を行うメソッド
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // リクエストごとのアクセス権限を設定
                .authorizeRequests()
                // 特定のパスは全員アクセス可能
                .mvcMatchers("/login/**").permitAll()
                // 他のすべてのリクエストは認証が必要
                .anyRequest().authenticated()
                .and()
                // フォームベースのログインを有効に
                .formLogin()
                // ログインページのパスを設定
                .loginPage("/login");
    }
    
    // ユーザー認証の設定を行うメソッド
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // UserDetailsServiceを設定し、ユーザー認証情報を提供
        auth.userDetailsService(userDetailsService)
                // パスワードエンコードを行わない設定
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}
```

<h2>まとめに</h2>
ユーザー情報をデータベースに登録し、ログイン機能を実装する手順を解説しました。この実装を通じて、Spring Securityを使用してセキュリティを強化する方法を学びました。