【Spring Boot】Spring Securityでログイン認証機能を実装する|簡単な掲示板アプリの作成
Spring BootとSpring Securityを使い、Webアプリに必須のログイン認証機能を実装する手順を解説します。ユーザー登録、BCryptによる安全なパスワードの保存、アクセス制御、Thymeleafでの表示切替まで、認証機能の基本を一から丁寧に学べます。
開発環境
- OS: Windows10
- Visual Studio Code: 1.73.0
- Java: OpenJDK 23
- Spring Boot: 3.4.0
- Lombok: 1.18.30
- Thymeleaf Layout Dialect: 3.3.0
- Spring Security: 6.1.1
- Thymeleaf Extras Spring Security: 3.1.2.RELEASE
サンプルコード
/pom.xml
/pom.xml1 <artifactId>thymeleaf-layout-dialect</artifactId> 2 <version>3.3.0</version> 3 </dependency> 4+ <dependency> 5+ <groupId>org.springframework.boot</groupId> 6+ <artifactId>spring-boot-starter-security</artifactId> 7+ </dependency> 8+ <dependency> 9+ <groupId>org.thymeleaf.extras</groupId> 10+ <artifactId>thymeleaf-extras-springsecurity6</artifactId> 11+ <version>3.1.2.RELEASE</version> 12+ </dependency> 13 </dependencies> 14 15 <build> 16
/src/main/java/com/example/bbs/config/SecurityConfig.java
/src/main/java/com/example/bbs/config/SecurityConfig.java1+package com.example.bbs.config; 2+ 3+import org.springframework.context.annotation.Bean; 4+import org.springframework.context.annotation.Configuration; 5+import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7+import org.springframework.security.web.SecurityFilterChain; 8+ 9+@Configuration 10+public class SecurityConfig { 11+ 12+ @Bean 13+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 14+ http 15+ .authorizeHttpRequests(auth -> auth 16+ .requestMatchers("/", "/auth/register", "/auth/login").permitAll() 17+ .anyRequest().authenticated()) 18+ .formLogin(form -> form 19+ .loginPage("/auth/login") 20+ .defaultSuccessUrl("/posts", true) 21+ .permitAll()) 22+ .logout(logout -> logout 23+ .logoutUrl("/auth/logout") // ログアウトURL 24+ .logoutSuccessUrl("/auth/login?logout") // ログアウト成功後のリダイレクト先 25+ .invalidateHttpSession(true) // セッションを無効化 26+ .deleteCookies("JSESSIONID") // クッキーを削除 27+ .permitAll()); 28+ return http.build(); 29+ } 30+ 31+ @Bean 32+ public BCryptPasswordEncoder passwordEncoder() { 33+ return new BCryptPasswordEncoder(); 34+ } 35+} 36
/src/main/java/com/example/bbs/controller/AuthController.java
/src/main/java/com/example/bbs/controller/AuthController.java1+package com.example.bbs.controller; 2+ 3+import com.example.bbs.model.User; 4+import com.example.bbs.service.CustomUserDetailsService; 5+import org.springframework.stereotype.Controller; 6+import org.springframework.ui.Model; 7+import org.springframework.web.bind.annotation.*; 8+ 9+@Controller 10+@RequestMapping("/auth") 11+public class AuthController { 12+ 13+ private final CustomUserDetailsService userDetailsService; 14+ 15+ public AuthController(CustomUserDetailsService userDetailsService) { 16+ this.userDetailsService = userDetailsService; 17+ } 18+ 19+ // ログインページ 20+ @GetMapping("/login") 21+ public String login() { 22+ return "auth/login"; 23+ } 24+ 25+ // 登録ページ 26+ @GetMapping("/register") 27+ public String registerForm(Model model) { 28+ model.addAttribute("user", new User()); 29+ return "auth/register"; 30+ } 31+ 32+ // 登録処理 33+ @PostMapping("/register") 34+ public String register(@ModelAttribute User user) { 35+ userDetailsService.registerUser(user); 36+ return "redirect:/auth/login"; // 登録後、ログインページにリダイレクト 37+ } 38+} 39
/src/main/java/com/example/bbs/controller/HomeController.java
/src/main/java/com/example/bbs/controller/HomeController.java1+package com.example.bbs.controller; 2+ 3+import org.springframework.stereotype.Controller; 4+import org.springframework.web.bind.annotation.GetMapping; 5+ 6+@Controller 7+public class HomeController { 8+ 9+ // ログイン後のホームページを表示 10+ @GetMapping("/") 11+ public String home() { 12+ return "home"; 13+ } 14+} 15
/src/main/java/com/example/bbs/model/User.java
/src/main/java/com/example/bbs/model/User.java1+package com.example.bbs.model; 2+ 3+import jakarta.persistence.*; 4+import lombok.Data; 5+ 6+@Entity 7+@Data 8+@Table(name = "users") 9+public class User { 10+ 11+ @Id 12+ @GeneratedValue(strategy = GenerationType.IDENTITY) 13+ private Long id; 14+ 15+ @Column(nullable = false, unique = true) 16+ private String username; 17+ 18+ @Column(nullable = false) 19+ private String password; 20+} 21
/src/main/java/com/example/bbs/repository/UserRepository.java
/src/main/java/com/example/bbs/repository/UserRepository.java1+package com.example.bbs.repository; 2+ 3+import com.example.bbs.model.User; 4+import org.springframework.data.jpa.repository.JpaRepository; 5+ 6+import java.util.Optional; 7+ 8+public interface UserRepository extends JpaRepository<User, Long> { 9+ Optional<User> findByUsername(String username); 10+} 11
/src/main/java/com/example/bbs/service/CustomUserDetailsService.java
/src/main/java/com/example/bbs/service/CustomUserDetailsService.java1+package com.example.bbs.service; 2+ 3+import com.example.bbs.model.User; 4+import com.example.bbs.repository.UserRepository; 5+ 6+import java.util.ArrayList; 7+ 8+import org.springframework.beans.factory.annotation.Autowired; 9+// import org.springframework.security.core.userdetails.User; 10+import org.springframework.security.core.userdetails.UserDetails; 11+import org.springframework.security.core.userdetails.UserDetailsService; 12+import org.springframework.security.core.userdetails.UsernameNotFoundException; 13+import org.springframework.security.crypto.password.PasswordEncoder; 14+import org.springframework.stereotype.Service; 15+ 16+@Service 17+public class CustomUserDetailsService implements UserDetailsService { 18+ 19+ @Autowired 20+ private final UserRepository userRepository; 21+ 22+ @Autowired 23+ private final PasswordEncoder passwordEncoder; 24+ 25+ public CustomUserDetailsService(UserRepository userRepository, PasswordEncoder passwordEncoder) { 26+ this.userRepository = userRepository; 27+ this.passwordEncoder = passwordEncoder; 28+ } 29+ 30+ @Override 31+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 32+ User user = userRepository.findByUsername(username) 33+ .orElseThrow(() -> new UsernameNotFoundException("User not found")); 34+ return new org.springframework.security.core.userdetails.User( 35+ user.getUsername(), 36+ user.getPassword(), 37+ new ArrayList<>()); 38+ } 39+ 40+ public void registerUser(User user) { 41+ // パスワードをハッシュ化 42+ user.setPassword(passwordEncoder.encode(user.getPassword())); 43+ 44+ // ユーザー情報をデータベースに保存 45+ userRepository.save(user); 46+ } 47+} 48
/src/main/resources/templates/auth/login.html
/src/main/resources/templates/auth/login.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Login</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">Login</h1> 9+ <form th:action="@{/auth/login}" method="post" class="mx-auto" style="max-width: 400px;"> 10+ <div class="mb-3"> 11+ <label for="username" class="form-label">UserName</label> 12+ <input type="username" name="username" class="form-control" id="username" required> 13+ </div> 14+ <div class="mb-3"> 15+ <label for="password" class="form-label">Password</label> 16+ <input type="password" name="password" class="form-control" id="password" required> 17+ </div> 18+ <button type="submit" class="btn btn-primary w-100">Login</button> 19+ </form> 20+</main> 21
/src/main/resources/templates/auth/register.html
/src/main/resources/templates/auth/register.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Register</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">Register</h1> 9+ <form th:action="@{/auth/register}" method="post" class="mx-auto" style="max-width: 400px;"> 10+ <div class="mb-3"> 11+ <label for="username" class="form-label">UserName</label> 12+ <input type="username" name="username" class="form-control" id="username" required> 13+ </div> 14+ <div class="mb-3"> 15+ <label for="password" class="form-label">Password</label> 16+ <input type="password" name="password" class="form-control" id="password" required> 17+ </div> 18+ <button type="submit" class="btn btn-success w-100">Register</button> 19+ </form> 20+</main> 21
/src/main/resources/templates/home.html
/src/main/resources/templates/home.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Home</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">掲示板アプリへようこそ</h1> 9+ <div class="text-center"> 10+ <a href="/auth/login" class="btn btn-primary">Login</a> 11+ <a href="/auth/register" class="btn btn-secondary">Register</a> 12+ </div> 13+</main> 14
/src/main/resources/templates/layout/layout.html
/src/main/resources/templates/layout/layout.html1 <!DOCTYPE html> 2 <html xmlns:th="http://www.thymeleaf.org" 3+ xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6" 4 xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"> 5 6 <head> 7 <li class="nav-item"> 8 <a class="nav-link active" href="/">Home</a> 9 </li> 10+ <li class="nav-item" sec:authorize="!isAuthenticated()"> 11+ <a class="nav-link" href="/auth/login">Login</a> 12+ </li> 13+ <li class="nav-item" sec:authorize="!isAuthenticated()"> 14+ <a class="nav-link" href="/auth/register">Register</a> 15+ </li> 16+ <li class="nav-item" sec:authorize="isAuthenticated()"> 17+ <form action="/auth/logout" method="post"> 18+ <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> 19+ <button type="submit" class="nav-link active">Logout</button> 20+ </form> 21+ </li> 22 </ul> 23 </div> 24 </div> 25
コード解説
変更点: Spring Security関連のライブラリを追加
/pom.xml1 </dependency> 2+ <dependency> 3+ <groupId>org.springframework.boot</groupId> 4+ <artifactId>spring-boot-starter-security</artifactId> 5+ </dependency> 6+ <dependency> 7+ <groupId>org.thymeleaf.extras</groupId> 8+ <artifactId>thymeleaf-extras-springsecurity6</artifactId> 9+ <version>3.1.2.RELEASE</version> 10+ </dependency> 11 </dependencies> 12 13 <build>
プロジェクトでSpring Securityの機能を使うために、必要なライブラリ(依存関係)をpom.xmlに追加します。
spring-boot-starter-securityは、ログイン認証やアクセス制御といったSpring Securityの基本的な機能を利用するために必須のライブラリです。
thymeleaf-extras-springsecurity6は、Thymeleaf(HTMLテンプレートエンジン)で「ログインしている時だけボタンを表示する」といった、認証状態に応じた表示の切り替えを簡単に行うためのライブラリです。
変更点: Spring Securityの全体設定クラスを作成
/src/main/java/com/example/bbs/config/SecurityConfig.java1+package com.example.bbs.config; 2+ 3+import org.springframework.context.annotation.Bean; 4+import org.springframework.context.annotation.Configuration; 5+import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7+import org.springframework.security.web.SecurityFilterChain; 8+ 9+@Configuration 10+public class SecurityConfig { 11+ 12+ @Bean 13+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 14+ http 15+ .authorizeHttpRequests(auth -> auth 16+ .requestMatchers("/", "/auth/register", "/auth/login").permitAll() 17+ .anyRequest().authenticated()) 18+ .formLogin(form -> form 19+ .loginPage("/auth/login") 20+ .defaultSuccessUrl("/posts", true) 21+ .permitAll()) 22+ .logout(logout -> logout 23+ .logoutUrl("/auth/logout") 24+ .logoutSuccessUrl("/auth/login?logout") 25+ .invalidateHttpSession(true) 26+ .deleteCookies("JSESSIONID") 27+ .permitAll()); 28+ return http.build(); 29+ } 30+ 31+ @Bean 32+ public BCryptPasswordEncoder passwordEncoder() { 33+ return new BCryptPasswordEncoder(); 34+ } 35+}
アプリケーション全体のセキュリティ設定を行うためのSecurityConfigクラスを新しく作成します。@Configurationアノテーションは、このクラスが設定用のクラスであることをSpringに伝えます。
securityFilterChainメソッドでは、Webサイトのどのページに誰がアクセスできるかを設定しています。
.authorizeHttpRequests(): URLごとのアクセス制御設定を開始します。.requestMatchers("/", "/auth/register", "/auth/login").permitAll(): トップページ、ユーザー登録ページ、ログインページは、ログインしていない人も含め、誰でもアクセスできるように許可(permitAll)します。.anyRequest().authenticated(): 上記以外のすべてのページは、ログインしているユーザー(認証済みユーザー)のみがアクセスできるように設定します。.formLogin(): フォームを使用したログイン設定を開始します。.loginPage("/auth/login"): ログインページのURLを指定します。.defaultSuccessUrl("/posts", true): ログイン成功後にリダイレクトする先のURLを指定します。.logout(): ログアウトに関する設定を開始します。.logoutUrl("/auth/logout"): ログアウト処理を実行するURLを指定します。.logoutSuccessUrl("/auth/login?logout"): ログアウト成功後にリダイレクトする先のURLを指定します。
passwordEncoderメソッドでは、パスワードを安全に保存するための仕組み(ハッシュ化)を提供するBCryptPasswordEncoderを準備しています。@Beanアノテーションを付けることで、この仕組みをアプリケーションのどこからでも利用できるようになります。
変更点: ログイン・ユーザー登録画面のコントローラーを作成
/src/main/java/com/example/bbs/controller/AuthController.java1+package com.example.bbs.controller; 2+ 3+import com.example.bbs.model.User; 4+import com.example.bbs.service.CustomUserDetailsService; 5+import org.springframework.stereotype.Controller; 6+import org.springframework.ui.Model; 7+import org.springframework.web.bind.annotation.*; 8+ 9+@Controller 10+@RequestMapping("/auth") 11+public class AuthController { 12+ 13+ private final CustomUserDetailsService userDetailsService; 14+ 15+ public AuthController(CustomUserDetailsService userDetailsService) { 16+ this.userDetailsService = userDetailsService; 17+ } 18+ 19+ @GetMapping("/login") 20+ public String login() { 21+ return "auth/login"; 22+ } 23+ 24+ @GetMapping("/register") 25+ public String registerForm(Model model) { 26+ model.addAttribute("user", new User()); 27+ return "auth/register"; 28+ } 29+ 30+ @PostMapping("/register") 31+ public String register(@ModelAttribute User user) { 32+ userDetailsService.registerUser(user); 33+ return "redirect:/auth/login"; 34+ } 35+}
ログインとユーザー登録に関連するリクエストを処理するためのAuthControllerクラスを新しく作成します。
@RequestMapping("/auth")により、このクラス内のメソッドはすべて/authから始まるURL(例: /auth/login)に対応します。
login()メソッド:/auth/loginへのGETリクエストを受け取り、auth/login.htmlという名前のHTMLファイルを表示します。registerForm()メソッド:/auth/registerへのGETリクエストを受け取り、auth/register.htmlを表示します。register()メソッド:/auth/registerへのPOSTリクエスト(登録フォームからの送信)を受け取ります。フォームから送信されたユーザー名とパスワードをUserオブジェクトとして受け取り、後述するuserDetailsServiceのregisterUserメソッドを呼び出してユーザー登録処理を行います。処理完了後は、ログインページへリダイレクトします。
変更点: トップページ用のコントローラーを作成
/src/main/java/com/example/bbs/controller/HomeController.java1+package com.example.bbs.controller; 2+ 3+import org.springframework.stereotype.Controller; 4+import org.springframework.web.bind.annotation.GetMapping; 5+ 6+@Controller 7+public class HomeController { 8+ 9+ @GetMapping("/") 10+ public String home() { 11+ return "home"; 12+ } 13+}
アプリケーションのトップページ(/)へのアクセスを処理するためのHomeControllerクラスを新しく作成します。
homeメソッドが/へのGETリクエストを受け取り、home.htmlという名前のHTMLファイルを表示するだけのシンプルなコントローラーです。
変更点: ユーザー情報を格納するエンティティクラスを作成
/src/main/java/com/example/bbs/model/User.java1+package com.example.bbs.model; 2+ 3+import jakarta.persistence.*; 4+import lombok.Data; 5+ 6+@Entity 7+@Data 8+@Table(name = "users") 9+public class User { 10+ 11+ @Id 12+ @GeneratedValue(strategy = GenerationType.IDENTITY) 13+ private Long id; 14+ 15+ @Column(nullable = false, unique = true) 16+ private String username; 17+ 18+ @Column(nullable = false) 19+ private String password; 20+}
データベースに保存するユーザー情報を定義するためのUserクラス(エンティティ)を新しく作成します。
@Entity: このクラスがデータベースのテーブルに対応することを示します。@Table(name = "users"): 対応するテーブル名をusersに指定します。@Id:idフィールドがテーブルの主キーであることを示します。@GeneratedValue(strategy = GenerationType.IDENTITY): 主キーの値をデータベースが自動で生成・採番するように設定します。@Column(...):usernameやpasswordフィールドがテーブルのカラムに対応することを示します。nullable = falseはNOT NULL制約(空の値を許可しない)、unique = trueはUNIQUE制約(同じ値を重複させない)を意味します。
変更点: ユーザー情報を操作するリポジトリインターフェースを作成
/src/main/java/com/example/bbs/repository/UserRepository.java1+package com.example.bbs.repository; 2+ 3+import com.example.bbs.model.User; 4+import org.springframework.data.jpa.repository.JpaRepository; 5+ 6+import java.util.Optional; 7+ 8+public interface UserRepository extends JpaRepository<User, Long> { 9+ Optional<User> findByUsername(String username); 10+}
Userエンティティ(usersテーブル)に対するデータベース操作を簡単に行うためのUserRepositoryインターフェースを新しく作成します。
JpaRepositoryを継承することで、データの保存(save)、削除(delete)、全件検索(findAll)などの基本的なデータベース操作メソッドが自動的に使えるようになります。
findByUsernameというメソッドを定義するだけで、Spring Data JPAがメソッド名を解析し、「usernameカラムを条件にUserを1件検索する」というSQLを自動で生成してくれます。戻り値のOptional<User>は、ユーザーが見つからない可能性がある(nullかもしれない)ことを安全に扱うための型です。
変更点: ユーザー認証処理を行うサービスクラスを作成
/src/main/java/com/example/bbs/service/CustomUserDetailsService.java1+package com.example.bbs.service; 2+ 3+import com.example.bbs.model.User; 4+import com.example.bbs.repository.UserRepository; 5+import java.util.ArrayList; 6+import org.springframework.beans.factory.annotation.Autowired; 7+import org.springframework.security.core.userdetails.UserDetails; 8+import org.springframework.security.core.userdetails.UserDetailsService; 9+import org.springframework.security.core.userdetails.UsernameNotFoundException; 10+import org.springframework.security.crypto.password.PasswordEncoder; 11+import org.springframework.stereotype.Service; 12+ 13+@Service 14+public class CustomUserDetailsService implements UserDetailsService { 15+ 16+ @Autowired 17+ private final UserRepository userRepository; 18+ 19+ @Autowired 20+ private final PasswordEncoder passwordEncoder; 21+ 22+ // ...コンストラクタ... 23+ 24+ @Override 25+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 26+ User user = userRepository.findByUsername(username) 27+ .orElseThrow(() -> new UsernameNotFoundException("User not found")); 28+ return new org.springframework.security.core.userdetails.User( 29+ user.getUsername(), 30+ user.getPassword(), 31+ new ArrayList<>()); 32+ } 33+ 34+ public void registerUser(User user) { 35+ user.setPassword(passwordEncoder.encode(user.getPassword())); 36+ userRepository.save(user); 37+ } 38+}
Spring Securityがログイン認証を行う際に実際にユーザー情報を取得するためのCustomUserDetailsServiceクラスを新しく作成します。
UserDetailsServiceインターフェースを実装することで、Spring Securityがこのクラスを認証処理に利用できるようになります。loadUserByUsernameメソッド: Spring Securityからログインフォームで入力されたusernameを受け取ります。そのusernameを使ってUserRepositoryでデータベースを検索し、ユーザー情報を取得します。ユーザーが見つからない場合はエラー(UsernameNotFoundException)を投げます。見つかった場合は、そのユーザー名と(ハッシュ化された)パスワードをSpring Securityが扱えるUserDetailsオブジェクトに変換して返します。registerUserメソッド: ユーザー登録処理を担当します。ユーザーが入力した生のパスワードを、SecurityConfigで準備したpasswordEncoderを使ってハッシュ化(暗号化のようなもの)してから、UserRepositoryを使ってデータベースに保存します。これにより、データベースに安全な形式でパスワードを保管できます。
変更点: ログインページのHTMLファイルを作成
/src/main/resources/templates/auth/login.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Login</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">Login</h1> 9+ <form th:action="@{/auth/login}" method="post" class="mx-auto" style="max-width: 400px;"> 10+ <div class="mb-3"> 11+ <label for="username" class="form-label">UserName</label> 12+ <input type="username" name="username" class="form-control" id="username" required> 13+ </div> 14+ <div class="mb-3"> 15+ <label for="password" class="form-label">Password</label> 16+ <input type="password" name="password" class="form-control" id="password" required> 17+ </div> 18+ <button type="submit" class="btn btn-primary w-100">Login</button> 19+ </form> 20+</main>
ログイン画面の見た目を定義するlogin.htmlファイルを新しく作成します。
<form>タグで、入力されたユーザー名とパスワードをどこに送信するかを指定しています。th:action="@{/auth/login}"は、このフォームの送信先が/auth/loginであることを示し、method="post"でPOSTリクエストとして送信することを指定しています。これはSpring Securityがデフォルトで待ち受けているログイン処理のURLです。
<input>タグのname属性がusernameとpasswordになっている点も重要で、これによりSpring Securityが自動的にユーザー名とパスワードを認識してくれます。
変更点: ユーザー登録ページのHTMLファイルを作成
/src/main/resources/templates/auth/register.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Register</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">Register</h1> 9+ <form th:action="@{/auth/register}" method="post" class="mx-auto" style="max-width: 400px;"> 10+ <div class="mb-3"> 11+ <label for="username" class="form-label">UserName</label> 12+ <input type="username" name="username" class="form-control" id="username" required> 13+ </div> 14+ <div class="mb-3"> 15+ <label for="password" class="form-label">Password</label> 16+ <input type="password" name="password" class="form-control" id="password" required> 17+ </div> 18+ <button type="submit" class="btn btn-success w-100">Register</button> 19+ </form> 20+</main>
ユーザー登録画面の見た目を定義するregister.htmlファイルを新しく作成します。
構成はログインページとほぼ同じですが、<form>タグの送信先がth:action="@{/auth/register}"になっており、AuthControllerのregisterメソッドに対応しています。ユーザーが入力したusernameとpasswordは、このフォームを通じてサーバーに送信されます。
変更点: トップページのHTMLファイルを作成
/src/main/resources/templates/home.html1+<!DOCTYPE html> 2+<html layout:decorate="layout/layout"> 3+<head> 4+ <title layout:fragment="title">Home</title> 5+</head> 6+ 7+<main layout:fragment="content"> 8+ <h1 class="text-center">掲示板アプリへようこそ</h1> 9+ <div class="text-center"> 10+ <a href="/auth/login" class="btn btn-primary">Login</a> 11+ <a href="/auth/register" class="btn btn-secondary">Register</a> 12+ </div> 13+</main>
ログインしていないユーザーが最初に訪れるトップページの見た目を定義するhome.htmlファイルを新しく作成します。
このページには、ログインページへのリンクとユーザー登録ページへのリンクが設置されています。
変更点: ログイン状態に応じた表示の切り替えを実装
/src/main/resources/templates/layout/layout.html1 <html xmlns:th="http://www.thymeleaf.org" 2+ xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6" 3 xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"> 4 5 <head> 6 </li> 7+ <li class="nav-item" sec:authorize="!isAuthenticated()"> 8+ <a class="nav-link" href="/auth/login">Login</a> 9+ </li> 10+ <li class="nav-item" sec:authorize="!isAuthenticated()"> 11+ <a class="nav-link" href="/auth/register">Register</a> 12+ </li> 13+ <li class="nav-item" sec:authorize="isAuthenticated()"> 14+ <form action="/auth/logout" method="post"> 15+ <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> 16+ <button type="submit" class="nav-link active">Logout</button> 17+ </form> 18+ </li> 19 </ul> 20 </div> 21 </div>
全ページ共通のレイアウトを定義するlayout.htmlファイルを変更し、ログイン状態によってナビゲーションバーの表示を切り替えるようにします。
xmlns:sec="...":pom.xmlで追加したthymeleaf-extras-springsecurity6ライブラリの機能を使うために、HTMLの<html>タグに名前空間の宣言を追加します。sec:authorize="!isAuthenticated()": 「認証されていない(ログインしていない)場合」にのみ、その要素(この場合はLoginとRegisterのリンク)を表示するという意味です。sec:authorize="isAuthenticated()": 「認証されている(ログインしている)場合」にのみ、その要素(この場合はLogoutボタン)を表示するという意味です。- ログアウトフォーム: ログアウトは、
/auth/logoutに対してPOSTリクエストを送ることで実行されます。inputタグは、CSRF(クロスサイトリクエストフォージェリ)というセキュリティ攻撃を防ぐためのおまじないです。
おわりに
今回はSpring Securityを導入し、掲示板アプリケーションに不可欠なログイン認証機能を実装しました。SecurityConfigクラスでURLごとのアクセス制御を一元管理し、BCryptPasswordEncoderでパスワードを安全な形式に変換してからデータベースへ保存する方法を学びました。また、UserDetailsServiceを実装して実際の認証処理を担わせ、Thymeleafのsec:authorize属性を使えばログイン状態に応じて画面表示を簡単に切り替えられることも確認しました。