👌

Spring Securityで再認証を実装してみる

2022/05/26に公開

Spring Securiyで再認証するサンプルアプリを作ってみたいと思います

  • この実装が正しいかどうかいまいちわかりません。もっとこうした方がいい等あればコメント頂けると嬉しいです
  • バグがあるかもしれません

はじめに

再認証とは

ここでは 以下の 1 の事として話を進めます

  1. 重要な処理の前、機密性の高い情報を表示する前にユーザー認証を行うこと

    • パスワード変更、アカウント削除、バックアップコード表示、など

    • セッションハイジャック対策、CSRF対策

  2. ログインしたままでいる場合、定期的にユーザー認証すること

作成するサンプルアプリ

以下の仕様で作成します

  • トップページはログイン画面
  • ログインするとMy Page画面を表示する
  • Sub PageをクリックするとSub Page画面を表示する
  • Settingsをクリックすると再認証の画面を表示する
  • 再認証にパスするとSettings画面を表示する
    • 現在ログインしているユーザーのみ再認証をパス可能
    • 再認証は3分間有効とする、3分経過するとSettings画面を表示する際ふたたび再認証が必要となる
  • Logoutをクリックするとトップページに戻る
    • ログイン状態/再認証状態がクリアされる

環境

  • macOS でやります
  • IDEは IntelliJ IDEA を使います
  • ブラウザは Chrome です

サンプルアプリプロジェクト

プロジェクト(sample-app)はSpring Bootアプリです。
spring initializr で以下の設定で作ったプロジェクトです。

  • Kotlin(Java 11)
  • Spring Web
  • Spring Security
  • Thymeleaf
  • H2 Database
  • Spring Data JPA

プロジェクトの初期状態

基本的な認証処理が実装済みの ↓ の状態のソースに対して処理を追加していきます。

spring-security-re-auth

  1. Login をクリックしたらログインする
    • ログイン処理の詳細は Login を参照
  2. Sub Page をクリックしたら Sub Page 画面を表示する
  3. Settings をクリックしたら Settings 画面を表示する
  4. Logout をクリックしたらログアウトする

Login

  1. loginクリックで POST /login をリクエストする

  2. CustomUserDetailsServiceUserID(usernameとして入力された情報) の存在を確認する

https://github.com/gebogebogebo/spring-security-re-auth/blob/7968baf11c5d4dc1167a6f2fb1b369756e3564dc/sample-app/src/main/kotlin/com/example/sampleapp/service/CustomUserDetailsService.kt#L11-L33

https://github.com/gebogebogebo/spring-security-fido2-login/blob/87c3afeea9d273dd6202294c1e4ae9d8b56827f3/sample-app/src/main/kotlin/com/example/springsecuritylogin/repository/MuserRepository.kt#L17-L19

  1. DaoAuthenticationProviderPassword をVerifyする

  2. 認証成功となったら SavedRequestAwareAuthenticationSuccessHandler から /mypage にリダイレクトする

実装

権限(ROLE)を設定する

まずは ROLE_USER , ROLE_ADMIN という2つの権限を追加します。

認証をパスしたユーザーに権限を付与して ROLE_ADMIN 権限が付与されたユーザーだけが/settings にアクセスできるようにします。

ROLE_USER / ROLE_ADMIN の定義

単純なenumを作成します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/8be6e5f1d214da90b2d550192cd2f7d80899efaf/sample-app/src/main/kotlin/com/example/sampleapp/util/AppUtil.kt#L7-L10

認証時に ユーザーに ROLE_USER を付与する

loadUserByUsername()では認証後にユーザーに付与される権限を authorities という配列で渡します。
ここに ROLE_USERSimpleGrantedAuthority クラスに包んで渡します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/8be6e5f1d214da90b2d550192cd2f7d80899efaf/sample-app/src/main/kotlin/com/example/sampleapp/service/CustomUserDetailsService.kt#L26-L36

/settings には ROLE_ADMIN が付与されたユーザーだけがアクセスできるようにする

AppWebSecurityConfigクラスのconfigure() で設定します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/8be6e5f1d214da90b2d550192cd2f7d80899efaf/sample-app/src/main/kotlin/com/example/sampleapp/AppWebSecurityConfig.kt#L16-L21

↑のソースだと ROLE_USER を付与するコードしか書いていないので 誰も /setting にアクセスできません。
なので Settingsボタンをクリックすると Forbidden(403) が発生します。

再認証を追加する

Settingsボタンをクリックしたら(再)認証画面を表示し、この画面から認証をパスしたユーザーに ROLE_ADMIN を付与するようにします。
普通にログインしたユーザーは ROLE_USER の権限しか持っていないので、Settings画面に遷移するときに再認証して ROLE_ADMIN権限をゲットするという動作になります。

Forbidden(403) が発生したら /login にリダイレクトする

AccessDeniedHandler をオーバーライドすることで Forbidden のときの動作をカスタマイズすることができます。
本来行きたかった場所をセッションに保存して /login にリダイレクトさせます。

https://github.com/gebogebogebo/spring-security-re-auth/blob/fde47ae5b9f33f46fc092c55b19ff3c446f5136c/sample-app/src/main/kotlin/com/example/sampleapp/CustomAccessDeniedHandler.kt#L12-L31

↑で作成した CustomAccessDeniedHandler を Config に登録します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/fde47ae5b9f33f46fc092c55b19ff3c446f5136c/sample-app/src/main/kotlin/com/example/sampleapp/AppWebSecurityConfig.kt#L35-L37

再認証したらROLE_ADMIN を付与する

既にログインしている状態で loadUserByUsername() が実行されるということは 再認証 である、という判断で、ROLE_ADMIN を付与します。
このとき、ログインしているユーザーと別のユーザーの場合、再認証は失敗するように考慮します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/fde47ae5b9f33f46fc092c55b19ff3c446f5136c/sample-app/src/main/kotlin/com/example/sampleapp/service/CustomUserDetailsService.kt#L34-L51

再認証で認証失敗したときの考慮が必要です。ログイン済みステータスをクリアしないようにカスタマイズします。

https://github.com/gebogebogebo/spring-security-re-auth/blob/fde47ae5b9f33f46fc092c55b19ff3c446f5136c/sample-app/src/main/kotlin/com/example/sampleapp/CustomUsernamePasswordAuthenticationFilter.kt#L15-L28

↑で作成した CustomUsernamePasswordAuthenticationFilter を Config に登録します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/fde47ae5b9f33f46fc092c55b19ff3c446f5136c/sample-app/src/main/kotlin/com/example/sampleapp/AppWebSecurityConfig.kt#L40-L44

再認証の有効期限をつける

ここまでの実装で再認証する動きの実装はできました。
現状はADMIN権限を取得するとそのユーザーがログアウトするまでずっとADMIN権限を持ったままです。
ここからは、一定時間でADMIN権限が無くなるように実装してみます。

ROLE_ADMIN に3分の有効期限をつけて付与する

ユーザーに付与するROLEは GrantedAuthority を継承したクラスであれば何でもOKです。有効期限の情報がついた ExpireGrantedAuthority クラスを作成します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/0cd03ffa0736f30c923f3ff6b0036686e7f8f933/sample-app/src/main/kotlin/com/example/sampleapp/ExpireGrantedAuthority.kt#L6-L35

loadUserByUsername で↑で作成した ExpireGrantedAuthority に包んで ROLE_ADMIN を作成します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/0cd03ffa0736f30c923f3ff6b0036686e7f8f933/sample-app/src/main/kotlin/com/example/sampleapp/service/CustomUserDetailsService.kt#L42-L50

権限の有効期限をチェックする

/settings に遷移するときに ROLE_ADMIN の有効期限をチェックして有効期限が切れていたら無効にするようにします。

AccessDecisionVoter を継承した AcceptAdminTokenVoter を作成します。この vote メソッドは /settings に遷移するときにその認可をするために呼び出されます。AccessDecisionVoter.ACCESS_DENIE を返すと否定、AccessDecisionVoter.ACCESS_GRANTED を返すと肯定という意味です。
ここで有効期限をチェックします。

https://github.com/gebogebogebo/spring-security-re-auth/blob/0cd03ffa0736f30c923f3ff6b0036686e7f8f933/sample-app/src/main/kotlin/com/example/sampleapp/AcceptAdminTokenVoter.kt#L8-L36

↑で作成した AcceptAdminTokenVoter を Config に登録します。

https://github.com/gebogebogebo/spring-security-re-auth/blob/0cd03ffa0736f30c923f3ff6b0036686e7f8f933/sample-app/src/main/kotlin/com/example/sampleapp/AppWebSecurityConfig.kt#L51-L58

https://github.com/gebogebogebo/spring-security-re-auth/blob/0cd03ffa0736f30c923f3ff6b0036686e7f8f933/sample-app/src/main/kotlin/com/example/sampleapp/AppWebSecurityConfig.kt#L21-L27

まとめ

作成したサンプルアプリの内容をまとめます

  • ログインしたら 一般ユーザー権限(ROLE_USER)を付与する

  • 一般ユーザーは /settings にアクセスできない

  • /settings にアクセスしようとしたタイミングで再認証をする

  • 再認証にパスしたら 管理者権限(ROLE_ADMIN)を付与する

  • 管理者権限があれば /settings にアクセスできる

  • 管理者権限は 3分で有効期限が切れる

ソースはこちら参照

おつかれさまでした

Spring Securityは学習コストが高い!難しい!

Discussion