SpringBoot&PostgreSQLで簡単なユーザ登録APIとログインAPIを実装する

# はじめに
SpringBootとPostgreSQLを組み合わせてDBへユーザ登録を行うユーザ登録APIと、DB参照を行なってログイン判定を行うログインAPIを簡易的に実装します。認証認可はSpringSecurityを使って実装すべきかもしれませんが、今回はSpringBootとPostgreSQLのデータの読み書きが目的なのでご容赦ください。
開発環境構築は[こちら](https://qiita.com/ngnmsn/items/636055bcc018783daa7f)で解説しており、当記事はその続きとなります。

# 開発環境
|項目|バージョン|
| ---- | ---- |
|OS|macOS Ventura 13.1|
|プロセッサ|1.6 GHz デュアルコアIntel Core i5|
|VSCode|1.75.0|
|Java(JDK)|17.0.6|
|SpringBoot|3.0.3|
|Gradle|7.6|
|PostgreSQL|15.1|

# 概念図
SpringBootとPostgreSQLを接続し、APIによりユーザテーブルにユーザ登録を行う機能、およびユーザテーブルから情報を参照しログイン判定を行う機能を実装します。
![SystemDesign.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/639284/c52437e1-c978-809a-1890-7396acdc7338.png)

# インターフェース仕様
各APIのインターフェース仕様は以下の通りです。
## ユーザ登録API
### エンドポイント
```POST```&emsp;/user/regist
### リクエストヘッダー
|項目|設定値|
| ---- | ---- |
|Content-Type|application/json|
### リクエストボディ
|パラメータ|説明|
| ---- | ---- |
|userName|ユーザ名|
|password|パスワード|
|mailAddress|メールアドレス|
### レスポンスボディ
|パラメータ|説明|
| ---- | ---- |
|userId|ユーザID|
|userName|ユーザ名|
|mailAddress|メールアドレス|

## ログインAPI
### エンドポイント
```POST```&emsp;/user/login
### リクエストヘッダー
|項目|設定値|
| ---- | ---- |
|Content-Type|application/json|
### リクエストボディ
|パラメータ|説明|
| ---- | ---- |
|mailAddress|メールアドレス|
|password|パスワード|
### レスポンスボディ
|パラメータ|説明|
| ---- | ---- |
|status|ステータス|
|userId|ユーザID|
|userName|ユーザ名|
|mailAddress|メールアドレス|

# テーブル設計
テーブル設計は以下の通りです。
## ユーザテーブル（テーブル物理名：user_table）
|項目|カラム名|型|PK|説明|
| ---- | ---- | ---- | ---- | ---- |
|ユーザID|user_id|integer|○|シーケンシャルなユーザIDを格納する。|
|ユーザ名|user_name|character varying(256)||ユーザの登録したユーザ名を格納する。|
|パスワード|password|character varying(256)||ユーザの登録したパスワードを格納する。|
|メールアドレス|mail_address|character varying(256)||ユーザの登録したメールアドレスを格納する。|

# 実装手順
## テーブルの作成
サンプルプロジェクト用のデータベースの作成方法はこちらの記事で解説しておりますのでこちら記事を参考にしてください。
[サンプルプロジェクト用のデータベースの作成](https://qiita.com/ngnmsn/items/a8c52460739051d60760#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E7%94%A8%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90)
今回はこちらのサンプルプロジェクト用のデータベースにテーブルを作成します。
### CREATE用SQLファイルの作成
CREATE用のSQLファイルを作成します。
```sql:create_table_user.sql
CREATE TABLE user_table (
  user_id serial PRIMARY KEY,
  user_name varchar(256),
  password varchar(256),
  mail_address varchar(256) UNIQUE
);
```
ファイルの作成場所はどこでもいいですが、筆者はSpringBootのサンプルプロジェクトの以下に格納しております。
[PostgreSQL/Creat/create_table_user_table.sql](https://github.com/ngnmsn/SpringBootSampleProject/blob/main/PostgreSQL/Create/create_table_user_table.sql)
### データベースへログインする
#### CREATE用SQLを作成したディレクトリにcdする
ターミナルで各々作成したディレクトリにcdしてください。
```
$ cd PostgreSQL/Create/
```
### psqlコマンド実行
ターミナルで以下のコマンドを実行し、サンプルプロジェクト用データベースにアクセスしてください。
```
$ psql -h localhost -p 5432 -U postgres samplepjdb
psql (15.1)
Type "help" for help.

samplepjdb=#
```
### CREATE用SQLファイルを実行する。
以下のコマンドを実行し、CREATE用SQLファイルを実行します。
```\i create_table_user_table.sql```
```
samplepjdb=# \i create_table_user_table.sql
CREATE TABLE
```
### テーブル情報の確認
以下のコマンドを実行し、作成したテーブルの情報を確認します。
```\d user```
```
samplepjdb=# \d user_table
                                         Table "public.user_table"
    Column    |          Type          | Collation | Nullable |                   Default
--------------+------------------------+-----------+----------+---------------------------------------------
 user_id      | integer                |           | not null | nextval('user_table_user_id_seq'::regclass)
 user_name    | character varying(256) |           |          |
 password     | character varying(256) |           |          |
 mail_address | character varying(256) |           |          |
Indexes:
    "user_table_pkey" PRIMARY KEY, btree (user_id)
    "user_table_mail_address_key" UNIQUE CONSTRAINT, btree (mail_address)
```
## ユーザ登録APIの実装
### コントローラクラスの実装
以下のユーザ関連処理のコントローラクラスを実装します。APIのリクエストを扱うクラスとなります。
```java:src/main/java/com/example/samplepj/controller/user/UserController.java
package com.example.samplepj.controller.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.samplepj.domain.model.user.RequestUserRegist;
import com.example.samplepj.domain.model.user.ResponseUserRegist;
import com.example.samplepj.domain.service.user.UserService;

/**
 * UserControllerクラス
 * ユーザ関連のAPI
 */
@RestController
@RequestMapping("/user")
public class UserController {

  // サービスクラスの依存性注入
  @Autowired
  UserService userService;

  /**
   * ユーザ登録API 
   * POST /user/regist
   * @param requestUserRegist ユーザ登録APIのリクエストボディ
   * @return responseUserRegist ユーザ登録APIのレスポンスボディ
   */
  @PostMapping("regist")
  public ResponseUserRegist userRegist(@RequestBody RequestUserRegist requestUserRegist) {

      // サービスクラスのユーザ登録処理呼び出し
      ResponseUserRegist responseUserRegist = userService.insertUser(requestUserRegist);

      // APIレスポンス
      return responseUserRegist;
  }
}
```
### モデルクラスの実装
次に処理の中で使用するモデルクラスを実装します。ユーザ登録APIの実装するモデルクラスは以下の3つです。
- Userクラス(User.java)：ユーザテーブルのデータモデルを格納するクラス
- RequestUserRegistクラス(RequestUserRegist.java)：ユーザ登録APIのリクエスト内容を格納するクラス
- ResponseUserRegistクラス(ResponseUserRegist.java)：ユーザ登録APIのレスポンス内容を格納するクラス

それぞれの実装は以下の通りです。
```java:src/main/java/com/example/samplepj/domain/model/user/User.java
package com.example.samplepj.domain.model.user;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

@Entity
@Table(name = "user_table")
@Data
public class User {
  // ユーザID
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private String userId;
  // ユーザ名
  private String userName;
  // パスワード
  private String password;
  // メールアドレス
  private String mailAddress;
}
```
```java:src/main/java/com/example/samplepj/domain/model/user/RequestUserRegist.java
package com.example.samplepj.domain.model.user;

import lombok.Data;

@Data
public class RequestUserRegist {
  // ユーザ名
  private String userName;
  // パスワード
  private String password;
  // メールアドレス
  private String mailAddress;
}
```
```java:src/main/java/com/example/samplepj/domain/model/user/ResponseUserRegist.java
package com.example.samplepj.domain.model.user;

import lombok.Data;

@Data
public class ResponseUserRegist {
  // ユーザID
  private String userId;
  // ユーザ名
  private String userName;
  // メールアドレス
  private String mailAddress;
}
```
User.javaの
```java
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
```
自動採番が可能なSpring JPAのアノテーションです。
```GenerationType.SEQUENCE```の設定でシーケンスオブジェクトを使用して主キー値を生成します。

### サービスクラスの実装
ユーザ関連処理のサービスクラスを実装します。

```java:src/main/java/com/example/samplepj/domain/service/user/UserService.java
package com.example.samplepj.domain.service.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;


import com.example.samplepj.domain.model.user.User;
import com.example.samplepj.domain.model.user.RequestUserRegist;
import com.example.samplepj.domain.model.user.ResponseUserRegist;
import com.example.samplepj.domain.repository.UserRepository;
import com.example.samplepj.util.user.PasswordUtil;

@Service
@Transactional
public class UserService {
  
  // リポジトリクラスの依存性注入
  @Autowired
  UserRepository userRepository;
  
  /**
   * ユーザ登録する情報のDBインサート処理
   * @param RequestUserRegist ユーザ登録APIのリクエストボディ
   * @return responseUserRegist ユーザ登録APIのレスポンスボディ
   */
  public ResponseUserRegist insertUser(RequestUserRegist requestUserRegist) {
    User user = new User();
    user = CreateUser(requestUserRegist);
    userRepository.save(user);
    ResponseUserRegist responseUserRegist = new ResponseUserRegist();
    responseUserRegist.setUserId(user.getUserId());
    responseUserRegist.setUserName(user.getUserName());
    responseUserRegist.setMailAddress(user.getMailAddress());
    return responseUserRegist;
  };

  /**
   * ユーザ登録するユーザ情報の作成処理
   * @param RequestUserRegist ユーザ登録APIのリクエストボディ
   * @return user ユーザ情報
   */
  private User CreateUser(RequestUserRegist requestUserRegist) {
    String hashPw;
    User user = new User();
    hashPw = PasswordUtil.hashSHA256(requestUserRegist.getPassword());
    user.setUserName(requestUserRegist.getUserName());
    user.setPassword(hashPw);
    user.setMailAddress(requestUserRegist.getMailAddress());
    
    return user;
  };
}
```

### リポジトリクラスの実装
データベースへのアクセスを行うリポジトリクラスを実装します。
```java:src/main/java/com/example/samplepj/domain/repository/user/UserRepository.java
package com.example.samplepj.domain.repository.user;

import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.samplepj.domain.model.user.User;

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

### パスワード用Utilクラスの実装
パスワードはハッシュ化してDBに格納するため、ハッシュ化用の処理をUtilクラスとして切り出して実装します。
```java:src/main/java/com/example/samplepj/util/user/PasswordUtil.java
package com.example.samplepj.util.user;

import java.security.MessageDigest;
import java.math.BigInteger;

public class PasswordUtil {

  /**
   * パスワードのハッシュ化処理
   * @param password ユーザ入力のパスワード
   * @return hashPw SHA256でハッシュ化されたパスワードp
   */
  public static String hashSHA256(String password) {
    byte[] byteHashPw;
    String hashPw = "";
    try {
        MessageDigest md = MessageDigest.getInstance("sha-256");
        md.update(password.getBytes("utf8"));
        byteHashPw = md.digest();
        hashPw = String.format("%064x", new BigInteger(1, byteHashPw));
        return hashPw;
    } catch(Exception e) {
        e.printStackTrace();
    }
    return hashPw;
  }
}
```

### ユーザ登録の動作確認
#### SpringBootプロジェクトの実行
プロジェクト実行方法は以下で紹介しています。
[プロジェクトの実行](https://qiita.com/ngnmsn/items/a8c52460739051d60760#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%AE%9F%E8%A1%8C)
#### 動作確認用curlコマンドの実行
ターミナルで以下のコマンドを実行し、ユーザ登録APIのリクエストを行います。
```
$ curl -v -X POST -H "Content-Type: application/json" http://localhost:8080/user/regist -d '{ "userName": "user1", "password": "password", "mailAddress": "user1@example.com" }'
```
ステータス200でレスポンスが返ってくれば成功です。
```
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /user/regist HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 83
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sun, 26 Mar 2023 11:00:11 GMT
<
* Connection #0 to host localhost left intact
{"userId":"2","userName":"user1","mailAddress":"user1@example.com"}
```

#### ユーザテーブルの確認
ターミナルで以下のコマンドを実行し、ユーザテーブルをSELECTしてユーザ情報がテーブルにINSERTされていることを確認します。
```
$ psql -h localhost -p 5432 -U postgres samplepjdb
samplepjdb=# select * from user_table;
 user_id | user_name |                             password                             |   mail_address
---------+-----------+------------------------------------------------------------------+-------------------
       2 | user1     | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 | user1@example.com
(1 row)
```
データが一件SELECTできればユーザ登録は成功です。

## ログインAPIの実装
### コントローラクラスの実装
ユーザ登録APIの実装の際に作成したコントローラクラスに以下の修正を行います。

```diff_java:src/main/java/com/example/samplepj/controller/user/UserController.java
package com.example.samplepj.controller.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.example.samplepj.domain.model.user.RequestUserRegist;
import com.example.samplepj.domain.model.user.ResponseUserRegist;
+ import com.example.samplepj.domain.model.user.RequestLogin;
+ import com.example.samplepj.domain.model.user.ResponseLogin;
import com.example.samplepj.domain.service.user.UserService;

/**
 * UserControllerクラス
 * ユーザ関連のAPI
 */
@RestController
@RequestMapping("/user")
public class UserController {

  // サービスクラスの依存性注入
  @Autowired
  UserService userService;

  /**
   * ユーザ登録API 
   * POST /user/regist
   * @param requestUserRegist ユーザ登録APIのリクエストボディ
   * @return responseUserRegist ユーザ登録APIのレスポンスボディ
   */
  @PostMapping("regist")
  public ResponseUserRegist userRegist(@RequestBody RequestUserRegist requestUserRegist) {

      // サービスクラスのユーザ登録処理呼び出し
      ResponseUserRegist responseUserRegist = userService.insertUser(requestUserRegist);

      // APIレスポンス
      return responseUserRegist;
  }
+
+  /**
+   * ログインAPI 
+   * POST /user/login
+   * @param requestLogin ログインAPIのリクエストボディ
+   * @return responseLogin ログインAPIのレスポンスボディ
+   */
+  @PostMapping("login")
+  public ResponseLogin login(@RequestBody RequestLogin requestLogin) {
+
+    // サービスクラスのログイン処理呼び出し
+    ResponseLogin responseLogin = userService.login(requestLogin);
+
+    // APIレスポンス
+    return responseLogin;
+  }
}
```

### モデルクラスの実装
ログインAPIでは以下の2つのモデルクラスを追加します。
- RequestLoginクラス(RequestLogin.java)：ログインAPIのリクエスト内容を格納するクラス
- ResponseLoginクラス(ResponseLogin.java)：ログインAPIのレスポンス内容を格納するクラス

それぞれの実装は以下の通りです。
```java:src/main/java/com/example/samplepj/domain/model/user/RequestLogin.java
package com.example.samplepj.domain.model.user;

import lombok.Data;

@Data
public class RequestLogin {
  // メールアドレス
  private String mailAddress;
  // パスワード
  private String password;
}
```
```java:src/main/java/com/example/samplepj/domain/model/user/ResponseLogin.java
package com.example.samplepj.domain.model.user;

import lombok.Data;

@Data
public class ResponseLogin {
  // ステータス
  private String status;
  // ユーザID
  private String userId;
  // ユーザ名
  private String userName;
  // メールアドレス
  private String mailAddress;
}
```

### サービスクラスの実装
ユーザ登録APIで実装したユーザ関連のサービスクラスに以下の実装を追加します。

```diff_java:src/main/java/com/example/samplepj/domain/service/user/UserService.java
package com.example.samplepj.domain.service.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;


import com.example.samplepj.domain.model.user.User;
import com.example.samplepj.domain.model.user.RequestUserRegist;
import com.example.samplepj.domain.model.user.ResponseUserRegist;
+ import com.example.samplepj.domain.model.user.RequestLogin;
+ import com.example.samplepj.domain.model.user.ResponseLogin;
import com.example.samplepj.domain.repository.UserRepository;
import com.example.samplepj.util.user.PasswordUtil;
+ import java.util.List;
+ import java.util.ArrayList;

@Service
@Transactional
public class UserService {
  
  // リポジトリクラスの依存性注入
  @Autowired
  UserRepository userRepository;
  
  /**
   * ユーザ登録する情報のDBインサート処理
   * @param RequestUserRegist ユーザ登録APIのリクエストボディ
   * @return responseUserRegist ユーザ登録APIのレスポンスボディ
   */
  public ResponseUserRegist insertUser(RequestUserRegist requestUserRegist) {
    User user = new User();
    user = CreateUser(requestUserRegist);
    userRepository.save(user);
    ResponseUserRegist responseUserRegist = new ResponseUserRegist();
    responseUserRegist.setUserId(user.getUserId());
    responseUserRegist.setUserName(user.getUserName());
    responseUserRegist.setMailAddress(user.getMailAddress());
    return responseUserRegist;
  };

  /**
   * ユーザ登録するユーザ情報の作成処理
   * @param RequestUserRegist ユーザ登録APIのリクエストボディ
   * @return user ユーザ情報
   */
  private User CreateUser(RequestUserRegist requestUserRegist) {
    String hashPw;
    User user = new User();
    hashPw = PasswordUtil.hashSHA256(requestUserRegist.getPassword());
    user.setUserName(requestUserRegist.getUserName());
    user.setPassword(hashPw);
    user.setMailAddress(requestUserRegist.getMailAddress());
    
    return user;
  };
+
+   /**
+    * ログインするユーザ情報の作成処理
+    * @param RequestLogin ログインAPIのリクエストボディ
+    * @return responseLogin ログインAPIのレスポンスボディ
+    */
+   public ResponseLogin login(RequestLogin requestLogin) {
+     User loginUser = new User();
+     loginUser = CreateUser(requestLogin);
+     List<User> userList = new ArrayList<User>();
+     userList = userRepository.findByMailAddress(loginUser.getMailAddress());
+
+     ResponseLogin responseLogin = new ResponseLogin();
+     if ( userList.size() == 0) {
+       responseLogin.setStatus("error");
+     } else if ( ! (loginUser.getPassword().equals(userList.get(0).getPassword() ))) {
+       responseLogin.setStatus("error");
+     } else {
+       responseLogin.setStatus("success");
+       responseLogin.setUserId(userList.get(0).getUserId());
+       responseLogin.setUserName(userList.get(0).getUserName());
+       responseLogin.setMailAddress(userList.get(0).getMailAddress());
+     }
+     return responseLogin;
+   };
+
+   /**
+    * ログインするユーザ情報の作成処理
+    * @param RequestLogin ログインAPIのリクエストボディ
+    * @return user ユーザ情報
+    */
+   private User CreateUser(RequestLogin requestLogin) {
+     String hashPw;
+     User user = new User();
+     hashPw = PasswordUtil.hashSHA256(requestLogin.getPassword());
+     user.setPassword(hashPw);
+     user.setMailAddress(requestLogin.getMailAddress());
+
+     return user;
+   };
}
```


### リポジトリクラスの実装
ログインAPIの処理にはメールアドレスをキーにしたテーブル参照があるので、リポジトリクラスに以下のを追記します。

```diff_java:src/main/java/com/example/samplepj/domain/repository/user/UserRepository.java
package com.example.samplepj.domain.repository.user;

import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.samplepj.domain.model.user.User;
+ import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
+  List<User> findByMailAddress(String mailAddress);
}
```
SpringBootのフレームワークにより"findBy〜(検索キー)"というメソッドをinterfaceクラスに追加するだけで、その検索キーでのDB参照メソッドを実装することができます。

### ログインの動作確認
#### SpringBootプロジェクトの実行
ユーザ登録APIと同様

#### 動作確認用curlコマンドの実行
ターミナルで以下のコマンドを実行し、ログインAPIのリクエストを行います。
```
$ curl -v -X POST -H "Content-Type: application/json" http://localhost:8080/user/login -d '{ "mailAddress": "user1@example.com", "password": "password" }'
```
ステータス200でレスポンスが返ってくれば成功です。
```
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /user/login HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 62
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Mon, 27 Mar 2023 12:41:41 GMT
<
* Connection #0 to host localhost left intact
{"status":"success","userId":"2","userName":"user1","mailAddress":"user1@example.com"}
```

以上がSpringBoot&PostgreSQLによるユーザ登録APIとログインAPIに実装になります。

# サンプルリポジトリ
上記のコードはGitHubに公開しています。
[SpringBootSampleProject](https://github.com/ngnmsn/SpringBootSampleProject)

# 参考
[Spring JPAでテーブル毎に自動採番のIDを作成する方法](https://intellectual-curiosity.tokyo/2021/06/08/spring-jpa%E3%81%A7%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E6%AF%8E%E3%81%AB%E8%87%AA%E5%8B%95%E6%8E%A1%E7%95%AA%E3%81%AEid%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/)