今日のテーマ
秀和システムのSpringBoot3プログラミング入門を用いて学習中です。
Spring Bootの学習としてSpring Securityによる認証機能を実装しました。
セットアップでつまずいた部分も多かったので、同じ初学者の参考になればと思います。
環境
- macOS
- Cursor
- Java 17
- Spring Boot 3.5.x
- Gradle
1. プロジェクトの作成(Spring Initializr)
Spring Initializr にアクセスしてプロジェクトを作成する。
設定内容
| 項目 | 設定値 |
|---|---|
| Project | Gradle - Groovy |
| Language | Java |
| Spring Boot | 3.x.x |
| Group | com.example |
| Artifact | samplesecurityapp |
| Packaging | Jar |
| Java | 17 |
設定が完了したら「GENERATE」ボタンをクリックするとzipファイルがダウンロードされる。解凍してCursorで開けばすぐに開発が始められる。
2. build.gradle の依存関係
Spring Initializrでプロジェクトを作成する際、以下の依存関係を追加しておく必要がある。
これがないとWebアプリとして起動しない。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
各依存関係の役割
| Spring Initializrでの名前 | build.gradleでの名前 | 用途 |
|---|---|---|
| Spring Web | spring-boot-starter-web | WebアプリのHTTP機能 |
| Thymeleaf | spring-boot-starter-thymeleaf | HTMLテンプレートエンジン |
| Spring Security | spring-boot-starter-security | ログイン・認証機能 |
| Spring Data JDBC | spring-boot-starter-data-jdbc | DBアクセス機能 |
| H2 Database | h2 | インメモリDB(開発用) |
| MySQL Driver | mysql-connector-j | MySQLドライバ |
※使用中の書籍ではこの部分についての記載はなく、追加を必要としない(元々入っている?)ようだったので、バージョンの違いや私の操作ミスなどにより追加が必要になった可能性を否定しきれません。
3. アプリの起動
依存関係を変更したあとは必ず clean してから起動する。
↓
./gradlew clean bootRun
通常の起動はこちらでOK
↓
./gradlew bootRun
起動に成功すると以下のログが出る。
Tomcat started on port 8080
Using generated security password: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
4. Spring Security の設定
SampleSecurityConfig.java でセキュリティの設定を行う。
@Configuration
@EnableWebSecurity
public class SampleSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(authorize -> {
authorize
.requestMatchers("/").permitAll() // "/" は誰でもOK
.requestMatchers("/js/**").permitAll() // /js/ 以下は誰でもOK
.requestMatchers("/css/**").permitAll() // /css/ 以下は誰でもOK
.requestMatchers("/img/**").permitAll() // /img/ 以下は誰でもOK
.anyRequest().authenticated(); // 上記以外のすべてはログイン必須
});
http.formLogin(form -> {
form.defaultSuccessUrl("/secret");
});
return http.build();
}
}
ポイント
authorizeHttpRequests
URLごとのアクセス制御を設定する。
-
.permitAll()→ ログイン不要でアクセス可 -
.authenticated()→ ログインが必要
個別に指定したURL以外のすべてに対するルールが anyRequest()
必ず一番最後に書くというルールがある。
ルールは上から順番に評価されるので、anyRequest() が先にあると「すべて」にマッチしてしまい、後ろの個別ルールが意味をなさなくなります。
formLogin
ログインフォームの設定。
-
defaultSuccessUrl("/secret")→ ログイン成功後のデフォルト遷移先
defaultSuccessUrlの第2引数にtrueを渡すと常に指定URLへ遷移、false(デフォルト)は元々アクセスしようとしていたページを優先する。
csrf
http.csrf(csrf -> csrf.disable());
↑この部分について、書籍では下記でした。推奨されないと警告が出ていたので書き換えています。
http.csrf().disable()
Spring Boot 3系(Spring Security 6)では http.csrf().disable() は非推奨。
ラムダ式を使った書き方にする。
5. ラムダ式とは
Spring Boot 3系ではラムダ式(->)が多用される。
http.formLogin(form -> {
form.defaultSuccessUrl("/secret");
});
-> の左側が「受け取る引数」、右側が「やること(処理)」。
「設定を渡す」場面でよく登場する。
6. Spring Security の動作の流れ
/secret にログインせずにアクセスすると、自動でログイン画面にリダイレクトされる。
これがSpring Securityが自動でやってくれていること。コントローラーに認証チェックのコードを書く必要はない。
ブラウザで /secret にアクセス
↓
Spring Security が「ログインしてる?」を自動チェック
↓
未ログイン → /login に自動リダイレクト
ログイン済み → /secret を表示
7. MVCの流れ
Webアプリは MVC(Model-View-Controller) という設計パターンで動く。
ブラウザが / にアクセス
↓
Controller がデータ(Model)と画面名(View)をセット
↓
Thymeleaf が HTML に ${title} などの形でデータを埋め込む
↓
完成したHTMLをブラウザに返す
Controller のコード
@RequestMapping("/")
public ModelAndView index(ModelAndView mav) {
mav.setViewName("index"); // 表示するHTMLファイル名
mav.addObject("title", "Index Page"); // HTMLに渡すデータ
mav.addObject("msg", "This is top page.");
return mav;
}
mav の型は ModelAndView です。名前を分解すると:
Model = データ(addObject で追加したもの 例:title, msg)
View = 画面(setViewName で指定したもの 例:index.html)
-
ModelAndView= Model(データ)+ View(画面)をセットにしたもの -
setViewName("index")→templates/index.htmlを表示 -
addObject("title", "Index Page")→ HTML内の${title}に値が入る
Thymeleaf(HTML側)
<h1 th:text="${title}"></h1>
<p th:text="${msg}"></p>
${title} の部分にControllerから渡された値が埋め込まれてブラウザに表示される。
8. 全体の流れまとめ
① ブラウザでアクセス
↓
② Spring Security が「ログインしてる?」と確認
↓
③ してなければ → ログイン画面へリダイレクト
↓
④ ユーザー名・パスワードを入力してログイン
↓
⑤ Controller がデータと画面名をセットして返す(MVC)
↓
⑥ Thymeleaf が HTML にデータを埋め込む
↓
⑦ ブラウザに画面が表示される
Spring Security・Java文法・MVCはバラバラに存在しているのではなく、この一本の流れの中でそれぞれが役割を持っている。
つまずきポイント
ラムダ式の引数名のミス
// NG: 引数名を formLogin にしたのに、中で form と使ってしまった
http.formLogin(formLogin -> {
form.defaultSuccessUrl("/secret"); // エラー: シンボルを見つけられません
});
// OK: 引数名と中で使う名前を一致させる
http.formLogin(form -> {
form.defaultSuccessUrl("/secret");
});
引数名は自分で自由につけられる。-> の左側の名前と右側で使う名前を一致させる。
依存関係追加後は clean が必要
build.gradle を変更したあとにそのまま bootRun すると古いキャッシュで動いてしまうことがある。
./gradlew clean bootRun
重ねてになりますが、自分がこれはどういうこと?とAIに質問したことを中心に、きっと同じように感じる人もいるはず!という気持ちと、忘備録を兼ねて記事にしています。
教科書的な記事ではないので、私が気にならずスルーしていることもあるかもしれません。
何かあったら教えていただけますとありがたいです。