はじめに
JavaにはphpのLaravelのようにコマンドひとつでログイン機能のテンプレートを作成する機能はありません。ログイン機能を実装するだけならば、SpringBootのSpring Securityを作成するプロジェクトに導入することで、固定されたユーザー名とパスワードでのログイン機能が付与されますが、データベースと連携させるためにはいくつかの知識が必要になったので、chatGPT(GPT4)を活用してログイン機能を作成した際の記録を備忘録としてこの記事を残します
環境について
- Javaのバージョン: 17
- Spring Bootのバージョン: 2.5.4
- MacOS
- Eclipse(IDE)
chatGPTの知識は2021年9月までの情報に基づいているためその時点で最新のバージョン情報で作業を実行しております。
Javaでログイン機能を実装する方法について
Javaのフレームワーク(Spring Boot)にはプロジェクトに導入するだけでプロジェクトにログイン機能を付与するSpring Security
という機能が存在します。
次のようにHTMLが存在している場合、Spring Securityを実装するだけでログインを経由しなければ到達できなくなります
Spring Securityの導入方法
SpringBootの導入方法は複数存在します。
以下の導入方法から1つ選択します
新規プロジェクトを作成する場合
Spring SecurityはEclipseの場合、新しいプロジェクトを作成する際に依存関係にチェックを入れることで自動的にSpring Securityの依存関係を設定してくれます。
依存関係とは特定の機能を呼び出す
設定です
Spring Bootには元々、データベースに接続したり、セキュリティを設定したり、Webアプリケーションを作成する機能が含まれており、それを呼び出すためには依存関係
を設定する必要があります
既存プロジェクトに追加する場合
既存のプロジェクトにSpring Securityの依存関係を追加するためには、プロジェクトのビルドツール(MavenやGradle)の設定ファイルを編集します。
MavenとGradleはプロジェクトを新規で作成した際にどちらか設定されています。
プロジェクトにbuild.gradleファイル
が存在すればGradle
pom.xmlファイル
が存在すればMavenです
Gradleの場合
- プロジェクトのビルドファイル(build.gradle)を開きます。
- dependenciesセクションに、Spring Securityの依存関係を追加します。
- ファイルを保存します
dependencies {
// 他の依存関係の設定
implementation 'org.springframework.boot:spring-boot-starter-security'
}
- 依存関係を更新するために、ビルドツールを実行します。
- ターミナルでプロジェクトのルートディレクトリに移動
- 以下のコマンドを実行します。
gradle build
Mavenの場合
-
プロジェクトのpom.xmlファイル(Mavenの場合)を開きます。
-
セクション内に、Spring Securityの依存関係を追加します。以下の依存関係のブロックを追加してください。
<dependencies>
<!-- 他の依存関係の設定 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
-
ファイルを保存し、ビルドツールによる依存関係の更新を行います。
-
プロジェクトのルートディレクトリで次のコマンドを実行します。
mvn clean install
これによりSpring Securityの依存関係がプロジェクトに追加されます。
ログイン情報(アカウント)について
Spring Securityですでにhtmlを表示できる段階まで進んでいれば、そのURLにアクセスするとログイン画面が表示されるようになります。しかしデータベースと連携できていないのでログイン画面では以下の固定されたアカウントしか使用できません
- user名:user
- パスワード:コンソールに表示されるランダムな文字列
パスワードはハッシュ化されたランダムな数値
がコンソールに表示されるため、それをコピー&ペーストする必要があります
データベースとの接続について
SpringBootではデータベースと接続することで固定アカウント以外のアカウントを使用することができます
それにはプロジェクトおよびデータベースに対してさまざまな準備が必要になります
テーブルについて
一般的にログイン画面に必要な情報はユーザー名とパスワードなのでテーブル情報は次の通りになります
テーブル名:Users
名前 | タイプ |
---|---|
id | int(11) |
username | varchar(255) |
password | varchar(255) |
varchar(255) |
ログイン画面に直接関係のないemail
のカラムが含まれていますが、ログイン機能を実行するユーザー名(username)とパスワード(password)が存在すれば、他にカラムが存在しても問題ありません。
保存するデータはハッシュ化が必要
以下の写真のように、パスワード
が記号と数字の羅列
に変換されています。
結論、Spring Securityではパスワードはハッシュ化して保存することが推奨されており、データベース側に保存されたパスワードはハッシュ化されていないと利用できない
ようにデフォルトで設定されています
つまり、画像のアカウントでは赤枠以外のアカウントでのログインができません。
ちなみに赤枠のパスワードはroot4
とroot5
というキーワードをハッシュ化させたものです。ハッシュ化して保存する方法は後述で説明がありますが、実際に新規アカウント作成画面を用意しそこでアカウントを作成する方法になります
パスワードのハッシュ化はSpring Securityで実施される
Spring Securityにはパスワードを変換するパスワードエンコーダーという機能が存在し、パスワードのハッシュ化やパスワードの検証(一致確認)などの操作を簡単に行うことができます。ハッシュ化は、パスワードを暗号化し、元のパスワードを復元不可能な形式に変換するために使用されます。
パスワードエンコーダーを使用するには
パスワードエンコーダーはインターフェースです。具体的なハッシュ化アルゴリズムを実装するためのメソッドが含まれているため、使用するにはプロジェクトに専用のクラスを用意する必要があります(コードなどについては後述します)
ログイン機能を試す前に
前述したとおり、Spring Securityで実装したログイン機能を試すにはハッシュ化されたPWが必要です。よって、テーブルにあらかじめ平文でユーザー名とパスワードを保存していてもそのアカウントは使用できません。そのため、ログイン機能を試すためにはアカウント新規作成機能
も実装する必要があります。
プロジェクトに必要な最低限のファイル
Spring Securityを使ってログイン機能と新規アカウント作成画面を実行するためには、以下のような一連のファイルが必要です
ファイル名やディレクトリ名に決まりはありません、以下は一般的な名前になります
.
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com
│ │ │ │ ├── example
│ │ │ │ │ ├── controller
│ │ │ │ │ │ ├── LoginController.java
│ │ │ │ │ │ └── RegisterController.java
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── User.java
│ │ │ │ │ │ └── UserDto.java
│ │ │ │ │ ├── service
│ │ │ │ │ │ ├── UserService.java
│ │ │ │ │ │ └── UserServiceImpl.java
│ │ │ │ │ └── config
│ │ │ │ │ ├── SecurityConfig.java
│ │ │ │ │ └── PasswordEncoderConfig.java
│ │ ├── resources
│ │ │ ├── templates
│ │ │ │ ├── login.html
│ │ │ │ └── register.html
│ │ │ └── application.properties
│ ├── test
├── build.gradle (またはpom.xml、もしMavenを使用している場合)
└── ...
モデルクラス (User.java)
ユーザーのデータを表現するためのクラス。状況によってはエンティティクラスもしくはDTOとも呼ばれます。ここでは、例えばデータベースのテーブルにUsersテーブルが存在する場合はUser.java
というファイルを作成し、ユーザー名とパスワードを格納する変数を用意することで、テーブルとJavaを紐付けすることができます
データアクセスオブジェクト (UserRepository.java)
ユーザーデータの保存や検索を行うためのインターフェース。Spring Data JPAを使用して、データベース操作を簡単に実行できます。
JPAとはテーブルのカラムとJavaの変数を紐付けする機能です。使用するにはSpring Securityと同じように依存関係の設定が必要になります
サービスクラス (UserService.java)
ユーザーに関連するビジネスロジックを実装するクラス。例えば、新規ユーザーの登録や既存ユーザーの検索などの処理をここに作成します
コントローラクラス (LoginController.java)
HTTPリクエストを処理するためのクラス。ログインや新規登録のリクエストを処理し、適切なビューの表示やメソッドの実行を行います
ビューファイル (login.html, register.html)
ログインフォームや新規登録フォームをユーザーに表示するためのHTMLテンプレート。
セキュリティ設定クラス (SecurityConfig.java)
Spring Securityの設定を定義するクラス。ログインメカニズムやパスワードエンコーディング、認可のルールなどを設定します。
データベース接続設定
データベースの接続情報(application.properties)
連携するデータベースのURLやポート番号などは
application.properties
に記述します
よって接続の設定は次のようになりました
# データベースに接続するためのURLを設定します。ここではMySQLのデータベース「LibraryManagementSystem」に接続するためのURLを指定しています。
spring.datasource.url=jdbc:mysql://localhost:8889/LibraryManagementSystem?useSSL=false&serverTimezone=UTC
# データベースに接続するためのユーザー名を設定します。ここでは'root'というユーザー名を指定しています。
spring.datasource.username=root
# データベースに接続するためのパスワードを設定します。ここでは'root'というパスワードを指定しています。
spring.datasource.password=root
# データベースに接続するためのドライバークラス名を設定します。ここではMySQL用のドライバーを指定しています。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# データベースとエンティティの間のテーブル作成・更新・削除の動作を制御します。ここでは何もしないことを示す'none'を指定しています。
spring.jpa.hibernate.ddl-auto=none
# 実行されるSQLをログに出力するかどうかを制御します。ここでは出力するために'true'を指定しています。
spring.jpa.show-sql=true
# 使用するデータベースの種類に合わせたSQLを生成するための方言(Dialect)を指定します。ここではMySQL用の方言を指定しています。
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# SQLをログに出力する際に整形するかどうかを制御します。ここでは整形するために'true'を指定しています。
spring.jpa.properties.hibernate.format_sql=true
# Springのログレベルを設定します。ここではDEBUGレベルのログを出力するように設定しています。
logging.level.org.springframework=DEBUG
接続設定以外にもSQLのログをコンソールに出力する設定など
全てのコードが必須であるわけではありません。
ファイルの場所(構造図)
- application.properties
project-directory # プロジェクトのルートディレクトリ
├── src # ソースコードを格納するディレクトリ
│ ├── main # メインのソースコードとリソースを格納するディレクトリ
│ │ ├── java # Javaソースコードを格納するディレクトリ
│ │ │ └── com # ここからパッケージ構造が始まる
│ │ │ └── example # 例えば 'com.example' パッケージ
│ │ │ └── ... # その他のパッケージとJavaクラス
│ │ └── resources # リソースファイルを格納するディレクトリ
│ │ ├── static # 静的リソースを格納するディレクトリ(CSSやJavaScriptファイルなど)
│ │ ├── templates # Thymeleafなどのテンプレートエンジンによるビューファイルを格納するディレクトリ
│ │ └── application.properties # Spring Bootの設定を格納するファイル
│ └── test # テストのソースコードとリソースを格納するディレクトリ
└── ... # その他のファイルとディレクトリ(例:pom.xml, .gitignore, README.mdなど)
必要なファイル(コード)
構造図(例)
project-directory # プロジェクトのルートディレクトリ
├── src # ソースコードを格納するディレクトリ
│ ├── main # メインのソースコードとリソースを格納するディレクトリ
│ │ ├── java # Javaソースコードを格納するディレクトリ
│ │ │ └── com # ここからパッケージ構造が始まる
│ │ │ └── example # 例えば 'com.example' パッケージ
│ │ │ ├── LibraryManagementSystemApplication.java # Spring Bootアプリケーションのエントリーポイント
│ │ │ ├── controller # controller パッケージ
│ │ │ │ ├── LoginController.java # ログインに関する処理を行うコントローラ
│ │ │ │ └── RegisterController.java # 登録に関する処理を行うコントローラ
│ │ │ ├── model # model パッケージ
│ │ │ │ ├── User.java # Userエンティティのモデルクラス
│ │ │ │ └── UserDto.java # Userエンティティに関するデータ転送オブジェクト (DTO)
│ │ │ ├── repository # repository パッケージ
│ │ │ │ └── UserRepository.java # Userエンティティに関するリポジトリインターフェース
│ │ │ └── service # service パッケージ
│ │ │ ├── UserPrincipal.java # ユーザーの認証情報を保持するクラス
│ │ │ └── UserService.java # Userエンティティに関するサービスクラス
│ │ └── resources # リソースファイルを格納するディレクトリ
│ └── test # テストのソースコードとリソースを格納するディレクトリ
└── ... # その他のファイルとディレクトリ(例:pom.xml, .gitignore, README.mdなど)
※仕様によってはmodelディレクトリをentityディレクトリと名称する場合があります
LibraryManagementSystemApplication.java
このプロジェクト名がLibraryManagementSystemなのでファイル名にプロジェクト名が名称付けされています
Spring Bootアプリケーションのメインクラスで、アプリケーションのエントリーポイントとなるmainメソッドを持っています。mainメソッドではSpringApplication.runを呼び出してアプリケーションを起動しています。@SpringBootApplicationアノテーションは、このクラスがSpring Bootアプリケーションであることを示しています。
package com.example; // このファイルが属するパッケージ(フォルダ)
// 必要なクラスをインポートします
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // Spring Bootアプリケーションであることを示します
public class LibraryManagementSystemApplication { // アプリケーションのメインクラス
public static void main(String[] args) { // Javaアプリケーションのエントリーポイント(最初に実行されるメソッド)
SpringApplication.run(LibraryManagementSystemApplication.class, args); // Spring Bootアプリケーションを起動します
}
}
User.java
UsersテーブルとJavaの変数(フィールド)を紐付けます
package com.example.model; // このファイルが属するパッケージ(フォルダ)
// 以下の部分はデータベースとのやり取りに必要な情報を持つためのものです。
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity // これはデータベースのテーブルを表しています
@Table(name = "Users") // このクラスが対応するテーブルの名前は "Users" です
public class User {
@Id // これが各ユーザを一意に識別するためのIDとなります
@GeneratedValue(strategy = GenerationType.IDENTITY) // IDは自動的に増加します
@Column(name = "id") // データベースに合わせてカラム名を修正
private Integer id;
@Column(name = "username", nullable = false, unique = true) // "username" カラム。各ユーザーのユーザー名を表します。同じ名前のユーザーは存在できません
private String username;
@Column(name = "password", nullable = false) // "password" カラム。ユーザーのパスワードを表します
private String password;
@Column(name = "email", nullable = false, unique = true) // "email" カラム。ユーザーのメールアドレスを表します。同じメールアドレスのユーザーは存在できません
private String email;
// 以下は各値を取得するためのメソッド(ゲッター)です。
public Integer getId() {
return this.id;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public String getEmail() {
return this.email;
}
// 以下は各値を設定するためのメソッド(セッター)です。
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEmail(String email) {
this.email = email;
}
}
UserDto.java
ユーザーによる入力データを保持するためのクラスです。ユーザーがフォームに入力したデータが、このクラスのインスタンスに変換され、アプリケーション内で利用されます。@NotEmptyアノテーションは入力値チェックを行い、対象のフィールドが空でないことを確認します。これにより、ユーザーが空欄のままデータを送信するのを防ぎます。
package com.example.model; // このファイルが属するパッケージ(フォルダ)
// 入力チェックをするためのツールをインポートしています。
import javax.validation.constraints.NotEmpty;
public class UserDto { // ユーザーのデータを扱うためのクラス
@NotEmpty // ユーザー名は空であってはならないというルール
private String username; // ユーザー名を保存するための場所
@NotEmpty // パスワードは空であってはならないというルール
private String password; // パスワードを保存するための場所
@NotEmpty // メールアドレスは空であってはならないというルール
private String email; // メールアドレスを保存するための場所
// 以下は各値を取得するためのメソッド(ゲッター)です。
public String getUsername() {
return username; // ユーザー名を返す
}
public void setUsername(String username) {
this.username = username; // ユーザー名を設定する
}
public String getPassword() {
return password; // パスワードを返す
}
public void setPassword(String password) {
this.password = password; // パスワードを設定する
}
public String getEmail() {
return email; // メールアドレスを返す
}
public void setEmail(String email) {
this.email = email; // メールアドレスを設定する
}
}
UserRepository.java
このインターフェースは、Userエンティティに対してデータベース操作を行うためのリポジトリを定義しています。JpaRepositoryを継承することで、様々なデータベース操作を自動的に提供します(例えば、保存、更新、削除、検索など)。findByUsernameメソッドは、指定したユーザー名でユーザーを検索するためのメソッドで、Spring Data JPAが自動的にその実装を生成します。
package com.example.repository; // このファイルが属するパッケージ(フォルダ)
// 必要なツールをインポートしています
import org.springframework.data.jpa.repository.JpaRepository;
// Userクラスを使うためにインポートしています
import com.example.model.User;
// UserRepositoryというインターフェースを作成します。JpaRepositoryを拡張して、UserオブジェクトとそれらのIDとしてLong型を扱えるようにします。
public interface UserRepository extends JpaRepository<User, Long> {
// ユーザー名でユーザーを探すメソッド。ユーザー名をパラメータとして渡すと、そのユーザー名を持つユーザーをデータベースから探して返します。
User findByUsername(String username);
}
UserPrincipal.java
Spring SecurityのUserDetailsインターフェースを実装しており、認証(ログイン)時にSpring Securityから使われます。ユーザーの情報(ユーザー名、パスワード、権限等)を保持し、それらの情報を提供します。
package com.example.service; // このファイルが属するパッケージ(フォルダ)
// 必要なツールをインポートしています
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
// Userクラスを使うためにインポートしています
import com.example.model.User;
// UserDetailsインターフェースを実装したUserPrincipalというクラスを作成します。これはSpring Securityでユーザー情報を扱うためのクラスです。
public class UserPrincipal implements UserDetails {
private User user; // Userオブジェクトを保持します。
// コンストラクタでUserオブジェクトを受け取り、それをこのクラスのuserにセットします。
public UserPrincipal(User user) {
this.user = user;
}
// ユーザーに与えられる権限を返します。ここでは全てのユーザーに"USER"という権限を与えています。
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("USER"));
}
// Userオブジェクトのパスワードを返します。
@Override
public String getPassword() {
return user.getPassword();
}
// Userオブジェクトのユーザー名を返します。
@Override
public String getUsername() {
return user.getUsername();
}
// アカウントが有効期限切れでないことを示すために、常にtrueを返します。
@Override
public boolean isAccountNonExpired() {
return true;
}
// アカウントがロックされていないことを示すために、常にtrueを返します。
@Override
public boolean isAccountNonLocked() {
return true;
}
// 資格情報(ここではパスワード)が有効期限切れでないことを示すために、常にtrueを返します。
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// アカウントが有効であることを示すために、常にtrueを返します。
@Override
public boolean isEnabled() {
return true;
}
}
UserService.java
package com.example.service; // このファイルが属するパッケージ(フォルダ)
// 必要なクラスをインポートします
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.model.User;
import com.example.model.UserDto;
import com.example.repository.UserRepository;
@Service // このクラスがサービス層のクラスであることを示します
public class UserService implements UserDetailsService { // UserDetailsServiceインターフェースを実装しています
@Autowired // Springが自動的にUserRepositoryの実装を注入します
private UserRepository userRepository;
@Autowired // Springが自動的にPasswordEncoderの実装を注入します
private PasswordEncoder passwordEncoder;
@Override // UserDetailsServiceインターフェースのメソッドを上書きします
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username); // ユーザー名でユーザーを検索します
if (user == null) {
throw new UsernameNotFoundException("User not found"); // ユーザーが見つからない場合、例外をスローします
}
return new UserPrincipal(user); // ユーザーが見つかった場合、UserPrincipalを作成し返します
}
//新たにメソッドを追加します
public User findByUsername(String username) {
return userRepository.findByUsername(username); // ユーザー名でユーザーを検索し返します
}
@Transactional // トランザクションを開始します。メソッドが終了したらトランザクションがコミットされます。
public void save(UserDto userDto) {
// UserDtoからUserへの変換
User user = new User();
user.setUsername(userDto.getUsername());
// パスワードをハッシュ化してから保存
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
user.setEmail(userDto.getEmail());
// データベースへの保存
userRepository.save(user); // UserRepositoryを使ってユーザーをデータベースに保存します
}
}
SecurityConfig.java
Spring Securityの設定を定義するクラスです。各メソッドがセキュリティ関連の設定を提供します。パスワードのハッシュ化、アクセス制御、ログインページの設定、ログアウトの設定などを行っています。
package com.example.Config; // このファイルが属するパッケージ(フォルダ)
// 必要なクラスやインターフェースをインポートします
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;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration // このクラスは設定クラスであることを示します
@EnableWebSecurity // Webセキュリティを有効にすることを示します
public class SecurityConfig { // セキュリティ設定のクラス
@Bean // このメソッドの返り値をSpringのBeanとして登録します
public PasswordEncoder passwordEncoder() { // パスワードエンコーダー(パスワードのハッシュ化)を提供するメソッド
return new BCryptPasswordEncoder(); // パスワードをBCrypt方式でハッシュ化するエンコーダーを返します
}
@Bean // このメソッドの返り値をSpringのBeanとして登録します
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // セキュリティフィルタチェーンを定義するメソッド
http
.authorizeRequests(authorizeRequests -> // 認証リクエストを設定します
authorizeRequests
.antMatchers("/login", "/register").permitAll() // "/login"と"/register"へのリクエストは認証なしで許可します
.anyRequest().authenticated() // それ以外の全てのリクエストは認証が必要です
)
.formLogin(formLogin -> // フォームベースのログインを設定します
formLogin
.loginPage("/login") // ログインページのURLを設定します
.permitAll() // ログインページは認証なしで許可します
)
.logout(logout -> // ログアウトを設定します
logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // ログアウトのリクエストURLを設定します
);
return http.build(); // 上記の設定を反映してHttpSecurityオブジェクトをビルドします
}
// 他のセキュリティ設定が必要な場合は、ここに追加します
}
LoginController.java
Webコントローラーで、ユーザーのHTTPリクエストを処理し、適切なレスポンスを返します。@Controllerアノテーションは、このクラスがWebコントローラーであることを示します。@GetMappingアノテーションは、特定のURLに対するGETリクエストを処理するメソッドを指定します。各メソッドの戻り値は、表示するHTMLページの名前、またはリダイレクト先のURLです。
package com.example.controller; // このファイルが属するパッケージ(フォルダ)
// 必要なクラスをインポートします
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller // このクラスがWebコントローラーであることを示します
public class LoginController {
@GetMapping("/login") // "/login"というURLに対するGETリクエストを処理します
public String login() {
return "login"; // login.htmlを表示します
}
@GetMapping("/") // ルートURL ("/") に対するGETリクエストを処理します
public String redirectToIndex() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 現在のユーザーの認証情報を取得します
if (authentication != null && authentication.isAuthenticated()) { // ユーザーがログインしている場合
return "redirect:/index"; // "/index"にリダイレクトします
}
return "redirect:/login"; // ユーザーがログインしていない場合、"/login"にリダイレクトします
}
@GetMapping("/index") // "/index"というURLに対するGETリクエストを処理します
public String index() {
return "index"; // index.htmlを表示します
}
}
RegisterController.java
ユーザーの登録に関するリクエストを処理するWebコントローラーです。registerForm()メソッドは、登録フォームを表示します。register(@ModelAttribute UserDto userDto)メソッドは、ユーザーの登録を処理します。ユーザー名がすでに存在する場合は、登録画面に戻ります。存在しない場合は、新しいユーザーを保存し、ログイン画面にリダイレクトします。
package com.example.controller; // このファイルが属するパッケージ(フォルダ)
// 必要なクラスをインポートします
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.ModelAndView;
import com.example.model.User;
import com.example.model.UserDto;
import com.example.service.UserService;
@Controller // このクラスがWebコントローラーであることを示します
public class RegisterController {
// Spring が自動的に UserService の実装を注入します
@Autowired
private UserService userService;
@GetMapping("/register") // "/register"というURLに対するGETリクエストを処理します
public ModelAndView registerForm() {
ModelAndView mav = new ModelAndView(); // ModelAndViewオブジェクトを作成します
mav.addObject("user", new UserDto()); // 新しいUserDtoオブジェクトを"ユーザー"という名前で追加します
mav.setViewName("register"); // 表示するビュー(HTMLファイル)の名前を"register"に設定します
return mav; // ModelAndViewオブジェクトを返します
}
@PostMapping("/register") // "/register"というURLに対するPOSTリクエストを処理します
public String register(@ModelAttribute UserDto userDto) {
User existing = userService.findByUsername(userDto.getUsername()); // ユーザー名で既存のユーザーを検索します
if(existing != null){
// ユーザーが既に存在する場合の処理
return "register"; // ユーザーが存在するため、再度登録画面を表示します
}
userService.save(userDto); // ユーザーが存在しない場合、新しいユーザーを保存します
return "login"; // 登録が成功した場合、ログイン画面を表示します
}
}
HTML
今回は以下の画面に絞っています
- ログイン画面 (login.html)
- 新規アカウント作成画面 (register.html)
- ホーム画面 (index.html)
ファイル構造図
LibraryManagementSystem
├── src
│ ├── main
│ │ └── resources
│ │ └── templates
│ │ ├── login.html
│ │ ├── register.html
│ │ └── index.html
└── ...
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<div th:if="${param.error}">
<p>Invalid username and password.</p>
</div>
<div th:if="${param.logout}">
<p>You have been logged out.</p>
</div>
<form th:action="@{/login}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div>
<label>Username: </label>
<input type="text" name="username" required/>
</div>
<div>
<label>Password: </label>
<input type="password" name="password" required/>
</div>
<button type="submit">Login</button>
</form>
<p>
Don't have an account? <a th:href="@{/register}">Register</a> <!-- 新規アカウント作成ページへのリンクを追加 -->
</p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>テスト</h1>
</body>
</html>
【重要】フォームのデータ送信について(CSRF)
Spring Boot
のSpring Security
では、デフォルトでCSRF(Cross-Site Request Forgery)対策が有効になっています。これは、ウェブアプリケーションがCSRF攻撃から守られるようにするためのものです。
具体的には、フォームからデータをPOSTで送信する際に、一緒にCSRFトークンという特殊な値を送信します。サーバー側では、このCSRFトークンが正しいものであることを確認し、そうでなければリクエストを拒否します。これにより、ユーザーが意図しない操作を行うことを防いでいます。
なお、このCSRF対策を無効にすることも可能ですが、その場合はウェブアプリケーションがCSRF攻撃に対して無防備になるため、通常は無効にしないことが推奨されます。
CSRF攻撃とは
悪意のあるウェブサイトが、ユーザーがログインしたままの別のサイトで操作を行うこと。ユーザー自身が知らないうちに情報を書き換えられたり、削除されたりする可能性があります。
つまり何が言いたいのか
新規アカウント作成のためデータの送信先を次のように指定してもCSRF対策
によりサーバーで通信が阻害された結果、データベースに通信が届かず新しいアカウントを登録することができません
<form action="/register" method="post">
実際にブラウザで操作すると途中まで送信はできるためにコンソールにエラーが発生しません
しかしデータベースにアカウントが追記されないため、理由が分かりにくい原因の一つです
フォームのデータの送信先について(CSRF対策)
デフォルトで設定されているCSRF対策の機能を無効化
すれば通常の送信先でも送信できますが、推奨はされません
そこで以下の書き方をすることでCSRF機能に通信が阻害されることなく、データベースに追加するアカウントを送信することができます
<html xmlns:th="http://www.thymeleaf.org">
th:action="@{/somepath}" のように使う
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Register</title>
</head>
<body>
<h2>Register</h2>
<form th:action="@{/register}" method="post">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" required><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" required><br>
<label for="email">Email:</label><br>
<input type="email" id="email" name="email" required><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
これによりアカウント情報がデータベースまで正しく送信されアカウント追加をすることができます
追加したアカウントのパスワードはSecurityConfigクラスによりハッシュ化され登録されます
ブラウザに入力するログイン用のパスワードは、普通にハッシュ化前の情報でログインできます
GitHub
あとがき
今回作成したファイルはファイル同士が密接に絡んでおり、ファイルが欠けているとどこかにエラーが発生するため、全て揃って初めてエラーを回避することができます。また、使用するSpringBoot,Java,Spring Securityのバージョンによってもエラーが出る傾向がありました。それは当たり前だと言われるのかもしれませんが、Eclipseに表示されるエラーが解決できず四苦八苦した理由がそれなので覚える必要があると感じました