前回の記事では、Spring Boot(Spring Secutiry)一番シンプルなログイン認証の実装について紹介しました。
今回はSpring Boot(Spring Security)で、データベースを利用したログイン認証を実装する方法を紹介します。また、ログインアカウントの権限をチェックして、リンクの表示/非表示、アクセスできるページの設定方法について解説します!
前提条件
環境は以下の通りです。
- Kotlin
- Spring Boot 3.4.1
- PostgreSQL 17.2(Docker)【必要に応じて変更可能です】
- データベースのマイグレーションにFlywayを使用
Flywayはデータベーススキーマのバージョン管理を簡単に行うためのツールです。
Flywayはsrc/main/resources/db/migration
ディレクトリに配置したSQLファイルを自動的に検出し、自動でSQLを実行してくれます。
SQLファイル名の命名規則があり、V<バージョン番号>__<説明>.sql
という決まりがあり、V1__Create_users_table.sql
のように記述します。バージョン番号が若い方から自動的にFlywayが実行してくれます。
Flywayについての参考サイトはコチラ
Spring Bootプロジェクトの作成
Spring Bootのプロジェクトを作る際、以下の依存関係を追加します。
- Spring Web: RESTfulなWebサービスやMVCアーキテクチャのWebアプリケーションを構築するために必要です。
- Spring Security: ユーザー認証や認可を実装できるセキュリティフレームワークです。
- Spring Data JPA: データベース操作を簡略化するためのツールです。
- PostgreSQL Driver: PostgreSQLデータベースとの接続を可能にします。
- Flyway Migration: データベーススキーマのバージョン管理を自動化します。
- Thymeleaf: HTMLテンプレートエンジンです。
- Docker Compose Support: Dockerを利用してアプリケーション環境を簡単に構築するためのサポートです(任意)。
- Spring Boot DevTools: 開発時のホットリロードや便利なデバッグ機能を提供します(任意)。
build.gradle.kts
に以下のような依存関係になります。
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-database-postgresql")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
developmentOnly("org.springframework.boot:spring-boot-devtools")
providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("org.springframework.boot:spring-boot-docker-compose")
}
データベースの準備
Dockerでサーバーを用意
今回はDockerでPostgreSQLを用意します。プロジェクトのディレクトリ直下にcompose.yml
を作成します。(IntelliJでプロジェクトを作成した場合は自動で生成されます)Docker Compose Support
を有効にしていると、Spring Bootを起動したときに、自動でDockerコンテナを生成してくれます。
services:
db:
image: 'postgres:17.2'
ports:
- '5432:5432'
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: 'password'
POSTGRES_DB: 'db'
volumes:
- 'postgres_data:/var/lib/postgresql/data'
volumes:
postgres_data:
# Dockerのボリューム機能を利用して、PostgreSQLのデータの永続化を行います
# 永続化を行うことで、コンテナを再起動してもデータが保持されます。
# これにより、環境のセットアップや再構築時のデータ損失を防ぐことができます。
データベーステーブルを作成
ユーザー情報を格納するためのテーブルを用意します。このテーブルのポイントは以下の通りです:
login_id
フィールドにはユニーク制約を設け、重複を防ぎます。-
password
フィールドにはBCryptでハッシュ化されたパスワードを保存します。 -
is_admin
フィールドでユーザーが管理者か一般ユーザーかを区別します。
DBマイグレーションにFlywayをつかっているので、SQLファイルをsrc/main/resources/db/migration
にV1.0__Create_user_table.sql
というファイル名で用意します。
アプリケーションを実行したときに、自動でこのSQLが実行されます。
-- ユーザーテーブル
CREATE TABLE users (
id SERIAL PRIMARY KEY,
login_id VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
user_name VARCHAR(255) NOT NULL,
is_admin BOOLEAN DEFAULT FALSE,
);
-- 動作確認用のデータ。管理者アカウントとユーザー01というアカウントを初期データにします。
INSERT INTO users (login_id, user_name, password, is_admin) VALUES
-- passwordは`admin`のハッシュ値(BCrypt)です
('admin', '管理者', '$2a$10$tFcbZ9WI/703ubavq9WGjee1NZrOdo6DSfiroD1NwRC/bRWS1AhJm', TRUE)
-- passwordは`user01`のハッシュ値(BCrypt)です
, ('user01', 'ユーザー01', '$2a$10$33odWXTBpUDrrXBuIym9I.JPd2aQAVaa2MX7TEp69GWDPPO1BOIhO', FALSE)
;
BCryptのハッシュ計算はコチラのサイトを利用しました:BCryptハッシュの計算
設定ファイルの修正
PostgreSQLに接続するために設定ファイルを更新します。以下がapplication.yml
の例です。もしapplication.properties
というファイルがあれば、application.yml
に名前を変更してください。
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/db
username: user
password: password
エンティティクラス、リポジトリの実装
usersテーブルにアクセスするためのUserエンティティクラスを作成します。
/**
* ユーザー情報を保持するエンティティクラス
*/
@Entity
@Table(name = "users")
data class User(
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(name = "login_id", nullable = false)
var loginId: String = "",
@Column(name = "password", nullable = false)
var loginPassword: String = "",
@Column(name = "user_name", nullable = false)
var displayName: String = "",
@Column(name = "is_admin", nullable = false)
var isAdmin: Boolean = false
): UserDetails {
/**
* ユーザーの権限や役割を返します
* 今回は単純にユーザーか管理者かを返します
* 管理者の場合はROLE_ADMIN、ユーザーの場合はROLE_USERを返します。
*
* 接頭辞にROLE_をつけることで、hasRoleで役割をチェックができます。
* 例:hasRole("ADMIN")でROLE_ADMINの権限を持っているかチェックできます。
*
* ただ、hasAuthorityで権限をチェックすることも可能。
* 例:hasAuthority("ROLE_ADMIN")でROLE_ADMINの権限を持っているかチェックできます。
*/
override fun getAuthorities(): Collection<GrantedAuthority> {
val role = if (isAdmin) "ROLE_ADMIN" else "ROLE_USER"
return mutableListOf(SimpleGrantedAuthority(role))
}
/**
* ログイン認証時に使用するパスワードを返します
*/
override fun getPassword() = loginPassword
/**
* ログイン認証時に使用するユーザー名を返します
*/
override fun getUsername() = loginId
/**
* アカウントの有効期限が切れているかどうかを返します
* falseを返すとログインできません
*/
override fun isAccountNonExpired() = true
/**
* アカウントがロックされているかどうかを返します
* falseを返すとログインできません
*/
override fun isAccountNonLocked() = true
/**
* 資格情報の有効期限が切れているかどうかを返します
* falseを返すとログインできません
*/
override fun isCredentialsNonExpired() = true
/**
* ユーザーが有効かどうかを返します
* falseを返すとログインできません
*/
override fun isEnabled() = true
}
次に、リポジトリを定義します。
リポジトリは、Spring Data JPAを利用してデータベースとやり取りを行うためのコンポーネントです。これにより、データの保存、取得、更新、削除などの操作を簡潔に記述でき、データベースアクセスに関する詳細なコードを記述する必要がなくなる非常に便利な機能です!
SQLを一切書いていませんが、findByLoginId
メソッドで引数に指定したloginId
に一致するUserエンティティを検索してくれます!
@Repository
interface UserRepository: JpaRepository<User, Long> {
fun findByLoginId(loginId: String): User?
}
Bean定義の作成
次に、Beanを定義します。
/admin
以下のURLには、ADMIN
ロールのユーザーがアクセスできる設定になっています。
/**
* セキュリティ設定クラス
*/
@Configuration
class SecurityConfig {
/**
* パスワードエンコーダーのBean定義
*/
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
/**
* ユーザー情報のBean定義
*/
@Bean
fun userDetailsService(userRepository: UserRepository): UserDetailsService {
return UserDetailsService { username ->
userRepository.findByLoginId(username)
?: throw IllegalArgumentException("ユーザーが見つかりません")
}
}
/**
* セキュリティ設定のBean定義
*/
@Bean
fun securityChain(httpSecurity: HttpSecurity): SecurityFilterChain {
httpSecurity.authorizeHttpRequests { requests ->
requests.requestMatchers("/login").permitAll() // ログインページは誰でもアクセス可能
// url:admin以下はADMIN権限が必要
// URLの文字列の影響で、コードハイライトが予期せぬ感じになってます・・・
requests.requestMatchers("/admin/**").hasRole("ADMIN") //*/
requests.anyRequest().authenticated() // その他のページは認証が必要
}
http.formLogin { form ->
form.loginPage("/login") // カスタムログインページを設定
.failureUrl("/login?error") // ログイン失敗時のリダイレクト先
.defaultSuccessUrl("/home", true) // ログイン成功時のリダイレクト先
.permitAll() // ログインページへのアクセスを許可
}
http.logout { logout ->
logout.logoutUrl("/logout") // ログアウト時のURLを設定
.logoutSuccessUrl("/login") // ログアウト成功後のリダイレクト先
.permitAll() // ログアウトへのアクセスを許可
}
return httpSecurity.build()
}
}
ログイン画面、ホーム画面、管理画面の作成
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<div>
<label for="username">ログインID:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="password">パスワード:</label>
<input type="password" id="password" name="password">
</div>
<button type="submit">Login</button>
<div th:if="${param.error}">
ログインIDまたはパスワードが間違っています
</div>
</form>
</body>
</html>
home.html
<!DOCTYPE html>
<!-- Spring Securityの機能を利用するための名前空間を追加 -->
<html lang="en" 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>Welcome!</h1>
<p>Home画面です</p>
<!-- ADMIN権限を持つユーザーのみ表示される -->
<a th:href="@{/admin}" sec:authorize="hasRole('ADMIN')">管理画面へ</a>
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</body>
</html>
admin.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>管理画面</title>
</head>
<body>
<h1>管理画面</h1>
<span>管理画面です。</span>
<a th:href="@{/home}">Home画面へ</a>
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</body>
</html>
コントローラーの実装
@Controller
class Controller {
@GetMapping("/login")
fun login() = "login"
@GetMapping("/home")
fun home() = "home"
@GetMapping("/admin")
fun admin() = "admin"
}
動作確認
アプリケーションを起動して、ブラウザでhttp://localhost:8080/login
にアクセスします。
ログインフォームが表示されたら、以下の情報を使ってログインできます。

- 管理者アカウント
- ログインID:
admin
- パスワード:
admin
- ログインID:
- ユーザー01
- ログインID:
user01
- パスワード:
user01
- ログインID:


ログイン成功後、ホーム画面に移動します。管理者権限がある場合、ホーム画面に「管理画面へ」とリンクが表示されます。管理画面はhttp://localhost:8080/admin
ですが、ユーザー01アカウントではアクセスできないようになっています。アクセスすると403エラーになります。
おわりに
以上で、Spring SecurityでのDBを使ったログイン処理が完成です!さらにSpring Securityの機能で表示切り替えやページアクセス制御ができることを紹介しました。UserDetailsを継承することで、アカウントのロックや、アカウントの有効期限切れを確認を実装できます。