【SE入門編】Spring Tool Suite 4を使ってWEBプロジェクトのログイン画面及びセッション管理機能を実装する。

## 0. 前書き
今回の記事は「SE入門編」シリーズの一部です。完全版はこちらにあります：

https://qiita.com/manabu-s/items/2f7f10752bcf44244718

当該バージョンのソースはGithubにアップロードしております：

https://github.com/yoruzuki/WebService_Spring/tree/main/demo%20ver0.3

## 1. 前提条件
- 前提条件として、DBおよびWebプロジェクトの構築が完了していることが前提です。もし構築がまだ完了していない場合は、以下の記事を参照してください。

- 今回の実装では、ログイン画面及びセッション管理のみに焦点を当てます。実装に際しては、SpringやThymeleafの基本実装が理解されていることが前提です。

- 実装にはjQueryとBootstrapを使用しますので、これらを使いこなすスキルが必要です。

### 【参考記事】
- DBの構築手順：

https://qiita.com/manabu-s/items/df29732448669b5429eb

- WEBプロジェクトの構築手順：

https://qiita.com/manabu-s/items/c9f1dc4d0d821861611b

- CRUDの基本実装：

https://qiita.com/manabu-s/items/9626457bb304109af17b

- このシリーズの完全版記事：

https://qiita.com/manabu-s/items/2f7f10752bcf44244718

## ２. 今回の記事で出来たこと

- セッション管理。

- ログイン画面の実装。

- ログアウトの実装。

- 画面説明：

ログイン初期画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/6dabe801-9c34-44e8-30ad-815e1d03df78.png)

ユーザーIDやパスワードが未入力時、ログインボタンを押すときの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/0375627a-800c-9f7a-67e0-339d233cbdc6.png)

ユーザーIDやパスワードが正しくないときの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/637dbfac-8033-b3a8-e108-d1ac15e3ce8e.png)

セッションタイムアウトが発生時の画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/fbe9246f-d89a-1c7c-f5b9-3f220710b9d8.png)

ログイン成功時の画面（ユーザーID＝ADMINでログインする時）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/2dfd764b-bdb8-c267-6a13-30a48308b5a3.png)

ログイン成功時の画面（ユーザーID＝DEMO001でログインする時）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/72125e6f-c052-c011-9201-953dc1413e59.png)

ログアウトの画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/f09faf9b-8648-83e2-80d1-2029d005ebd5.png)

## ３. 具体的な設定やソースの解説

- 追加＆修正したソースファイル：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/3ade039b-10c8-bccd-9af2-ecaced0f5384.png)

<table>
<tr><td>分類（目的別）</td><td>ファイル名</td><td>修正区分</td><td>説明</td></tr>
<tr><td>設定ファイル</td><td>application.properties</td><td>修正</td><td>セッション管理の設定を追加。</td></tr>
<tr><td>設定ファイル</td><td>pom.xml</td><td>修正</td><td>セッション管理の設定を追加ル。</td></tr>
<tr><td>全体フィルター</td><td>SessionFilter.java</td><td>追加</td><td>全てのリクエストはSessionFilter.javaを通して、セッションタイムアウト時の画面遷移などを実装。</td></tr>
<tr><td>ログイン画面</td><td>LoginController.java</td><td>追加</td><td>ログイン＆ログアウト＆タイムアウトのアクションを実装。</td></tr>
<tr><td>ログイン画面</td><td>LoginForm.java</td><td>追加</td><td>Login画面の項目（ユーザーID、パスワード）を保持するフォーム。</td></tr>
<tr><td>ログイン画面</td><td>login.html</td><td>追加</td><td>ログイン画面</td></tr>
<tr><td>ログイン画面</td><td>timeout.html</td><td>追加</td><td>タイムアウト画面。役目はタイムアウトが発生時ログイン画面に遷移する。</td></tr>
<tr><td>ログアウトメニュー関係</td><td>04.MENU.sql</td><td>修正</td><td>ログアウトのメニューを追加、ユーザー管理のアクションを修正。</td></tr>
<tr><td>ログアウトメニュー関係</td><td>05.MENU_CATEGORY.sql</td><td>修正</td><td>カテゴリーに一番上の親カテゴリー（#）を追加。</td></tr>
<tr><td>ログアウトメニュー関係</td><td>06.ROLE_MENU.sql</td><td>修正</td><td>各ロールにログアウトメニューの権限を追加。</td></tr>
<tr><td>セッション関係修正（メイン画面）</td><td>MainController.java</td><td>修正</td><td>セッション管理が出来たため、パラメータからuserIdの取得が不要になりました。</td></tr>
<tr><td>セッション関係修正（メイン画面）</td><td>main.html</td><td>修正</td><td>セッション管理が出来たため、パラメータからuserIdの取得をセッションから取得に変更しました。</td></tr>
<tr><td>セッション関係修正（メニュー画面）</td><td>MenuController.java</td><td>修正</td><td>セッション管理が出来たため、パラメータからuserIdの取得をセッションから取得に変更しました。</td></tr>
<tr><td>セッション関係修正（メニュー画面）</td><td>menu.html</td><td>修正</td><td>ログアウトのアクション処理を追加。</td></tr>
<tr><td>ユーザーテーブル更新</td><td>UserRepository.java</td><td>修正</td><td>ログイン成功時、ユーザーテーブルの最終ログイン日時を更新する。</td></tr>
<tr><td>ユーザーテーブル更新</td><td>UserService.java</td><td>修正</td><td>ログイン成功時、ユーザーテーブルの最終ログイン日時を更新する。</td></tr>
<tr><td>ユーザーテーブル更新</td><td>UserRepository.xml</td><td>修正</td><td>ログイン成功時、ユーザーテーブルの最終ログイン日時を更新する。</td></tr>
<tr><td>その他</td><td>error.html</td><td>修正</td><td>今回の記事とは関係ないが、詳細メッセージがない時、詳細ボタンを表示しないように修正。</td></tr>
</table>

### ３.１ セッション管理
セッション管理はHTTPSessionだけでも充分だが、今回は独断でSpring Sessionを採用し、セッション情報をRDBMSのOracleに保持し、見えるようにします。

Spring Sessionはいろんなデータストア（Redis、RDBMS、Hazelcastなど）にセッション情報を保存できる。
- Hazelcast（ヘイゼルキャスト）は、オープンソースの分散型インメモリデータグリッド（In-Memory Data Grid）および分散コンピューティングプラットフォームです。クラウド、オンプレミス、またはハイブリッド環境で使用できるように設計されています。
- Redis（リディス）は、高性能なキー-バリューストアとして知られるオープンソースのインメモリデータベースです。
- Redisはインストールが必要なので、今回は既に採用しているRDBMSのOracleに保存します。

#### セッション管理の設定(JDBC)
- データベースの準備：公式githubから、セッション情報保持用のテーブルを作成スクリプトをダウンロードして、テーブルを作成します。

https://github.com/spring-projects/spring-session/blob/main/spring-session-jdbc/src/main/resources/org/springframework/session/jdbc/schema-oracle.sql

テーブル作成成功：SPRING_SESSIONとSPRING_SESSION_ATTRIBUTESが作成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/789cd2d8-5da3-012e-c1f0-2613164a043b.png)

- application.propertiesの設定を追加
ストアタイプ＝JDBC
セッション情報保持テーブル名＝SPRING_SESSION
セッションタイムアウト時間＝1分、タイムアウトのテストをできるように、１分を設定しております、実際のプロジェクトは需要に応じて設定してください。

```
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=embedded
spring.session.jdbc.table-name=SPRING_SESSION
server.servlet.session.timeout=1m
```
- pom.xmlを編集
セッション管理の依存関係を追加する。
```
		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session-jdbc</artifactId>
		</dependency>
```
- 設定結果を確認：上記の設定だけで、セッション管理ができるようになっております。以下のURLを叩いて、セッション管理テーブルにレコードが作成されることを確認できます。
http://localhost:8000/main

このようなデータが表示されたら設定成功です（IDはランダム）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/f3c1b506-20e5-f1d4-f4ae-2c3a980cf258.png)

#### セッションタイムアウトの実装

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1442793/6df1d722-5664-8811-581d-6a3230141733.png)
SessionFilter.java：
- 全てのリクエストをSessionFilter.javaを通すように、インタフェースFilterを実装する。
```
public class SessionFilter implements Filter
```

- セッションタイムアウトが発生する時、タイムアウト画面へ遷移する。
- CSSやJSなど、static配下のリソースはリクエストの判定対象外にする。現時点ではJqueryやBootstrapなどのCSSとJSしか使われていないが、今後これ以外のリソースを使う時、isTargetに追加実装が必要です。
```
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;

		if (isTarget(request) && isSessionTimeout(httpRequest)) { // セッションタイムアウト
			String requestUri = httpRequest.getRequestURI();
			// ログイン・ログアウト・タイムアウトページのタイムアウトは行わない。
			if (requestUri != null && !requestUri.endsWith("/login") && !requestUri.endsWith("/logout")
					&& !requestUri.endsWith("/timeout")) {
				// 通常画面の場合は、ログイン画面を表示しセッションタイムアウトメッセージを伝える。
				httpResponse.sendRedirect(httpRequest.getContextPath() + "/timeout");
			} else {
				chain.doFilter(request, response);
			}
		} else {
			chain.doFilter(request, response);
		}
	}
```

- セッションタイムアウト判定：セッション情報がない時や、セッション情報からuserID取得できないときはタイムアウトと判定する。
```
	/**
	 * セッションタイムアウト判定
	 *
	 * @param request
	 * @return
	 */
	private boolean isSessionTimeout(HttpServletRequest request) {
		HttpSession httpSession = request.getSession(false);
		if (httpSession == null) {
			return true;
		}
		if (httpSession.getAttribute("userID") == null) {
			return true;
		}
		String sessionId = request.getRequestedSessionId();
		boolean isValid = request.isRequestedSessionIdValid();
		return httpSession == null || sessionId == null || !isValid || !sessionId.equals(httpSession.getId());
	}

	/**
	 * 対象のリクエストか判定 css, js等のリソースアクセスについては出力対象外 uri.indexOf("/static") < 0 ：
	 * staticフォルダにすべてのリソースが出力対象外 !uri.endsWith(".css") ： staticフォルダ以外の.cssファイルが出力対象外
	 * !uri.endsWith(".js") ： staticフォルダ以外の.jsファイルが出力対象外
	 *
	 * @param request リクエスト
	 * @return true : ロギング対象 / false : ロギング対象ではない
	 */
	private boolean isTarget(ServletRequest request) {
		String uri = ((HttpServletRequest) request).getRequestURI().toLowerCase();
		return (uri.indexOf("/static") < 0 && !uri.endsWith(".css") && !uri.endsWith(".js"));
	}
```

#### ログイン画面（login.html）の実装

errorLogin：エラーメッセージを受け取るJS
```
var errorLogin = /*[[${errorLogin}]]*/;
```

エラーメッセージを表示するエリア
```
<div th:if="${errorLogin}" th:text="*{errorLogin}" class="alert alert-danger" role="alert"></div>
```

フォームエリア：
- アクションは”/login”
- ユーザーID、パスワードは必須入力項目（required="required" ）
- ログインボタン：押下時入力されたユーザーIDとパスワードを送信する。
- ほか、Tyeamleafや、Bootstrapの説明は省きます。
```
      <form th:action="@{/login}" method="post">
          <div class="form-group">
              <label for="userID">ユーザーID:</label>
              <input type="text" id="userID" name="userID" class="form-control" required="required" />
          </div>
          <div class="form-group">
              <label for="password">パスワード:</label>
              <input type="password" id="password" name="password" class="form-control" required="required" />
          </div>
          <button type="submit" class="btn btn-primary btn-block">ログイン</button>
      </form>
```

#### ログイン画面アクション（LoginController.java）の実装
loginアクション：
```
	@RequestMapping(value = "/login")
	@ResponseBody
	public ModelAndView loginAction(@ModelAttribute LoginForm form, ModelAndView model, HttpSession httpSession, HttpServletRequest request) {
		String userID = form.getUserID();
		if(userID != null && !"".equals(userID)) {
			String password = form.getPassword();
			User user = userService.selectUserByPK(userID);
			if(user != null && password != null && password.equals(user.getPassword())) {
				httpSession.setAttribute("userID", userID);
				httpSession.setAttribute("userName", user.getUserName());
				model.addObject("userID", userID);
				model.setViewName("main");
			} else {
				model.setViewName("login");
				model.addObject("errorLogin", "ユーザーIDまたはパスワードが正しくありません。");
			}
		}
		
		String timeout = request.getParameter("timeout");
		if(timeout != null && "true".equals(timeout)) {
			model.addObject("errorLogin", "ログアウトされたか、セッションタイムアウトが発生しました、ログインし直してください。");
		}
		return model;
	}
```
- ログイン画面（login.html）の初期表示時に、form.getUserID() が null なので、何の処理も行いません。
- ログイン画面（login.html）からログインボタンを押下すると、ユーザーIDとパスワードの正当性チェックを行います。チェックがOKの場合、セッションの属性に userID と userName を保存し、main画面に遷移します。チェックが失敗した場合、メッセージを保持し、login画面に遷移します。
- タイムアウトが発生した場合、timeout画面から遷移されるため、エラーメッセージを保持し、login画面に遷移します。

logoutアクション：セッション情報を破棄する
```
	@RequestMapping(value = "/logout")
	@ResponseBody
	public ModelAndView logoutAction(ModelAndView model, HttpSession httpSession, SessionStatus sessionStatus) {
		httpSession.removeAttribute("userID"); //セッションに登録したuserIDを削除する
		httpSession.removeAttribute("userName");//セッションに登録したuserNameを削除する
		httpSession.invalidate(); //セッションの破棄
		sessionStatus.setComplete(); //セッション情報を破棄する
		model.setViewName("login");
		return model;
	}
```

## おわりに
全体のソースコードはGitHubにアップロードしましたので、ぜひダウンロードして確認してください。

https://github.com/yoruzuki/WebService_Spring/tree/main/demo%20ver0.3

また、この記事では一部のソースコードしか説明していませんが、説明を希望する特定の部分があればリクエストしていただければと思います。詳細や特定の質問について連絡いただければ、それに基づいて訂正や説明を提供できます。

次回は多言語対応に、メッセージおよび画面の言語を切り替えられるように実装していきたいと考えています。お楽しみにお待ちください。

それではまた。