SpringSecurityでアカウント登録を実装する手順を解説しています

SpringBoot

はじめに

こちらの記事で使用しているアプリは既存で作成したアプリに機能追加として、SpringSecurityのアカウント作成機能を追加している形となります。

SpringSecurityのアカウント作成でお悩みの方は参考になる可能性があります。

概要から実装まで行い、アカウント登録までをゴールとします。

要件定義

おおまかな要件などからまとめていきます。

目的:アカウントを新規で登録できるようにすること

アカウント登録ページ

  • 「ユーザー名」「メールアドレス」「パスワード」「パスワード(確認用)」の4項目を入力できるフォームを作成する。
  • 「ユーザー名」「メールアドレス」「パスワード」「パスワード(確認用)」のそれぞれに、空欄入力を許可しないバリデーションを設定する。
  • 「メールアドレス」はメールアドレスの形式に沿った入力以外を許可しない設定とする。
  • 「パスワード」は6文字以上で入力しないと登録出来ない設定とする。
  • 空欄入力を許可しないため「必須」の文字をレイアウトに組み込む。

userのServiceクラス

ビジネスロジックでは以下を実装する

  • 「アカウント登録ページから入力された情報を基に、アカウントの情報をDBへ登録できるようにする」
  • 「メールアドレスの重複がないように、登録済みかを確認する」
  • 「アカウント登録ページのパスワード入力の値が、パスワード(確認用)と一致するかを確認する」

ユーザーのServiceクラス

ビジネスロジックでは以下を実装する

  • 「アカウント登録ページから入力された情報を基に、アカウントの情報をDBへ登録できるようにする」
  • 「メールアドレスの重複がないように、登録済みかを確認する」
  • 「アカウント登録ページのパスワード入力の値が、パスワード(確認用)と一致するかを確認する」

新規アカウント登録を実装

RoleRepository.javaを編集する

権限名称を取得するインターフェースを定義します。つまり引数の文字列とDBのroleNameの値が合致した情報を検索するインターフェースという意味です。

RoleRepository.java
package com.app.progTrack.repository;

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

import com.app.progTrack.entity.Role;

public interface RoleRepository extends JpaRepository<Role, Integer>{
	// Roleエンティティのフィールド「roleName」を検索する
	public Role findByRoleName(String roleName);
}

UserService.javaを作成する

それでは、以下を参考にして「UserService.java」を作成してみましょう!

「UserService.java」を編集します。

UserService.java
package com.app.progTrack.service;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.app.progTrack.entity.Role;
import com.app.progTrack.entity.User;
import com.app.progTrack.form.SignupForm;
import com.app.progTrack.repository.RoleRepository;
import com.app.progTrack.repository.UserRepository;

import jakarta.transaction.Transactional;

@Service
public class UserService {
	private final UserRepository userRepository;
	private final RoleRepository roleRepository;
	private final PasswordEncoder passwordEncoder;
	
	public UserService (UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
		this.userRepository = userRepository;
		this.roleRepository = roleRepository;
		this.passwordEncoder = passwordEncoder;
	}
	
	// フォームから送信された情報を基にユーザーをDBに登録する
	// 以下「@Transactional」により、途中で中断された場合、登録情報を保存しない
	@Transactional
	public User createUser(SignupForm signupForm) {
		// アカウント登録するため、登録情報を格納する変数を用意
		User user = new User();
		// ロール名を取得する この場合「MEMBER」という権限名を取得する
		Role role = roleRepository.findByRoleName("MEMBER");
		// signupFormから入力された情報を取得し、その情報を「user」へセットしている
		user.setUserName(signupForm.getName());
		user.setEmail(signupForm.getEmail());
		// 取得したパスワードはハッシュ化してからセットする
		user.setPassword(passwordEncoder.encode(signupForm.getPassword()));
	    // この場合、権限名は「MEMBER」をセット
		user.setRole(role);
		// ユーザーは有効として「true」をセットする
		user.setEnabled(true);
		// 取得しセットした情報をDBへ保存する
		return userRepository.save(user);
	}
	
	// メールアドレスが登録済みかをチェックする
	public boolean isEmailChecked(String email) {
		// 引数で受け取ったメールアドレスに一致する「user」を検索する
		User user = userRepository.findByEmail(email);
		// 検索結果:結果がnullでない場合(メールアドレスが登録済み)  == 検索結果が一致する  すなわちtrueを返す
		// 検索結果: 結果が一致しない つまりDBに同じメールアドレスが存在しないのでfalseを返す
		return user != null;
	}
	
	// パスワードとパスワード(確認用)の入力値を確認する
	public boolean isPasswordValid(String password, String rePassword) {
		// 引数で取得したパスワードとパスワード(確認用)を比較し、合致していればtrueを返す 違うならfalseを返す
		return password.equals(rePassword);
	}
}

AuthenticationController.javaを編集する

「AuthenticationController」の「post」アクションを実装します。

AuthenticationController.java
package com.app.progTrack.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.app.progTrack.form.SignupForm;
import com.app.progTrack.service.UserService;

@Controller
public class AuthenticationController {
	private final UserService userService;
	
	public AuthenticationController (UserService userService) {
		this.userService = userService;
	}
	
	// ログインページ 初期表示
	@GetMapping("/login")
	public String login() {
		return "authentication/login";
	}
	
	// アカウント登録ページ 初期表示
	@GetMapping("/signup")
	public String signup(Model model) {
		model.addAttribute("signupForm", new SignupForm());
		return "authentication/signup";
	}
	
	// アカウント登録の処理
	@PostMapping("/signup")
	public String signup(@ModelAttribute @Validated SignupForm signupForm, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
		
		// メールアドレスが重複している場合
		if (userService.isEmailChecked(signupForm.getEmail())) {
			// 新規アカウント登録画面で発生したフォームのエラーをオブジェクト化し、bindingResultに格納している
			FieldError  fieldError = new FieldError(bindingResult.getObjectName(), "email", "そのメールアドレスは登録済みされています");
			bindingResult.addError(fieldError);
		}
		
		// パスワードとパスワード(確認用)の一致しない場合
		if (! userService.isPasswordValid(signupForm.getPassword(), signupForm.getRePassword())) {
			FieldError fieldError = new FieldError(bindingResult.getObjectName(), "password", "入力されたパスワードが一致しません");
			bindingResult.addError(fieldError);
		}
		
		// フィールドに設定したバリデーションルールを確認する
		if (bindingResult.hasErrors()) {
			// エラーの場合は、もう一度アカウント登録画面を表示しないといけないので、modelにaddし画面に返す
			model.addAttribute("signupForm", signupForm);
			return "authentication/signup";
		}
		
		// 登録の処理
		userService.createUser(signupForm);
		redirectAttributes.addFlashAttribute("successMessage", "新規アカウントの登録が完了しました");
		
		return "redirect:/studytime";
	}
}

引数の解説をします。

Java
@ModelAttribute SignupForm signupForm
  • HTTPリクエストのパラメーターをもとに、SignupFormのオブジェクトを生成する。これにより、フォームのデータを簡単に扱う事が可能となる。
  • コントローラーがこのオブジェクトを自動的に初期化し、リクエストパラメーターをバインドする。
Java
@Validated SignupForm signupForm
  • signupFormの設定したバリデーションを実行してくれる。
  • バリデーションに失敗時はBindingResultにエラー情報が格納される。
Java
BindingResult bindingResult
  • バリデーションが成功したかをチェックする。
  • もしエラーならその情報を格納する。
Java
RedirectAttributes redirectAttributes
  • リダイレクト後に表示するエラーメッセージやサクセスメッセージを保存するために一時的に使用できる。

ここまでで、DBへユーザーを登録する事は出来るようになりました。

学習時間一覧側の処理に関して

考え方の参考にして頂ければと思います。

「users」テーブルをSQLで出力する

SQL
SELECT *
FROM users
;

「study_times」テーブルをSQLで出力する

SQL
SELECT *
FROM study_times
;

出力した結果、「user_id」を作成し、該当ユーザーごとに振り分けてあげれば関連性が担保される。

■「users」のid「12」に紐づく学習データをSQLで出力する

SQL
SELECT *
FROM users
LEFT JOIN study_times
ON users.user_id = study_times.user_id
WHERE users.user_id = 12
;

■「users」のid「12」に紐づく学習データの「2024/11」の学習時間合計をSQLで出力する

SQL
SELECT SUM(TIMESTAMPDIFF(MINUTE, start_time, end_time))
FROM study_times
WHERE MONTH(start_time) = 11
AND YEAR(start_time) = 2024
AND end_time IS NOT NULL
AND user_id = 12
;

改修したコード全般

Controller
StudyTimeController
package com.app.progTrack.controller;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.TextStyle;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.security.UserDetailsImpl;
import com.app.progTrack.service.StudyTimeService;

@Controller
@RequestMapping("/studytime")
public class StudyTimeController {
	private final StudyTimeService studyTimeService;
	
	public StudyTimeController(StudyTimeService studyTimeService) {
		this.studyTimeService = studyTimeService;
	}
	
	@GetMapping
	public String listStudyTimes(Model model, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @RequestParam(value = "month", required = false) Integer month, @RequestParam(value = "year", required = false) Integer year) {
		// 現在の月と前月の累計時間を取得する
		int currentMonth = LocalDate.now().getMonthValue();
		int currentYear = LocalDate.now().getYear();
		Integer userId = userDetailsImpl.getId();
		model.addAttribute("userId", userId);
		
		// 初期表示は当月の学習時間を取得する
		if (month == null || year == null) {
			month = currentMonth;
			year = currentYear;
		}
		
		// 学習時間を取得する
		List<StudyTime> studyTimes = studyTimeService.findAllByMonthAndYear(month, year, userId);
		model.addAttribute("studyTimes", studyTimes);
		
		// 当月の学習時間を算出
		double _currentMonthTotal = studyTimeService.getTotalStudyTimeForMonth(month, year, userId) / 60.0;
		
		// 現在の月 - 前月 = 0 なら 12を返す 0でないなら-1を引いた数字を返す
		int lastMonth = month - 1 == 0 ? 12 : month -1;
		int lastYear = month - 1 == 0 ? year - 1 : year;
		// 前月の学習時間を算出
		double _lastMonthTotal = studyTimeService.getTotalStudyTimeForMonth(lastMonth, lastYear, userId) / 60.0;
		
		// 小数点第二位を切り上げてから小数点第一位まで表示する
		String currentMonthTotal = String.format("%.1f", Math.ceil(_currentMonthTotal * 10) / 10);
		String lastMonthTotal = String.format("%.1f", Math.ceil(_lastMonthTotal * 10) / 10);
		
		model.addAttribute("currentMonthTotal", currentMonthTotal);
		model.addAttribute("lastMonthTotal", lastMonthTotal);
		
		// セレクトボックス用のデータ
		model.addAttribute("months", createMonthList());
		model.addAttribute("years", createYearList(currentYear));
		model.addAttribute("currentYear", year);
		model.addAttribute("currentMonth", month);
		
		return "studytime/studyTimeList";
	}
	
	// セレクトボックス用:月選択用
	private List<Integer> createMonthList() {
		return IntStream.rangeClosed(1, 12).boxed().collect(Collectors.toList());
	}
	
	// セレクトボックス用:日選択用
	private List<Integer> createYearList(int currentYear) {
		return IntStream.rangeClosed(currentYear - 5, currentYear).boxed().collect(Collectors.toList());
	}
	
	@GetMapping("/new")
	public String newStudyTimeForm(Model model) {
		
		// ベースとなる日付を取得する この場合はJST取得の日付 現在の時刻取得用
		LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
		// どういう型で用意するかを定義
		DateTimeFormatter dateFormateer = DateTimeFormatter.ofPattern("MM/dd"); // 日付で形成する
		DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); // 時間で形成する
		// 現在の時刻にあてはめる
		String date = currentDateTime.format(dateFormateer);
		String time = currentDateTime.format(timeFormatter);

		model.addAttribute("studyTimeObject", new StudyTime());
		model.addAttribute("date", date);
		model.addAttribute("time", time); //ページが読み込まれた時の初期表示の時刻表示用
		// 曜日を取得する
		model.addAttribute("currentDayOfWeek", currentDateTime.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.JAPAN));
		// 学習中フラグの状態を追加する
		model.addAttribute("isLearningActive", studyTimeService.isLiarningActive());
		return "studytime/studyTimeForm";
	}
	
	// 時刻を返すエンドポイント
	@GetMapping("/current-time")
	@ResponseBody
	public String getCurrentTime() {
		LocalDateTime currentDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
		return currentDateTime.format(formatter);
	}
	
	@PostMapping
	public String createStudyTime(@ModelAttribute StudyTime studyTime, @RequestParam String action, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
		
		if ("start".equals(action)) {
			studyTime.setStartTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
			studyTime.setEndTime(null);
			studyTime.setIsLearning(true);
			studyTime.setUserId(userDetailsImpl.getId());
			studyTimeService.saveStudyTime(studyTime);
		} else if ("end".equals(action)) {
			// 直近の学習セッションを取得する
			StudyTime lastStudyTime = studyTimeService.getLastStudyTime();
			
			if (lastStudyTime != null) {
				lastStudyTime.setEndTime(studyTimeService.convertToJST(LocalDateTime.now(ZoneId.of("UTC"))));
				lastStudyTime.setIsLearning(false);
				studyTimeService.saveStudyTime(lastStudyTime);
			}
		}
		
		return "redirect:/studytime";
	}
	
	@GetMapping("/{id}/edit")
	public String editStudyTimeForm(@PathVariable Long id, Model model) {
		StudyTime studyTime = studyTimeService.getStudyTime(id);
		model.addAttribute("studyTimeObject", studyTime);
		return "studytime/edit";
	}
	
	@PostMapping("/{id}")
	public String updateStudyTime(@PathVariable Long id, @ModelAttribute StudyTime studyTime) {
		studyTime.setStudyTimesId(id);
		studyTimeService.saveStudyTime(studyTime);
		return "redirect:/studytime";
	}
	
	@PostMapping("/{id}/delete")
	public String deleteStudyTime(@PathVariable Long id) {
		studyTimeService.deleteStudyTime(id);
		return "redirect:/studytime";
	}
}
Entity
Java
package com.app.progTrack.entity;

import java.sql.Timestamp;
import java.time.LocalDateTime;

import jakarta.persistence.Column;
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 = "study_times")
@Data
public class StudyTime {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "study_times_id")
	private Long studyTimesId;
	
	@Column(name = "start_time")
	private LocalDateTime startTime;
	
	@Column(name = "end_time")
	private LocalDateTime endTime;
	
	@Column(name = "created_at", insertable = false, updatable = false)
	private Timestamp createdAt;
	
	@Column(name = "updated_at", insertable = false, updatable = false)
	private Timestamp updatedAt;
	
	@Column(name = "is_learning")
	private Boolean isLearning;
	
	// userIdを格納する事によって、学習時間がどのuserに紐づくかを判別する
	@Column(name = "user_id")
	private Integer userId;
}
Java
package com.app.progTrack.entity;

import java.sql.Timestamp;

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

@Entity
@Table(name = "users")
@Data
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "user_id")
	private Integer userId;
	
	@Column(name = "user_name")
	private String userName;
	
	@Column(name = "email")
	private String email;
	
	@Column(name = "password")
	private String password;
	
	@Column(name = "enabled")
	private Boolean enabled;
	
	@ManyToOne
	@JoinColumn(name = "role_id")
	private Role role;
	
	@Column(name = "created_at", insertable = false, updatable = false)
	private Timestamp createdAt;
	
	@Column(name = "updated_at", insertable = false, updatable = false)
	private Timestamp updatedAt;

}
Repository
Java
package com.app.progTrack.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.app.progTrack.entity.StudyTime;

public interface StudyTimeRepository extends JpaRepository<StudyTime, Long>{
	 
	 // 学習時間を計算する
	 @Query("SELECT SUM(TIMESTAMPDIFF(MINUTE, s.startTime, s.endTime)) FROM StudyTime s WHERE MONTH(s.startTime) = :month AND YEAR(s.startTime) = :year AND s.endTime IS NOT NULL AND s.userId = :userId")
	 Long findTotalStudyTimeByMonth(@Param("month") int month, @Param("year") int year, @Param("userId") Integer userId);
	 
	 // 当月を絞り込む
	 @Query("SELECT s FROM StudyTime s WHERE MONTH(s.startTime) = :month AND YEAR(s.startTime) = :year AND s.userId = :userId")
	 List<StudyTime> findAllByMonthAndYear(@Param("month") int month, @Param("year") int year, @Param("userId") Integer userId);
	 
}
Service
Java
package com.app.progTrack.service;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;

import org.springframework.stereotype.Service;

import com.app.progTrack.entity.StudyTime;
import com.app.progTrack.repository.StudyTimeRepository;

@Service
public class StudyTimeService {
	private final StudyTimeRepository studyTimeRepository;
	
	public StudyTimeService(StudyTimeRepository studyTimeRepository) {
		this.studyTimeRepository = studyTimeRepository;
	}
	
	// 学習時間の打刻セット
	public void saveStudyTime(StudyTime studyTime) {

		studyTimeRepository.save(studyTime);
	}
	
	// セレクトボックスで選択された月のデータを取得する
	public List<StudyTime> findAllByMonthAndYear(int month, int year, Integer userId) {
		return studyTimeRepository.findAllByMonthAndYear(month, year, userId);
	}
	
	// 月毎の累計時間を取得する
	public long getTotalStudyTimeForMonth(int month, int year, Integer userId) {
		Long total = studyTimeRepository.findTotalStudyTimeByMonth(month, year, userId);
		return total != null ? total : 0; // nullの場合は0を返す
	}
	
	// 最後のレコードを取得する
	public StudyTime getLastStudyTime() {
		List<StudyTime> studyTimes = studyTimeRepository.findAll();
		return studyTimes.isEmpty() ? null : studyTimes.get(studyTimes.size() - 1);
	}
	
	// UTCからJSTへ変換する処理
	public LocalDateTime convertToJST(LocalDateTime utcDateTime) {
		// LocalDateTimeをUTCのZoneTimeに変換
		ZonedDateTime utcZoned = utcDateTime.atZone(ZoneId.of("UTC"));
		// UTCからJSTへ同じ瞬間の時間を取得
		ZonedDateTime jstZonde = utcZoned.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
		// LocalDateTimeに変換する
		return jstZonde.toLocalDateTime();
	}
	
	// 学習中の判定をする
	public boolean isLiarningActive() {
		StudyTime lastStudyTime = getLastStudyTime(); // 最後のレコードを取得
		// lastStudyTimeがnullではない 且つ getIsLearningメソッドがtrueの場合に戻り値はtrueが返る
		return lastStudyTime != null && Boolean.TRUE.equals(lastStudyTime.getIsLearning());
	}
	
	// 編集用
	public StudyTime getStudyTime(Long id) {
		return studyTimeRepository.findById(id).orElse(null);
	}
	// 削除用
	public void deleteStudyTime(Long id) {
		studyTimeRepository.deleteById(id);
	}
}
html
HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>ログイン画面</title>
	<!-- Bootstrap -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">

</head>
<body>
	<header>
		<nav class="navbar navbar-expand-lg mb-5" style="background-color: white; box-shadow: 0 2px 5px rgba(128, 128, 128, 0.5);">
		    <div class="container">
		        <a class="navbar-brand" th:href="@{/}">
		            <img src="/images/heder_logo.png" alt="PROG-TRACK" style="height: 50px;" />
		        </a>
		        <ul class="navbar-nav">
		            <li class="nav-item">
		                <a class="nav-link" th:href="@{/login}">ログイン</a>
		            </li>
		            <li class="nav-item">
		                <a class="nav-link" th:href="@{/signup}">会員登録</a>
		            </li>
		        </ul>
		    </div>
		</nav>
	</header>
	<main>
		<div class="container">
			<div class="row text-center">
				<div th:if="${successMessage}" th:text="${successMessage}" class="alert alert-info"></div>
		        <div th:if="${param.loggedOut}" class="alert alert-info" role="alert">
		            ログアウトしました
		        </div> 
		        <div th:if="${param.error}" class="alert alert-danger" role="alert">
		            メールアドレス、パスワードが正しく入力されていません
		        </div>
			</div>
		</div>
		
		<div class="container d-flex align-items-center justify-content-center">
		    <div class="row">
		        <h1 class="mb-4 text-center" style="color: #2d196f;">ログイン</h1>
		        
		        
		        <form th:action="@{/login}" method="post" class="p-4 border rounded shadow-sm bg-light">
		            <div class="mb-3">
		                <label class="form-label fw-bold">メールアドレス</label>
		                <input type="text" name="username" autocomplete="email" placeholder="メールアドレス" class="form-control" autofocus required>
		            </div>
		            <div class="mb-3">
		                <label class="form-label fw-bold">パスワード</label>
		                <input type="password" name="password" autocomplete="new-password" placeholder="パスワード" class="form-control" required>
		            </div>
		            <div>
		                <button type="submit" class="btn btn-primary w-100">ログイン</button>
		            </div>
		        </form>
		    </div>
		</div>	
	</main>

	<!-- Bootstrap -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>
HTML
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>新規アカウント登録</title>
	<!-- Bootstrap -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
	<header>
		<nav class="navbar navbar-expand-lg mb-5" style="background-color: white; box-shadow: 0 2px 5px rgba(128, 128, 128, 0.5);">
		    <div class="container">
		        <a class="navbar-brand" th:href="@{/}">
		            <img src="/images/heder_logo.png" alt="PROG-TRACK" style="height: 50px;" />
		        </a>
		        <ul class="navbar-nav">
		            <li class="nav-item">
		                <a class="nav-link" th:href="@{/login}">ログイン</a>
		            </li>
		        </ul>
		    </div>
		</nav>
	</header>
	
	<main>
		<div class="container d-flex align-items-center justify-content-center">
		    <div class="col-md-4">
		        <h2 class="text-center mb-4">新規アカウント登録</h2>

		        <form method="post" th:action="@{/signup}" th:object="${signupForm}" class="p-4 border rounded shadow-sm bg-light">
		            <div class="mb-3">
						<label for="name" class="form-label">
							ユーザー名 <span class="text-danger">必須</span>
						</label>
						<input type="text" th:field="*{name}" class="form-control" autocomplete="name" autofocus placeholder="ユーザー名">
						<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="text-danger"></div>
					</div>
					
					<div class="mb-3">
						<label for="email" class="form-label">
							メールアドレス <span class="text-danger">必須</span>
						</label>
						<input type="email" th:field="*{email}" class="form-control" autocomplete="email" placeholder="sample@gmail.com">
						<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="text-danger"></div>
					</div>
					
					<div class="mb-3">
						<label for="password" class="form-label">
							パスワード <span class="text-danger">必須</span>
						</label>
						<input type="password" th:field="*{password}" class="form-control" autocomplete="new-password">
						<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="text-danger"></div>
					</div>
					
					<div class="mb-3">
						<label for="rePassword" class="form-label">
							パスワード(確認用) <span class="text-danger">必須</span>
						</label>
						<input type="password" th:field="*{rePassword}" class="form-control" autocomplete="new-password">
						<div th:if="${#fields.hasErrors('rePassword')}" th:errors="*{rePassword}" class="text-danger"></div>
					</div>
					
					<div>
						<button type="submit" class="btn btn-primary w-100">アカウント登録</button>
					</div>
		        </form>
		    </div>
		</div>	
	</main>

	<!-- Bootstrap -->
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</body>
</html>

ブラウザで確認

おわりに

お読みいただきありがとうございました。

コメント