Spring Securityで始める認証入門 - Basic認証からフォーム認証まで


初めてSpring Securityを導入したら、突然ログイン画面が出てきて混乱した――そんな経験をした開発者は少なくないでしょう。Spring Securityは強力ですが、その設定の多様さゆえに「どこから手をつければいいのか」と戸惑うことも多いものです。

この記事では、Spring Securityを初めて使う開発者向けに、認証機能の基本を段階的に解説します。最小構成から始めて、Basic認証、フォーム認証へと順を追って実装することで、各設定の意味と初心者がつまずきやすいポイントを丁寧に説明します。

読み終えた後には、Spring Securityの認証の仕組みを理解し、自分のプロジェクトに合った認証方式を選択・実装できる状態になることを目指します。

Spring Securityとは - 認証と認可の違い

Spring Securityは、Springアプリケーションのセキュリティを包括的に担うフレームワークです。ログイン機能やアクセス制御など、セキュリティに関わる幅広い機能を提供します。

まず理解しておきたいのが、認証(Authentication)認可(Authorization) の違いです。

  • 認証: 「誰か」を確認するプロセス(ログイン)
  • 認可: 「何ができるか」を制御するプロセス(権限チェック)

この記事では認証に焦点を当て、認可の詳細(ロールベースのアクセス制御など)は別の記事で扱います。

Spring Securityが提供する主な認証方式には以下があります。

  • Basic認証: HTTPヘッダーでユーザー名とパスワードを送信
  • フォーム認証: Webページのフォームからログイン
  • OAuth2/OpenID Connect: 外部サービス(Google、GitHubなど)と連携
  • JWT: トークンベースの認証

この記事では、Basic認証とフォーム認証を段階的に実装していきます。

Spring Securityのデフォルト動作を確認する

まずは、Spring Securityを導入したときのデフォルト動作を確認しましょう。pom.xmlに以下の依存関係を追加するだけで、自動的に全エンドポイントが保護されます。

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

アプリケーションを起動すると、コンソールに以下のようなログが表示されます。

Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336

このパスワードは 起動ごとに変わります。デフォルトユーザー名のuserと組み合わせてログインできます。ブラウザで任意のエンドポイントにアクセスすると、自動生成されたログインページが表示されます。

セキュリティ上の注意: このデフォルトパスワードは開発時のみ使用してください。固定したい場合はapplication.ymlで以下のように設定できますが、本番環境では必ず無効化する必要があります。

spring:
  security:
    user:
      name: user
      password: dev-password

この自動設定の背後には、SecurityFilterChain という仕組みがあります。次のセクションでこの概念を理解しましょう。

SecurityFilterChainの基本概念

Spring Securityの中核となるのが SecurityFilterChain です。これは、HTTPリクエストに対するセキュリティルール(どのURLを保護するか、どの認証方式を使うかなど)を定義するものです。

Spring Security 6以降では、SecurityFilterChain@Beanとして登録する方法が推奨されています(従来のWebSecurityConfigurerAdapterは非推奨)。また、Lambda DSL記法を使った設定が標準となっています。

基本的なパターンは以下の通りです。

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.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());  // この時点で認証方式を指定
        return http.build();
    }
}

ここでは、@Configurationアノテーションを使って設定クラスを作成し、SecurityFilterChain@Beanとして登録しています。HttpSecurityオブジェクトを使ったビルダーパターンで、セキュリティルールを設定します。

この仕組みは、DIの実践的な応用例でもあります。Spring BootがこのBeanを自動的に検出し、アプリケーション全体のセキュリティ設定として適用します。

.httpBasic(withDefaults())の部分で認証方式を指定しています。この例ではBasic認証を使用しますが、次のセクションで詳しく見ていきましょう。

Basic認証の最小実装

それでは、InMemoryユーザーを使ったBasic認証を実装してみましょう。

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(user);
    }
}

.httpBasic(withDefaults())メソッドでBasic認証を有効にしています。UserDetailsServiceのBeanでは、InMemoryUserDetailsManagerを使ってメモリ上にテストユーザーを作成しています。

重要: 最初からBCryptPasswordEncoderを使っています。古いコード例ではUser.withDefaultPasswordEncoder()が使われていることがありますが、これは Spring Security 5.7以降で完全に非推奨(deprecated) となっており、開発環境でも使用すべきではありません。

Basic認証では、ユーザー名とパスワードをBase64エンコードしてAuthorizationヘッダーで送信します。

curl -u user:password http://localhost:8080/api/hello

または明示的にヘッダーを指定することもできます(以下のdXNlcjpwYXNzd29yZA==user:passwordをBase64エンコードした文字列です)。

curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/hello

ブラウザでアクセスすると、ブラウザ標準の認証ダイアログが表示されます。

パスワードエンコーダーの設定

パスワードを平文で保存すると、データベースが漏洩した際に全ユーザーのパスワードが露出してしまいます。本番環境では必ず パスワードエンコーダー を使って暗号化する必要があります。

前のセクションですでにBCryptPasswordEncoderを導入しましたが、なぜ@Beanとして登録する必要があるのでしょうか。

理由: Spring Securityは認証時に、自動的にApplicationContextからPasswordEncoder型のBeanを探して使用します。ユーザーがログインしようとすると、入力されたパスワードをエンコードし、保存されているエンコード済みパスワードと比較します。この仕組みにより、DIを活用した柔軟な設定が可能になります。

BCryptPasswordEncoderでエンコードされたパスワードは、以下のような形式になります。

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

$2a$で始まるこの文字列は、BCryptアルゴリズムのバージョン、コストファクター、ソルト、ハッシュ値を含んでいます。{bcrypt}のようなプレフィックスは、DelegatingPasswordEncoderを使う場合に付加されるもので、BCryptPasswordEncoder単体では付きません。

フォーム認証への移行

Basic認証はシンプルですが、ブラウザの認証ダイアログは使い勝手がよくありません。一般的なWebアプリケーションでは、フォーム認証がより適しています。

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .defaultSuccessUrl("/home", true)
                .permitAll()
            );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(user);
    }
}

.httpBasic(withDefaults()).formLogin()に置き換えました。Lambda DSL記法を使い、form -> form...の形で設定を記述しています。defaultSuccessUrl("/home", true)でログイン成功時のリダイレクト先を指定し、.permitAll()でログインページ自体へのアクセスを許可しています(これがないと無限リダイレクトが発生します)。

デフォルトのログインページは/loginで自動生成されます。ブラウザでアクセスすると、Spring Securityが提供する標準的なログインフォームが表示されます。

ログインページのカスタマイズ

独自のログインページを使う場合は、以下のように設定します。

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/custom-login", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/custom-login")
                .defaultSuccessUrl("/home", true)
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/custom-login?logout")
                .permitAll()
            );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("password"))
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(user);
    }
}

.loginPage("/custom-login")でカスタムログインページを指定し、.requestMatchers()で静的リソースとログインページへのアクセスを許可しています。

.requestMatchers("/css/**")**Ant形式のパターンマッチング で、「/css/以下の全てのパス」を意味します。たとえば/css/style.css/css/admin/layout.cssなどすべてにマッチします。

ログアウト機能の実装

.logout()メソッドで、ログアウト機能を設定しています。logoutSuccessUrl("/custom-login?logout")により、ログアウト後はログインページにリダイレクトされ、?logoutパラメータでログアウト成功メッセージを表示できます。

Thymeleafを使ったログインページの例は以下の通りです(src/main/resources/templates/custom-login.htmlに配置)。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <div class="login-container">
        <h1>ログイン</h1>
        
        <div th:if="${param.error}" class="error">
            ユーザー名またはパスワードが正しくありません。
        </div>
        
        <div th:if="${param.logout}" class="success">
            ログアウトしました。
        </div>
        
        <form th:action="@{/custom-login}" method="post">
            <div>
                <label for="username">ユーザー名:</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div>
                <label for="password">パスワード:</label>
                <input type="password" id="password" name="password" required>
            </div>
            <button type="submit">ログイン</button>
        </form>
    </div>
</body>
</html>

重要なポイント:

  • フォームのaction属性はth:actionを使ってThymeleafのURL式で指定します。
  • method="post"は必須です。
  • name属性はusernamepasswordである必要があります(カスタマイズも可能ですが、デフォルトはこの名前)。
  • ${param.error}でログインエラーを検出し、エラーメッセージを表示できます。

CSRFトークンについて: Thymeleafを使用している場合、POSTメソッドのフォームには自動的に以下のような隠しフィールドが追加されます。

<input type="hidden" name="_csrf" value="ランダムなトークン値"/>

これはThymeleafがth:actionを処理する際に自動的に行うもので、手動で追加する必要はありません。

静的リソースの配置: CSSファイル(/css/style.css)は、src/main/resources/static/css/style.cssに配置してください。Spring Bootはstaticフォルダ以下のファイルを自動的に静的リソースとして公開します。

初心者がつまずきやすい設定エラーと解決法

Spring Securityを使い始めると、いくつかの典型的なエラーに遭遇します。主なものを紹介します。

1. “There is no PasswordEncoder mapped for the id “null"" エラー

このエラーは、パスワードエンコーダーを設定せずに平文パスワードを使おうとしたときに発生します。

解決法: PasswordEncoder@Beanとして登録し、パスワードをエンコードしてください。

2. ログインページへの無限リダイレクト

ログインページ自体が認証を要求すると、無限リダイレクトが発生します。

解決法: Lambda DSL使用時は、以下のように.permitAll()を記述してください。

.formLogin(form -> form
    .loginPage("/custom-login")
    .permitAll()  // formオブジェクトのメソッドとして呼び出す
)

また、.requestMatchers()でも明示的に許可してください。

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/custom-login").permitAll()
    .anyRequest().authenticated()
)

3. CSRFトークンエラー

フォームにCSRFトークンが含まれていないと、ログインが失敗します。

解決法: Thymeleafのth:actionを使えば自動的に埋め込まれます。手動でHTMLを書く場合やThymeleafを使わない場合は、以下のように追加してください。

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

4. Spring Security 5と6の設定方法の違い

Web上の古い記事ではWebSecurityConfigurerAdapterを継承する方法が紹介されていますが、Spring Security 6以降では非推奨です。また、Lambda DSL記法が標準となりました。

解決法: 本記事で紹介しているSecurityFilterChain@Beanとして登録し、Lambda DSL記法(.formLogin(form -> form...)を使う方法に従ってください。

デバッグログの有効化

トラブルシューティングには、デバッグログが非常に役立ちます。application.ymlに以下を追加してください。

logging:
  level:
    org.springframework.security: DEBUG

これにより、Spring Securityの内部動作が詳細にログ出力されます。

認証方式の選択基準

Basic認証とフォーム認証、どちらを選ぶべきでしょうか。

Basic認証が適している場面

  • REST APIのエンドポイント保護
  • 簡易的な管理画面や開発用ツール
  • ステートレスなアプリケーション
  • curlやPostmanなどのツールからのアクセスが主な場合

フォーム認証が適している場面

  • 一般ユーザー向けのWebアプリケーション
  • ブラウザからのアクセスが主な場合
  • ログイン画面をカスタマイズしたい場合
  • セッション管理が必要な場合

環境による切り替え

Spring BootのProfilesを使って環境によって設定を切り替えることもできます。例えば、開発環境ではBasic認証、本番環境ではフォーム認証といった使い分けが可能です。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    @Bean
    @Profile("dev")
    public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

    @Bean
    @Profile("prod")
    public SecurityFilterChain prodSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .defaultSuccessUrl("/home", true)
                .permitAll()
            );
        return http.build();
    }
}

注意: 同じ型(SecurityFilterChain)の複数のBeanを定義する場合、Profileで明確に分離しないとSpring起動時にエラーが発生する可能性があります。上記の例では@Profileアノテーションで環境を分けているため問題ありませんが、同じProfile内で複数定義する場合は@Primaryアノテーションや@ConditionalOnPropertyを使った条件分岐が必要です。

次のステップ

この記事では、Spring Securityの基本的な認証機能を段階的に実装しました。ここから先、以下のようなステップに進むことができます。

  • データベース連携: UserDetailsServiceを実装してデータベースからユーザー情報を取得
  • OAuth2/OpenID Connect: GoogleやGitHubなどの外部サービスと連携したログイン
  • JWT認証: トークンベースの認証でステートレスなAPIを構築
  • 認可(Authorization): ロールベースのアクセス制御(@PreAuthorizeなど)

これらの発展的トピックについては、今後の記事で扱う予定です。まずは、この記事で学んだ基本をしっかり理解し、自分のプロジェクトで試してみることをお勧めします。

よくある質問

Q: Spring Securityを使うと何も設定しなくてもログイン画面が出るのはなぜ?

A: Spring Bootの自動設定機能により、spring-boot-starter-securityを依存関係に追加するだけで、デフォルトのSecurityFilterChainが自動的に構成されます。これにより、全エンドポイントが保護され、デフォルトのログインページが表示されます。

Q: Basic認証とフォーム認証はどう使い分ければいい?

A: API保護や簡易的な管理画面にはBasic認証、一般ユーザー向けのWebアプリケーションにはフォーム認証が適しています。ブラウザからのアクセスが主な場合はフォーム認証を選択するのが一般的です。

Q: “There is no PasswordEncoder mapped for the id null” エラーが出た時の対処法は?

A: PasswordEncoder@Beanとして登録し、パスワードをエンコードしてください。本番環境では必ずBCryptPasswordEncoderなどの安全なエンコーダーを使用する必要があります。Spring Securityは認証時に自動的にこのBeanを探して使用します。

Q: WebSecurityConfigurerAdapterを使った古い記事との違いは?

A: Spring Security 5.7以降、WebSecurityConfigurerAdapterは非推奨になりました。現在はSecurityFilterChain@Beanとして登録する方法が推奨されています。また、Spring Security 6以降ではLambda DSL記法(.formLogin(form -> form...)が標準となっています。機能的には同じですが、設定方法が異なります。

Q: パスワードを平文で保存してはいけない理由は?

A: データベースが漏洩した際に全ユーザーのパスワードが露出してしまうためです。また、多くのユーザーは複数のサービスで同じパスワードを使い回しているため、被害が拡大する可能性があります。必ずBCryptPasswordEncoderなどで暗号化してください。

Q: CSRFトークンとは何で、なぜログインフォームに必要?

A: CSRF(Cross-Site Request Forgery)は、悪意のあるサイトから意図しないリクエストを送信させる攻撃です。CSRFトークンは、正規のフォームから送信されたリクエストかどうかを検証するためのランダムな値です。Spring SecurityはデフォルトでCSRF保護を有効にしており、ThymeleafがPOSTメソッドのフォームに自動的に<input type="hidden" name="_csrf" ...>を追加します。

Q: 認証と認可の違いは?

A: 認証は「誰か」を確認するプロセス(ログイン)で、認可は「何ができるか」を制御するプロセス(権限チェック)です。この記事では認証に焦点を当てています。

Q: InMemoryユーザーから実際のデータベース連携に移行するには?

A: UserDetailsServiceインターフェースを実装し、データベースからユーザー情報を取得する独自のサービスを作成します。JpaRepositoryなどを使ってユーザーエンティティを管理し、loadUserByUsernameメソッドで取得したユーザー情報を返すようにします。具体的なコード例や詳細な実装手順については、今後の記事で扱う予定です。