Spring Boot Security 入門 第一回 - 認証・認可・ログアウト
概要
Java / Spring Boot の Spring Security の基本を学ぶ勉強会です。認証(ログインできるか)と認可(アクセスできるか)の違い、ログインユーザー取得ロジックのカスタマイズ、認可の設定、ログアウトの実装を扱います。
対象読者
- Java / Spring Boot でアプリケーションを開発しているが、Spring Security を「なんとなく」使っている方
- 認証と認可の違いを整理したい方
- ログイン・ログアウトのカスタマイズポイントを学びたい方
この資料の構成
この資料は 2 部構成 になっています。
- 触って学ぶ Spring Security: サンプルアプリを実際に作りながら、認証・認可・ログアウトを実装していくハンズオン
- 座学で学ぶ Spring Security: 認証と認可の違い、フィルタチェーン、パスワードハッシュなどの理論を整理する座学
「触って学ぶ」で実装した内容を、「座学で学ぶ」で概念として整理・復習する流れです。対応関係は次の表の通りです。
触って学ぶ Spring Security
セキュリティの重要性
ハンズオンに入る前に、そもそもなぜセキュリティ対策が必要なのかを確認しておきましょう。Web アプリケーションは、意図しないアクセスや不正な操作の対象になりえます。認証と認可を適切に設定することで、「誰が」「何を」できるかを制御し、アプリケーションとデータを守ります。
このハンズオンでは、認証・認可・ログアウトを実際に実装しながら、「セキュリティを設定する」ことの価値を体感していきます。セキュリティの目的の詳細(認証・認可・パスワードハッシュなど)は、後半の「座学で学ぶ Spring Security」で整理します。
認証と認可の軽い導入
Spring Security を理解するうえで、最初に押さえるべきは「認証」と「認可」の違いです。
- 認証(Authentication): 「誰であるか」を確認する。ログインできるかどうか
- 認可(Authorization): 「何を許可するか」を決める。アクセスできるかどうか
認証は「その人が本当にその人か」を確かめる処理で、認可は「その人に何を許すか」を決める処理です。ログインに成功したとしても、すべてのページを見られるとは限りません。ログインできた(認証)としても、管理者専用ページを見られるかは別の判断(認可)になります。
このハンズオンでは、まず認証(ログインユーザー取得ロジック)をカスタマイズし、その後で認可(アクセス制御)を設定します。詳細な概念は、後半の「座学で学ぶ Spring Security」で改めて整理します。
ベースプロジェクト
spring-boot-security-workshop-base.zip をダウンロードし、展開してください。
本ハンズオンでは、このベースプロジェクトを元に Spring Security のカスタマイズを行っていきます。
インデックスページの作成
HTML の配置
src/main/resources/templates/index.html に、以下 HTML ファイルを配置します。
コントローラーの作成
src/main/java/dev/mikoto2000/security/controller/IndexController.java に、以下 java ファイルを配置します。
デフォルト時の動作確認
Spring Security の設定をしていない場合、起動時に以下のようにログインパスワードが表示されます。
デフォルトユーザーは user なので、このユーザー名・パスワードでログインできます。
http://localhost:8080 を開くとログインフォームが表示されます。
ユーザー名: user, パスワード: <起動時に表示されたパスワード> でログインし、インデックスページが表示されれば OK です。
もちろんこれでは業務要件を満たすわけないので、これからハンズオンで行うようなカスタマイズをしていくこととなります。
ログインユーザー取得ロジックのカスタマイズ
ここでは、 Spring Security のカスタマイズポイントのひとつである「ログインユーザー取得ロジック」のカスタマイズを行います。
ここから先の動作確認では、デフォルトの生成パスワードは使われなくなり、
mikoto2000/password(またはmikoto2001/password)でログインします。
Spring Security では、ログイン時に「ユーザー名から認証対象ユーザーを取得する処理」を UserDetailsService というインタフェースに切り出しています。
この UserDetailsService を差し替えることで、「どこからユーザー情報を取得するか」をカスタマイズできるようになっています。
src/main/java/dev/mikoto2000/security/configuration/UserDetailsServiceImpl.java を作成し、次のように実装します。
こうすることで、「ユーザーを探して Spring Security の認証処理に必要なユーザー情報を返却する」という処理を自分で実装できます。 (UserDetailsService を実装したクラスを Bean として登録すると、Spring Security が自動的にこれを認証処理に利用します)
今回の実装では、メモリ上に HashMap でユーザー名とパスワードを保持し、そこからフォームで渡されたユーザー( loadUserByUsername の仮引数 username )を探すように実装しています。
今回は仕組み理解が目的のため、DB ではなく HashMap を使って最小構成で実装していますが、
本格的に実装するなら loadUserByUsername の中で DB 接続してユーザー情報を検索し、返却することになります。
これはハンズオンの後半でやってみましょう。
また、パスワードについても、デフォルトで対応している bcrypt 形式でハッシュ化された文字列を使っています。
{bcrypt} プレフィックスは「この文字列は bcrypt でハッシュ化されたパスワードだ」ということを Spring Security に伝えるための目印です。
ここもカスタマイズポイントですので、後の方で取り上げます。
認可のカスタマイズ
ここからは「どのユーザーが、どのページを見られるか」を制御する「認可(Authorization)」の設定をします。 認証(ログインできるか)と認可(アクセスできるか)は、Spring Security では別の関心事として扱われます。
SecurityFilterChain を Bean として定義することで、「認証方法」と「認可」のカスタマイズができます。
今回は「認証方法」は Spring Security が提供するデフォルト(ユーザー名・パスワードによるフォームログイン)のままで、「認可」のみをカスタマイズしてみましょう。
認可の条件は次の通りとします。
/(インデックスページ)は誰でも表示可能- ルート以外のページはすべてログイン必須
/private は「ルート以外のページ」の代表例として、認証必須ページを作成します。
認証必須ページの作成
認証したら見えるページを作成します。
src/main/resources/templates/private.html を作成します。
認証必須ページのコントローラーを作成
インデックスページと同じようにコントローラーを作成します。
インデックスページの更新
認証必須ページへのリンクを追加します。
src/main/resources/templates/index.html を以下のように更新します。
SecurityFilterChain の定義
src/main/java/dev/mikoto2000/security/configuration/SecurityConfig.java を作成し、以下のように実装します。
SecurityFilterChain を Bean として定義すると、Spring Boot が自動設定していたデフォルトの SecurityFilterChain は無効化され、この設定が全面的に使われるようになります。
動作確認
http://localhost:8080 にアクセスするとトップページが表示され、「認証必須ページ」のリンクを押下するとログイン画面に遷移。
その後ログインすると認証必須ページが表示されます。
ログインには、前述の UserDetailsServiceImpl で定義した mikoto2000 / password を使います。
これで、認可設定のカスタマイズが確認できました。
ログアウトの実装
認証認可のカスタマイズができたので、今度はログアウトの実装をしましょう。
ログアウトは、 SecurityFilterChain にログアウトの設定を追加することで実現できます。
ログアウト設定の追加
src/main/java/dev/mikoto2000/security/configuration/SecurityConfig.java を以下のように編集してください。
Spring Security が提供するデフォルトでは、 /logout にアクセスするとログアウト確認画面が表示されます。
Log Out ボタンを押下するとログアウトし、 /login にリダイレクトされます。
(ログアウトは、 /logout への POST リクエストで実行される)
前述の通り、ログアウトは /logout への POST リクエストで実行されるため、
自作のフォームから(ログアウト画面を介さず)ログアウトさせることも可能です。
このあたりのカスタマイズはハンズオンの後の方でやりましょう。
ログアウト用リンクの追加
index.html
private.html
動作確認
http://localhost:8080 へアクセスし、private へ遷移 -> ログイン -> ログアウト -> ルート -> private というように移動してみましょう。
ログアウトできていることが確認できます。
まとめ
ここまでで、次の作業を進めてきました。
- プロジェクトの作成
- デフォルト時の動作確認
- ログインユーザー取得ロジックのカスタマイズ
- 認可のカスタマイズ
- ログアウトの実装
これで、基本的な「ユーザー名とパスワードを用いたログイン」のカスタマイズポイントがわかってきたはずです。 次回は次の作業を進めます。
- ログイン・ログアウトのカスタマイズ
- DB からユーザー情報を取得するように修正
- ユーザー登録
座学で学ぶ Spring Security
認証と認可の違い
認証(Authentication)と認可(Authorization)は別の関心事
Spring Security を理解するうえで、最初に押さえるべきは「認証」と「認可」が別の概念だということです。
- 認証(Authentication): 「誰であるか」を確認する。ログインできるかどうか
- 認可(Authorization): 「何を許可するか」を決める。アクセスできるかどうか
認証は「その人が本当にその人か」を確かめる処理で、認可は「その人に何を許すか」を決める処理です。たとえば、ログインに成功したユーザーが、必ずしもすべてのページを見られるわけではありません。ログインできた(認証)としても、管理者専用ページを見られるかは別の判断(認可)になります。
ハンズオンでは、この 2 つを別のタイミングで扱いました。まず UserDetailsServiceImpl で「誰としてログインするか」をカスタマイズし、その後 SecurityFilterChain で「どのページを見られるか」を設定しました。この順番は、認証が先で認可が後、という処理の流れに対応しています。
なぜ分けて考えるのか
認証と認可を分けて考えると、それぞれの関心事を独立して設定できます。「認証方法は変えずに、認可のルールだけ変える」といった変更が、互いに影響を与えずに行えます。ハンズオンでも、認証方法はデフォルトのままにして認可だけをカスタマイズしました。これは、2 つの関心事が独立しているからこそできる切り分けです。
認証の仕組み(UserDetailsService)
認証の流れ
Spring Security のフォームログインでは、次のような流れで認証が行われます。
この流れの中で、アプリケーションがカスタマイズする代表的なポイントが UserDetailsService です。
UserDetailsService の役割
UserDetailsService は「ユーザー名から認証対象ユーザーを取得する処理」を切り出したインタフェースです。ハンズオンでは、このインタフェースを実装して「どこからユーザー情報を取得するか」を自分で決めました。
ハンズオンでは HashMap から探しましたが、ここを DB 検索に置き換えれば「DB からユーザー情報を取得する」実装になります。UserDetailsService は「ユーザー情報の取得元」を差し替えるための窓口であり、認証の仕組みそのものは Spring Security が担当します。
なぜ UserDetailsService に切り出すのか
ユーザー情報の取得元は、アプリケーションごとに異なります。メモリ上のマップ、DB、外部の認証サービスなど、取得元はさまざまです。Spring Security はこの部分をインタフェースに切り出し、アプリケーション側で実装を差し替えられるようにしています。これにより、認証の仕組みは共通のまま、ユーザー情報の取得方法だけを自由に変えられます。
パスワードハッシュ(bcrypt / DelegatingPasswordEncoder)
パスワードをそのまま保存しない理由
パスワードを平文のまま保存すると、データが漏れたときにパスワードがそのまま相手に渡ってしまいます。そのため、保存時にはハッシュ化して、元のパスワードをそのまま持たないようにします。ハッシュ化された値から元のパスワードを復元するのは困難で、たとえデータが漏れてもパスワード自体は守られやすくなります。
bcrypt とは
bcrypt はパスワードのハッシュ化に使われるアルゴリズムのひとつです。特徴として、計算にわざと時間がかかる設計になっており、総当たり攻撃への耐性を高めています。Spring Security は bcrypt をデフォルトでサポートしています。
ハンズオンでは、次のように {bcrypt} プレフィックス付きのハッシュ文字列をパスワードとして設定しました。
{bcrypt} プレフィックスの意味
{bcrypt} は「この文字列は bcrypt でハッシュ化されたパスワードだ」ということを Spring Security に伝える目印です。このプレフィックスは DelegatingPasswordEncoder という仕組みで解釈されます。
DelegatingPasswordEncoder とは
DelegatingPasswordEncoder は、複数のパスワードエンコーダーをプレフィックスで使い分けるための仕組みです。{bcrypt} なら bcrypt、{noop} なら平文、といったように、プレフィックスを見て適切なエンコーダーを選びます。これにより、既存のパスワードを移行しながら新しいアルゴリズムを導入する際も、形式を混在させられます。
ハンズオンでは bcrypt を使いましたが、この仕組みがあることで「どのアルゴリズムでハッシュ化したか」を文字列の先頭で管理できます。
認可の仕組み(SecurityFilterChain / requestMatchers)
SecurityFilterChain とは
Spring Security は、リクエストを一連のフィルタで処理します。このフィルタの並びを定義するのが SecurityFilterChain です。認証や認可の処理は、このフィルタチェーンの中で実行されます。
ハンズオンでは、SecurityFilterChain を Bean として定義しました。これを定義すると、Spring Boot が自動設定していたデフォルトのチェーンは無効化され、自分で定義した設定が全面的に使われます。
requestMatchers と anyRequest の違い
認可のルールは authorizeHttpRequests の中で指定します。
requestMatchers("/"): 指定したパスにだけルールを適用するpermitAll(): 認証なしでアクセスを許可するanyRequest(): 上で指定しなかった残りのすべてのリクエストにルールを適用するauthenticated(): ログイン済みのユーザーだけ許可する
この設定では「/ は誰でも見られるが、それ以外はログイン必須」というルールになっています。ルールは上から順に評価されるため、先に requestMatchers("/").permitAll() で / を許可し、残りを anyRequest().authenticated() でログイン必須にしています。
認可の判断はどのタイミングで行われるか
認可は、認証が成功した後に実行されます。フィルタチェーンの中で、まず認証フィルタがユーザーを特定し、その後で認可フィルタが「このユーザーはこのパスにアクセスしてよいか」を判断します。ハンズオンで「ログインしていないと /private にアクセスするとログイン画面に遷移する」という挙動は、この認可フィルタによるものです。
ログアウトの仕組み
ログアウトは POST で実行される
ログアウトは、/logout への POST リクエストで実行されます。ハンズオンでは logout(Customizer.withDefaults()) を追加し、Spring Security が提供するデフォルトのログアウト処理を利用しました。
デフォルトでは、/logout に GET でアクセスするとログアウト確認画面が表示され、Log Out ボタンが押されると POST でログアウトが実行されます。
なぜ GET ではなく POST なのか
ログアウトは状態を変更する操作です。状態を変更する操作を GET で実行すると、リンクを踏んだだけでログアウトが実行されてしまいます。たとえば、ブラウザのキャッシュやリンクの共有によって意図しないログアウトが発生する可能性があります。そのため、ログアウトのような状態変更は POST で実行するのが安全です。
ハンズオンで追加した <a href="/logout"> は GET なので、確認画面を経由してから POST でログアウトする、という流れになっています。
ログアウト後の状態
ログアウトが実行されると、セッションに保持されていた認証情報が破棄され、以降のリクエストは認証されていない状態として扱われます。ハンズオンでは、ログアウト後にルートへ戻り、再度 /private へアクセスするとログイン画面に遷移することを確認しました。