🦔

【1日1zenn - day14】Spring Bootで認証機能とか作ってみる part.2

に公開

以下の続きです
https://zenn.dev/shunsuke108m/articles/6d94bfbbe96e77

一旦公式チュートリアル通りに認証機能を実装したので、これの理解と応用をやっていきます。

認証機能の理解

コードとか諸々。

MvcConfig.kt

package com.example.securingweb

import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class MvcConfig : WebMvcConfigurer {
    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addViewController("/home").setViewName("home")
        registry.addViewController("/").setViewName("home")
        registry.addViewController("/hello").setViewName("hello")
        registry.addViewController("/login").setViewName("login")
    }
}
  • @ConfigurationにするとSpringの設定系をかける
    • 今回はURIを決めてる感じ
    • /homehome.htmlを表示するとか
  • WebMvcConfigurerをoverrideしたaddViewControllersを作る
    • ViewControllerRegistry型のregistryに設定を追加していく感じ
    • ViewControllerRegistryは、リダイレクトしたいURLパスとかを登録しておけるイメージ
  • 今回は//homeでhome.htmlを表示し、homeにはhello.htmlに遷移できるリンクがあり、ログインしたらhelloを表示できる感じになっていく

home.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>

<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
  • Tymeleaf要素のth:hrefで、hereをクリックするとhello.htmlを表示できるように設定する
  • それ以外はまあデフォルトって感じ

hello.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
    <title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello <span th:remove="tag" sec:authentication="name">thymeleaf</span>!</h1>
<form th:action="@{/logout}" method="post">
    <input type="submit" value="Sign Out"/>
</form>
</body>
</html>
  • helloはこんな感じで、ログインした後の挙動を作るので、Tymeleafのフォーム要素であるth:actionでsubmitボタンをクリックされたら/logoutというポストメソッドにアクセスする
  • <span th:remove="tag" sec:authentication="name">で受け取ったユーザー名を表示
    • th:remove="tag"は、<span>タグを削除して中身だけをHTMLに残す感じ
    • sec:authenticationは、Tymeleafからユーザー情報を取得できる

SecuringWebApplication.kt

package com.example.securingweb

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class SecuringWebApplication

fun main(args: Array<String>) {
    runApplication<SecuringWebApplication>(*args)
}
  • これは実行するためのベース。

WebSecurityConfig.kt

これが根幹。

package com.example.securingweb

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.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class WebSecurityConfig {
    @Bean
    @kotlin.Throws(Exception::class)
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .authorizeHttpRequests({ requests ->
                requests
                    .requestMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
            }
            )
            .formLogin({ form ->
                form
                    .loginPage("/login")
                    .permitAll()
            }
            )
            .logout({ logout -> logout.permitAll() })

        return http.build()
    }

    @Bean
    fun userDetailsService(): UserDetailsService {
        val user: UserDetails =
            User.withDefaultPasswordEncoder()
                .username("testUset")
                .password("password")
                .roles("USER")
                .build()

        return InMemoryUserDetailsManager(user)
    }
}
  • WebSecurityConfigクラスには2つの関数がある
    • 前提WebSecurityConfigは、@Configuration
      • @ConfigurationはBeanの定義とサービスのリクエストを生成できる
      • @EnableWebSecurityは、@Configuration クラスに追加してSpring Securityを使えるようにする感じ
  • 最初のメソッドsecurityFilterChainは、HttpSecurity型のhttpを引数として受け取るSecurityFilterChain型の関数
    • SecurityFilterChainはSpring Securityのアクセス制限、ログイン、ログアウトの挙動を設定するもの
    • HttpSecurityの持つauthorizeHttpRequestsを使用してアクセス制限ができる
      • .requestMatchers("/", "/home").permitAll()で、homeやルートはアクセス制限しないように記載
      • .anyRequest().authenticated()によって、上記以外の全てのリクエストには認証を必須にする
    • HttpSecurity.formLogin()は、HTMLフォームを介して提供されるユーザー名とパスワードのサポートを提供する
      • 認可されていないリソースに認証されていないリクエストを行うと、AccessDeniedExceptionがスローされ、ログインページへのリダイレクトを行う感じ
      • そのログインページとして.loginPage("/login").permitAll()を定義しておく
    • HttpSecurity.logout()は、ログアウト機能。
      • EnableWebSecurity を使用するときに自動的に適用されます。デフォルトでは、URL "/logout" にアクセスすると、HTTP セッションを無効にし、構成された rememberMe() 認証をクリーンアップし、SecurityContextHolder をクリアしてから、"/login?success" にリダイレクトすることでユーザーをログアウトする
      • Cookieの削除とかもできる
    • return http.build()で、設定した内容を SecurityFilterChainとして構成して返すらしい。
  • 2つ目のメソッドのuserDetailsService()について
    • これはデータベース代わりにユーザー名とかパスワードとかを定義しておく感じ
    • InMemoryUserDetailsManagerはDB接続を使用せずにメモリ内にユーザー情報を保存することで認証処理を単純化するもの。そこに、規定した``user```のデータを貯める。
      • 実務だとDBに問い合わせる感じ
    • 実際の処理はSpring Securityで定義されているが、以下みたいな感じ
      • /loginに対してHTTP POSTリクエストが送信される
        • デフォルトだとURL/loginに応答するようになってる
      • Spring Securityの UsernamePasswordAuthenticationFilterが以下の処理を行う
        • 送信されたフォームデータからusernameとpasswordを取得
        • UserDetailsServiceには、ユーザー名からパスワードなどのユーザー情報を取得するloadUserByUsernameというメソッドがあり、これが走る
        • userDetailsService()で登録したInMemoryUserDetailsManagerに問い合わせを行い、指定されたuserNameに対応するUserDetailsを返す
      • リクエストから取得したパスワードを、UserDetailsに登録されているパスワードと比較してくれるらしい。Spring Securityが勝手に。
        • その際にwithDefaultPasswordEncoder()でエンコードされたパスワードが使用される
      • リクエストに成功した場合、UserDetailsがSecurityContextに保存される
      • リクエストに失敗すると/login?errorにリダイレクトされる
        • login.htmlに書いてある<div th:if="${param.error}">に従い、URLパラメータにerrorがある判定になってエラー要素を表示する

抽象化してみる

箇条書き

超抽象化したメンタルモデルを作ると、以下。

  • DBなどにユーザー情報を保存しておく
  • 認証なしで行える挙動と、認証が必要な挙動を定義
    • ページ遷移やAPI問い合わせなど
  • ユーザー情報の取得
    • ユーザーに入力させたり、Cookieからの取得だったり、取得方法は様々
  • DBとのマッチング
    • セキュリティ担保しつつやり方は様々
  • 認証成功した場合と失敗した場合の挙動を定義
    • success
  • ログアウト方法の定義

色々調べてみる

様々な認証方法について見ていく。
https://panasonic.co.jp/ew/pewnw/solution/column/network/025.html

  • そもそも認証とは
    • ユーザを識別するための情報と、ユーザであることを確認するための情報を組み合わせることで行われる
    • システム側では、誰にIDを発行し、そのIDにどのような権限を与えたかを管理し、認証が正常に行われれば、管理している情報に基づいてユーザに権限を与える
  • 認証要素について
    • 以下の3つがガイドラインで定められているらしい。
      • 記憶
        • パスワードや秘密の質問など
      • 所持
        • ICカードや物理的な鍵など
          • 言われてみればこれも認証か。
      • 生体情報
        • 指紋・虹彩・音声など
  • 認証方式について
    • メモりたかったもの
      • CAPTCHA認証
        • 人間かソフトウェアかを区別するもの
          • ユーザーを特定する用途じゃなくて「ユーザーか」を区別するのも認証っていう概念なのね、という感想
      • シングルサインオン
        • 1回の認証で複数システムやサービスの利用を可能にする
          • 会社のツールとかもSSO認証でやれたりしてて、ユーザー情報とか入力しなくてもログインできてるけどどういう仕組みなんだろう
          • これちょっと掘りたい

SSO認証とは?

https://aws.amazon.com/jp/what-is/sso/

  • SSOの仕組み
    • SSO サービス
      • 認証されていないユーザーがアプリケーションへのアクセスを要求すると、アプリはそのユーザーを SSO サービスにリダイレクトする
      • サービスはユーザーを認証し、元のアプリケーションにリダイレクトする
    • SSO トークン
      • ユーザー名や E メールアドレスなどのユーザー識別情報を含むデジタルファイル
      • ユーザーがアプリケーションへのアクセスを要求すると、アプリケーションは SSO サービスと SSO トークンを交換してユーザーを認証する
    • SSO プロセス
      • ユーザーがアプリケーションにサインインすると、アプリは SSO トークンを生成し、認証リクエストを SSO サービスに送信する
      • サービスは、ユーザーが以前にシステムで認証されたかどうかを確認する
      • 認証されていた場合、アプリケーションに認証確認レスポンスを送信して、ユーザーにアクセスを許可する
      • ユーザーが検証済みの認証情報を持っていない場合、SSO サービスはユーザーを中央のログインシステムにリダイレクトし、ユーザー名とパスワードを送信するように求める
      • 送信時に、サービスはユーザー認証情報を検証し、肯定的なレスポンスをアプリケーションに送信する
      • 送信されないと、ユーザーはエラーメッセージを受け取り、認証情報を再入力する必要がある
  • SSOのタイプ
    • SAML
      • アプリケーションが認証情報を SSO サービスと交換するために使用するプロトコルまたは一連のルール
      • ブラウザに適したマークアップ言語である XML を使用して、ユーザー識別データを交換
    • OAuth
      • アプリケーションがユーザーにパスワードを与えることなく、他のウェブサイトからユーザー情報に安全にアクセスできるようにするオープンスタンダード
      • ユーザーのパスワードを要求する代わりに、アプリケーションは OAuth を使用して、パスワードで保護されたデータにアクセスするためのユーザー許可を取得
      • OAuth は、API を介してアプリケーション間の信頼を確立
      • これにより、アプリケーションは、確立されたフレームワークで認証要求を送信および応答できる
    • OIDC
      • OpenID は、1 組のユーザー認証情報を使用して複数のサイトにアクセスする方法
      • これにより、サービスプロバイダーは、ユーザーの認証情報を認証する役割を引き受けることができる
      • 認証トークンをサードパーティーの ID プロバイダーに渡す代わりに、ウェブアプリケーションは OIDC を使用して追加情報を要求し、ユーザーの信頼性を検証する
    • Kerberos
      • Kerberos は、ネットワーク上で 2 者以上の当事者が相互に身元を確認できるようにするチケットベースの認証システム
      • セキュリティ暗号化を使用して、サーバー、クライアント、およびキーディストリビューションセンター間で送受信される識別情報への不正アクセスを防止する

むずいな。なんかの機会に作ってみよう、
ざっくり分けると、XMLを使用するSAMLと、他のウェブサイトからAPIでユーザー情報を取得するOAuthと、認証トークンを保持するOIDCと、暗号化を使用するKerberosという感じ?
OAuthを使うときはわかりやすいが、他でどう選定していくかイメージつかないので、掘る余地がありそう。

一旦しめる

次の予定が迫ってきたので一旦閉めます。
が、認証のざっくりとした理解はできたような気がしなくはない。
他の認証周りのチュートリアルやったりしていくと、明確に「認証ってこうやるやつ」という軸ができて「この案件はこういう認証が良さそう」「この案件は基本的なこの認証だとこれができないからこういう処理が必要そう」みたいなのがわかってくる気がする。

Discussion