🔒

【Spring Security】ユーザー認証のイメージを掴む編

2023/08/19に公開

初めに

Spring SecurityはSpringプロジェクトに入れるだけで自動で色々とセキュリティ関連のことをやってくれる優れものです。使ってみるとネットにいろんな記事が転がってるけど、意外と理解できないポイントがあり、経験の浅い私では理解に時間がかかりました。今回はユーザー認証に関して説明しつつ、「ここを教えてくれる人がいればもっと早く理解できたのに」と自分が思ったポイントなどをできるだけ伝えられればと思います。

この記事でやること

  • Spring Securityの簡単な導入
  • InMemoryUserDetailsManagerを用いた認証
  • ユーザー認証のざっくりとした流れを掴み、何が必要かを整理する
  • JdbcUserDetailsManagerを用いた認証(DBとの連携)

環境

  • JDK:Amazon Correto17
  • ビルドツール:maven
  • Spring Boot:3.1.2
  • Spring Security:6.1.2

導入

pom.xmlに以下を追加します。

pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

これだけで勝手にログイン機能が実装されます。
今まで見れていたページを開こうとすると下図のようなログイン画面に遷移します。

ログイン画面は自分で用意できますが、初めはデフォルトでログイン画面も用意してくれています。
初めは勝手にログインするユーザーも作成され、ユーザー名はuser、パスワードはアプリ起動時に以下のようにログに出力され、この値はアプリ起動する度に異なります。

Using generated security password: c4f9dc09-1e75-49a7-a940-84ebe5d2f8de

ちなみに、このユーザー名とパスワードはapplication.properties(ymlでも可)で次のプロパティを設定することで固定化できるようです。

spring.security.user.password
spring.security.user.name

InMemoryUserDetailsManagerを用いた認証

認証の流れを説明する前にInMemoryUserDetailsManagerを用いた認証を実装してみます。InMemoryUserDetailsManagerはメモリ内にユーザーとパスワードの情報を保持しており、それを使ってユーザの認証を行います。こちらもDBを使用せずにユーザー認証ができるということですね。以下のようなSecurityConfig.javaを作成します。ファイル名はわかりやすければなんでも大丈夫です。

SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class SecurityConfig{
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User
                .withUsername("user")
                .password(passwordEncoder().encode("123456"))
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}

@Configuration

まずアノテーションから説明して行きます。@Configurationをつけたclassは構成クラスとして認識されます(参考)。そうするとアプリケーション実行時にこのクラス内にある@Beanなどが全て検索され、DIコンテナにインスタンスとして登録されるようになります。@Beanに決まりに則ったメソッドを書くことで、Spring Securityが勝手にそれらを利用してくれるという流れでセキュリティの設定をいじることができます。

@EnableWebSecurity

あまり詳しくはわかっていませんが、「Webセキュリティを動作する」ためのもので、@EnableWebSecurityを指定すると、Spring Securityが提供しているコンフィギュレーションクラスがインポートされ、Spring Securityを利用するためのに必要となるコンポーネントのBean定義が自動で行われる仕組みになっているようです。Spring Security関連のConfigにつけておけば良さそうです。ちなみに、Spring Security 5.8までは @EnableWebSecurityに @Configurationが付加されていたため、@EnableWebSecurityを付加するだけでJava Configクラスと認識されていたようですが、6.0からは@Configurationが削除されたため、明示的に@Configurationを付加する必要があるようです。

PasswordEncoder

ユーザーが入力したパスワードをエンコードする方法を設定できます。今回はbcryptを使用しています。この書き方でBean登録しておけば、勝手にSpring Security使用してくれます。

InMemoryUserDetailsManager使用部分

細かい説明はこの後するので、一旦ざっくり理解してもらえればと思います。以下コード見るとなんとなくわかるように、ここではどんなユーザーでログインできるかを設定しています。今回だとユーザー名:user、パスワード:123456(→実際はこの値がbycryptでハッシュ化された値)、役割USERという内容のユーザー情報になります。あとはユーザー名とパスワードをログイン画面で入力すればログインできます。

SecurityConfig.javaの一部
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User
	.withUsername("user")
	.password(passwordEncoder().encode("123456"))
	.roles("USER")
	.build();
return new InMemoryUserDetailsManager(user);
}

補足として、戻り値の型はInMemoryUserDetailsManagerの継承元であるUserDetailsServiceを指定したり、メソッド名は他の名前でも大丈夫そうです。

認証の流れ

InMemoryUserDetailsManagerだけなら、なんとなく使い方がわかるのですが、この先自分で自由に実装していけるようにSpring Securityの認証の流れについてもう少し理解を深めていきましょう。
Spring Securityを理解するのに以下の記事とyoutubeが参考になりました。
【Spring Security】認証・認可の基礎
JSUG勉強会 2022年その2 Spring Security特集!

認証に必要なもの

まず認証には何が必要なのかをまとめました。意識していませんでしたが、InMemoryUserDetailsManagerの場合もこれらは使われています。

  • UserDetails →ユーザー情報の入れ物
  • UserDetailsService →UserDtailsを取得する処理を記載
  • PasswordEncoder →エンコードの設定

認証の流れ

以下はDBを使った認証の流れの図でよかったものを参照しましたが、基本このイメージでいいと思います。

画像の参照先

上の番号とは異なりますが、流れとしては

  1. ユーザー名とパスワードが送られてくる
  2. ユーザー名を基にUserDetailsServiceでユーザー情報が入ったUserDetailsを作成
     →この際ユーザー名に一致する情報(パスワード等)をDBから取得したりする
  3. 送られたパスワードをハッシュ化し、UserDetailsのハッシュ化済みパスと照合
     →パスワードが一致していれば認証成功!

この時UserDetailsとUserDetailsServiceはこれらを実装したクラスであれば、自作したクラスでもいいし、既に用意されているものもあります。ただ基本としてUserDetailsとUserDetailsServiceは使われているんだなぁという認識を持ってもらえればと思います。

UserDetails

FQCNで書くと以下です。

org.springframework.security.core.userdetails.UserDetails

ユーザーの情報の入れ物となるもので、UserDetails自体はインターフェースになります。必要となるメソッドを実装すれば、自作できます(参考)。
自作しなくても、以下のUserクラスを使用することもできます。

import org.springframework.security.core.userdetails.User;

InMemoryUserDetailsManagerの例では以下のようにUserにユーザー名などをセットして、最後にbuildすることでUserDetailsのインスタンスを作成しています。

UserDetails user = User
	.withUsername("user")
	.password(passwordEncoder().encode("123456"))
	.roles("USER")
	.build();

UserDetailsService

こちらは以下のFQCNで示されるインターフェースです。

org.springframework.security.core.userdetails;

UserDetailsServiceでは、loadUserByUsernameメソッドでユーザー名を基にUserDetailsを戻り値として返します。こちらもUserDetailsServiceを実装して、自作できます参考
自作しなくても、UserDetailsServiceを継承したUserDetailsManagerという既存のものを使用することもできます。UserDetailsManagerはいくつか種類があるらしく、先ほどのInMemoryUserDetailsManagerもその内の1つです。
継承イメージ図は下図です。

InMemoryUserDetailsManagerもコードを見てみるとloadUserByUsernameが実装されています。継承されてややこしくなっていますが、InMemoryUserDetailsManagerを使う=認証に必要なUserDetailsServiceを使っているということになるのです。

ざっくりとした理解だとUserDetailsManagerはユーザーの管理を色々行うための便利なものみたいな感じです。

JdbcUserDetailsManagerを用いた認証

最後にJdbcUserDetailsManagerを用いてDBから取得したユーザーの情報で認証を行なっていきます。JdbcUserDetailsManagerもUserDetailsManagerの1種となります。

以下リンクが参考になりました。
Spring BootでSpring Security機能を使う ー データベースを使った認証
ログイン、ログアウトの実装

JdbcUserDetailsManagerはUserDetailsManagerを介してUserDetailsの管理を提供するもので、以下のような機能を提供しています(UserDetailsManagerのjavadoc)。

  • changePassword: パスワード変更
  • createUser: ユーザの作成
  • deleteUser: ユーザの削除
  • updateUser: ユーザの更新
  • userExists: ユーザの存在確認

もう少し砕けた表現で言うとJdbcUserDetailsManagerを使えば、データベースからユーザー情報取得したり、ユーザー作成する等が簡単にできますよということです。

実際に作成したコード内容

使い方を理解するために作成したコードを確認していきます。import部分など一部省略していますが、以下のようになります。

SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig{

    @Autowired
    private DataSource dataSource;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public UserDetailsManager userDetailsManager() {
        JdbcUserDetailsManager user = new JdbcUserDetailsManager(this.dataSource);
        // ユーザーを追加したい時
	// user.createUser(makeUser("user", "pass", "USER"));

        return user;
    }

    private UserDetails makeUser(String user, String pass, String role) {
        return User.withUsername(user)
                .password(passwordEncoder().encode(pass))
                .roles(role)
                .disabled(false)
                .build();
    }
}

何をやっているかわからない部分もあるかもしれませんが、userDetailsManagerメソッドでUserDetailsManagerのインスタンスをBeanに登録している→つまり、使用するUserDetailsServiceはこれだ!と宣言しているということです。これだけであとはJdbcUserDetailsManagerの中に書かれた処理でうまいことDBからユーザー情報を取得して、UserDetailsを作成してくれるようです。

ポイント(SecurityConfig内)

まず1つ目のポイントとしては、DataSourceをDIして、それをJdbcUserDetailsManagerの引数に渡して、JdbcUserDetailsManagerのインスタンスを作成するということです。
DataSourceはJDBCの機能の1つでアクセスするデータベースとその接続手段などをオブジェクト化して管理するものです。特に設定しなくても、DIコンテナに用意されているので、あとは使う箇所で注入してあげればいいだけです。以下の部分でDIしています。

@Autowired
private DataSource dataSource;

記事の本筋とは逸れますが、DBとの接続の部分の実装について残しておきます。内容としては、使用したDB(MySQL)のcompose.ymlとDataSourceで使うMySQLの属性をapplication.ymlに記載したものを折りたたみにしておくので、必要であれば見てください。

compose.yml
compose.yml
services:
  mysql:
    image: mysql:8.0.27
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: mysql
      MYSQL_DATABASE: db
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    volumes:
      - db-data:/var/lib/mysql
  phpmyadmin:
    image: phpmyadmin
    depends_on:
      - mysql
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOSTS=mysql
      - PMA_USER=root
      - PMA_PASSWORD=mysql
    ports:
      - "3001:80"
volumes:
  db-data:

application.yml
application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db
    username: user
    password: password
  sql:
    init:
      encoding: UTF-8

結論コードとしてはここまでの内容で問題なく動作します。

ポイント(データベース作成)

さて、DataSourceでデータベースの接続先の情報をもったJdbcUserDetailsManagerが作成されましたが、一体どのテーブルがユーザー情報を管理するテーブルなのかということは自分で一切書いていません。
ここが注意ポイントなのですが、JdbcUserDetailsManagerを用いる場合は決まったテーブル名・テーブル構造でデータベースを作成する必要があります。そこまでコードを読み進めてはないのですが、JdbcUserDetailsManagerの中にユーザー情報は〜というテーブルからデータ取得してねということが書いてあると思います。既にテーブル名等が決まっているから、情報としてはDBの接続先の情報しかないDataSourceを渡すだけで事足りるのです。

決まりに則るため、以下のようなテーブル名、テーブル構造、カラム名でDBを作成してください。

同様にテーブル構造を示す内容ですが、実際にmysqlだとこんな感じという参考に以下の折りたたみを記載。同じユーザーが複数できておかしくなっちゃうと困るのでusernameはユニークに設定しています。

phpmyadminでのテーブル構造の例


ユーザーデータの作成

あとはユーザーのデータをDBに作成すれば、ログインが可能になります。
以下のJdbcUserDetailsManagerのインスタンスメソッドであるcreateUserは引数のUserDetailsのデータをDBに追加します。
以下のコメントアウトを解除すれば、Bean登録時にユーザーが追加されます。

// user.createUser(makeUser("user", "pass", "USER"));

makeUserのprivateメソッドでUserDetailsを作成しています。今回の場合はusernameがuser、passwordがpass、roleがUSERというUserDetailsが作成され、それを基にデータが追加されます。

本来この場所にユーザー作成の処理を書くべきではないですが、手っ取り早くユーザー作成するのに楽だったので書いています。同様のやり方で他にユーザ作成の機能を作成したら、このファイルからは削除すれば良いと思います。

テーブルを2つ作成したように、createUserメソッドが実行されると、userテーブルにパスワードなどの情報が追加され、authorityテーブルにroleの情報がそれぞれ追加されます。

実際に作成されたデータの例

↓userテーブル


↓authorityテーブル

ユーザー名がユニークなら重複してデータが追加されることはありませんが、データの追加が不要となったらコメントアウトや削除するようにしましょう。

JdbcUserDetailsManagerを使う場合はUserDetailsをユーザー情報の入れ物として使用しているみたいですが、自分で設定したりもしないので、特に意識することはないですね。

最後に

Spring Securityを用いたユーザー認証のイメージをなんとなく掴むことができたでしょうか?
自分としては、初めにInMemoryUserDetailsManagerやJdbcUserDetailsManagerを用いた認証の記事を見たけど、結局何をやっているかイメージを掴みづらかったので、今回の記事が初学者の役に立てれば幸いです。理解するために必要なことは結局ユーザー認証にはUserDetailsやUserDetailsServiceが必要で、それらがどういう役割を果たすのか、そして必要なものをBeanに登録しておけばあとはSpring Securityがいい感じに使ってくれるということです。今回はUserDetailsManagerを使用しましたが、自作ファイルを作成し色々設定を加えた記事も書きたいと思っています。

余談(SecurityFilterChain)

詳しくは書きませんが、SecurityFilterChainをConfigファイル内に書くことで、色々と認証・認可の設定を変えることができます。
中身はないですが、以下のようなメソッドを記載し、SecurityFilterChainをBeanに登録することで、このページは認証なしでも見れるとか、権限(role)がないと見れないとか、自作のログインページを設定したり等ができます。というさらっとした紹介ですが、ただログインできるかどうかという今回の記事では、特に設定する必要はありません。

@Bean
public SecurityFilterChain メソッド名(HttpSecurity http) throws Exception {
	return <<SecurityFilterChain>>;
}

余談(bcryptでハッシュ化した値を知る)

例えばパスワードとして「password」という文字列を設定したくて、そのためにハッシュ化した値をDBに自分でコマンド叩いて保存したい場合などに、passwordをハッシュ化した値を知る方法です。
以下をJShelで実行することで簡単にハッシュ化した結果の文字列を得ることができました。

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("password"));

その他の参考リンク

Spring Security リファレンス
Spring Boot 3 プログラミング入門 掌田津耶乃 (著)

Discussion