はじめに
必要最低限のログイン機能のみを実装しています。ソースファイルはgithubに載せています。
環境構築
pleiadesからpleiades All in One EclipseのJavaを使ってインストールしました。Java開発に必要なものをまとめてインストールできてとても便利なのでおすすめです。
開発環境
- Eclipse
 - Java17
 - Maven
 - Spring Tools
 - Spring boot
 - Spring Security
 - PostgreSQL 16
 
データベース作成
PostgreSQLで以下のカラムを持つテーブルを作成します。

下記を入力して▶を押すとテーブルが作成できます。テーブル名xxxxは好きなものに変えてください。今回のソースコードではtestにしています。

下記のユーザーをテーブルに登録しておきます。
| id | password | 
|---|---|
| user | $2a$10$cFLRib1xp1u9JqeWuNawbe0NpLaTQP4/Klx6.QCEhNE7D/RryTRC. | 
INSERT INTO xxxx (id, password) 
VALUES ('user', '$2a$10$cFLRib1xp1u9JqeWuNawbe0NpLaTQP4/Klx6.QCEhNE7D/RryTRC.');
新規プロジェクト作成
左上 ファイル(F)>新規(N)>Springスターター・プロジェクト(Spring Initializr)を開き、下記の設定で[次へ]をクリック。

プロジェクトの実行
pom.xmlを開き、下記を追加します。
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
次に、application.propertiesに内容を追加します。
上4行はPostgreSQLに接続するための設定です。
パスワードは自分で設定したものに変更します。
それ以外はデフォルトであれば下記のように設定されています。
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=postgresのパスワード
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
次に、プロジェクトを右クリック>実行>5 Maven Installをクリックして実行します。

コンソールで下記のようにBUILD SUCCESSと表示されれば成功です。

次に、プロジェクトを右クリック>実行>9 Spring Bootアプリケーションをクリックして実行します。

http://localhost:8080/ にアクセスするとデフォルトのログイン画面が表示されます。

今後認証の設定を行うことでログインする必要はなくなりますが、こちらの記事にUsernameとPasswordの設定方法を記載しています。
ログイン機能の実装
ここから、ログイン機能の実装をしていきます。
プロジェクト構成

プロジェクトには、デフォルトでフォルダが作成されていますが、フォルダごとに格納するファイルが決まっています。以下のサイトが参考になります。
https://qiita.com/aaaaaayyymmm/items/f5458d2302c11202136d
View
トップページ
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
   <header>
	   会員登録
	   <a href="/loginPage">ログイン</a>
   </header>
   <h1>Index</h1>
</body>
</html>
ログイン用ページ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
</head>
<body>
    <h1>ログイン</h1>
    <div th:if="${param.error}" > <!-- リクエストパラメータの判定 -->
        <div>
          UserNameまたは、Passwordが違います。 <!-- 認証エラーメッセージ -->
        </div>
    </div>
    <form method="post" th:action="@{/authenticate}"> <!-- ログイン処理のパスを指定  -->
        <label>UserName:</label>
        <input type="text" id="userName" name="userName"> <!-- ユーザ名の入力部 -->
        <br>
        <label>Password:</label>
        <input type="password" id="password" name="password"> <!-- パスワードの入力部 -->
        <br>
        <input type="submit" id="submit" value="ログイン">
    </form>
</body>
</html>
ログイン後ページ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
    <h1>Home</h1>
    <form method="post" th:action="@{/logout}">
        <input type="submit" value="ログアウト">
    </form>
</body>
</html>
Controller
Mapping情報を受け取ってページを表示します。
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class IndexController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String helloWorld(Model model) {
        return "index";
    }
}
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
    @GetMapping("/loginPage")
    String loginPage() {
        return "login";
    }
}
package com.bootsample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
    @GetMapping("/home")
    public String home() {
        return "home";
    }
}
Config
package com.bootsample.config;
//import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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
    protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 認可の設定
        http.authorizeHttpRequests(authz -> authz
        		.requestMatchers("/").permitAll()           //ページは全ユーザからのアクセスを許可
                .requestMatchers("/loginPage").permitAll()  //loginPageは、全ユーザからのアクセスを許可
                .anyRequest().authenticated()               //上記ページ以外は認証を求める
                );
        // ログイン設定
        http.formLogin(login -> login                   //フォーム認証の有効化
                .loginPage("/loginPage")                //ログインフォームを表示するパス
                .loginProcessingUrl("/authenticate")    //フォーム認証処理のパス
                .usernameParameter("userName")          //ユーザ名のリクエストパラメータ名
                .passwordParameter("password")          //パスワードのリクエストパラメータ名
                .defaultSuccessUrl("/home")             //認証成功時に遷移するデフォルトのパス
                .failureUrl("/loginPage?error=true")    //認証失敗時に遷移するパス
        		);
        // ログアウト設定
        http.logout(logout -> logout
                .logoutSuccessUrl("/loginPage")         //ログアウト成功時に遷移するパス
                .permitAll()                            //全ユーザに対してアクセスを許可
                );
        return http.build();
    }
	
	@Bean
	protected PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}
Entity
package com.bootsample.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity
@Table(name = "test")
public class MyUser {
	@Id
	private String id;
	private String password;
}
Repository
package com.bootsample.repository;
import org.springframework.stereotype.Repository;
import com.bootsample.entity.MyUser;
@Repository
public interface UserDao {
    MyUser findUserByUserName(String userName);
}
package com.bootsample.repository;
import java.util.Map;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.bootsample.entity.MyUser;
@Repository
public class UserDaoImpl implements UserDao {
    private final JdbcTemplate jdbcTemplate;
    public UserDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    /**
     * userNameを検索条件にSELECT文を実行して、DBに登録されているユーザを検索する
     * @param userName
     * @return User
     */
    @Override
    public MyUser findUserByUserName(String userName) {
        String sql = "SELECT id, password FROM test WHERE id = ?";
        //ユーザを一件取得
        Map<String, Object> result = jdbcTemplate.queryForMap(sql, userName);
        // Entityクラス(User型)に変換
        MyUser user = convMapToUser(result);
        return user;
    }
    /**
     * SQL SELECT文を実行した結果(Map<String, Object>)をUser型に変換する
     * @param Map<String, Object>
     * @return User
     */
    private MyUser convMapToUser(Map<String, Object> map) {
        MyUser user = new MyUser();
        user.setId((String) map.get("username"));
        user.setPassword((String) map.get("password"));
        return user;
    }
}
Service
package com.bootsample.service;
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.bootsample.entity.MyUser;
public class AccountUserDetails implements UserDetails {
	private final MyUser myUser;
    public AccountUserDetails(MyUser myUser) {
        this.myUser = myUser;
    }
    public MyUser getUser() { // --- (1) Entityである MyUserを返却するメソッド
        return myUser;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { // --- (3) ユーザに与えられている権限リストを返却するメソッド
        return AuthorityUtils.createAuthorityList("ROLE_USER");
    }
    @Override
    public String getPassword() { // --- (4) 登録されているパスワードを返却するメソッド
        return this.myUser.getPassword();
    }
    @Override
    public String getUsername() { // --- (5) ユーザ名を返却するメソッド
        return this.myUser.getId();
    }
    @Override
    public boolean isAccountNonExpired() { // --- (6) アカウントの有効期限の状態を判定するメソッド
        return true;
    }
    @Override
    public boolean isAccountNonLocked() { // --- (7) アカウントのロック状態を判定するメソッド
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() { // --- (8) 資格情報の有効期限の状態を判定するメソッド
        return true;
    }
    @Override
    public boolean isEnabled() { // --- (9) 有効なユーザかを判定するメソッド
        return true;
    }
}
package com.bootsample.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
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.bootsample.entity.MyUser;
import com.bootsample.repository.UserDaoImpl;
@Service
public class AccountUserDetailsService implements UserDetailsService {
	@Autowired
    private UserDaoImpl userDaoImpl;
	
    public UserDetails loadUserByUsername(String userName)
    	throws UsernameNotFoundException{
    	
    	if(userName == null || "".equals(userName)) {
    		throw new UsernameNotFoundException(userName + "is not found");
    	}
    	
    	try {
    		MyUser myUser = userDaoImpl.findUserByUserName(userName);
    		
            if (myUser != null) {
                return new AccountUserDetails(myUser); // --- (2) UserDetailsの実装クラスを生成
            } else {
                throw new UsernameNotFoundException(userName + "is not found");
            }
    	} catch(EmptyResultDataAccessException e) {
    		throw new UsernameNotFoundException(userName + "is not found");
    	}
        
    }
}

