Spring Security入門|認証 / 認可とSecurityFilterChain設定

Spring Bootでログイン機能や管理者向けのURLを実装すると、認証と認可をどこで設定するのか、未ログイン時に何が返るのかが気になってきます。Spring Securityは機能が多い一方、入門段階で確認する範囲を絞れば、基本的な仕組みと設定の流れを整理できます。

この記事では、Spring Securityの認証 / 認可の違いから、現在の基本構成であるSecurityFilterChain、URLごとのアクセス制御、フォームログインやHTTP Basic、CSRF、セッション管理までを説明します。Spring Bootのバージョンや独自設定によって挙動が変わる部分にも注意しながら、認証設定をコードから追える形で見ていきます。

目次

Spring Security入門で押さえる範囲

Spring Securityは、Springベースのアプリケーションに認証、認可、一般的な攻撃への対策を組み込むためのフレームワークです。すべての機能を最初から理解する必要はありません。まずは、誰がアクセスしているかを確認する仕組みと、何を許可するかを決める仕組みを分けて考えることが出発点になります。

Spring Securityが担う認証 / 認可と主な保護機能

Spring Securityが担う主な役割は、ユーザーの本人確認である認証、認証済みユーザーの権限を確認する認可、そしてCSRFなど一般的な攻撃への対策です。

たとえば、次のような制御を実装できます。

  • ログイン済みユーザーだけがページを閲覧できるようにする
  • 管理者だけが/admin/**へアクセスできるようにする
  • 未認証のリクエストにログイン画面や401を返す
  • セッションを使うWebアプリケーションでCSRFを検証する

ServletベースのSpringアプリケーションでは、Spring Securityは主にサーブレットフィルターを通してリクエストを処理します。そのため、Controllerの各メソッドで毎回認証状態を確認するのではなく、アプリケーションの処理へ到達する前の段階でセキュリティ処理を行う構成が基本です。

認証と認可の違い

認証は、アクセスしているユーザーが誰なのかを確認することです。ユーザー名とパスワードの検証や、トークンの検証などが該当します。

認可は、認証されたユーザーにどの操作を許可するかを決めることです。たとえば、ログイン済みであっても、管理者専用ページは管理者だけに許可します。

  • 認証:そのユーザーは誰か
  • 認可:そのユーザーに何を許可するか

ログイン画面で認証情報を確認する処理と、ログイン後に管理画面へのアクセス権を判定する処理は別のものです。この違いを意識すると、authenticated()やhasRole()の役割も理解しやすくなります。

フィルターを中心としたリクエスト保護

Spring Securityでは、複数のフィルターが連携してリクエストを処理します。認証情報を読み取り、ユーザー情報を確認し、認証結果をセキュリティコンテキストへ設定したうえで、URLや権限に基づいてアクセス可否を判定します。

アプリケーション開発者は、これらのフィルターを通常個別に組み立てるのではなく、SecurityFilterChainを通して必要な認証方式や認可ルールを設定します。

Spring Securityの導入とバージョン依存の注意点

Spring Securityの設定方法は、Spring SecurityやSpring Bootのバージョンによって異なる場合があります。ここでは、現在のSpring Securityで基本となるJava設定を前提に説明します。

依存関係の追加とバージョン管理(Maven / Gradle)

Spring Bootで導入する場合は、通常spring-boot-starter-securityを追加します。Spring Bootの親プロジェクトや依存関係管理を利用している場合、Spring Bootが対応するSpring Securityのバージョンを管理するため、個別にバージョンを指定しない構成が一般的です。

Mavenでは、次の依存関係を追加します。

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

Gradleでは次のように記述します。

implementation 'org.springframework.boot:spring-boot-starter-security'

Spring Initializrでプロジェクトを作成する場合は、Spring Securityを依存関係として選択できます。利用しているSpring Bootのバージョンを確認し、そのバージョンが管理するSpring Securityの仕様に合わせて設定してください。

Spring Bootで依存関係を追加したときのデフォルト挙動

Spring Bootのセキュリティ自動構成が有効な状態でSpring Securityをクラスパスに追加すると、明示的な設定がない場合にWebアプリケーションのエンドポイントが保護されます。生成されるユーザー情報やログイン方式などは、Spring BootやSpring Securityのバージョン、アプリケーションの構成によって異なります。

そのため、「依存関係を追加すれば常に同じ認証設定になる」とは考えないでください。独自のSecurityFilterChainや認証関連Beanを定義した場合、自動構成の一部がそのまま適用されないことがあります。

ブラウザからアクセスしたときにログイン画面が表示されたり、APIクライアントからのアクセスで401が返ったりすることがあります。ただし、これはすべての環境で同じ表示や応答になると決まっているわけではなく、有効な設定とリクエストの内容によって変わります。

古いWebSecurityConfigurerAdapterを使わない理由

以前のSpring Securityでは、WebSecurityConfigurerAdapterを継承して設定する方法が広く使われていました。しかし、現在の基本的なJava設定では、SecurityFilterChainをBeanとして定義する構成を採用します。

古い記事を参考にすると、次のようなコードが見つかる場合があります。

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 古い構成で使われていた設定方法
}

利用しているバージョンで使用できるかを確認せず、古い構成をそのまま新しいプロジェクトへ持ち込むと、コンパイルエラーや設定方法の違いに遭遇します。新しく設定を始める場合は、SecurityFilterChainを使う構成を基本にするとよいでしょう。

SecurityFilterChainの基本設定

SecurityFilterChainは、どのリクエストを許可するか、どの認証方式を使うか、CSRFやセッションをどう扱うかなどを設定するBeanです。

公開URLと認証必須URLの定義

URLごとのルールを作るときは、公開するURLと認証を求めるURLを最初に分けます。たとえば、トップページやログイン画面、静的ファイルは公開し、それ以外の画面はログイン必須にする構成です。

管理者専用URLがある場合は、ログイン済みであるだけでなく、管理者ロールを持っていることも条件にします。

authorizeHttpRequestsとrequestMatchersの使い方

authorizeHttpRequestsは、HTTPリクエスト単位の認可ルールを設定するためのメソッドです。requestMatchersで対象となるURLパターンを指定し、その後ろにpermitAll()やauthenticated()などの条件を書きます。

http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/", "/about").permitAll()
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    );

この例では、トップページと/aboutは公開され、/admin/**は管理者ロールが必要です。それ以外のURLには認証が必要になります。

requestMatchersの記述順とanyRequestの位置

認可ルールは、記述する順序を意識して定義します。広い条件を先に置くと、その条件が先に適用され、後ろに書いた具体的なルールが意図どおり評価されないことがあります。

一般的には、より具体的なURLを先に書き、最後にanyRequest()で残りのリクエストを扱います。

http
    .authorizeHttpRequests(auth -> auth
        // 管理者向けの具体的な条件を先に書く
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .requestMatchers("/user/**").authenticated()

        // その他のURLを最後にまとめて扱う
        .anyRequest().permitAll()
    );

anyRequest()がJava文法上必ず最後でなければならないという意味ではありません。ただし、残りすべてのリクエストに適用する条件であるため、通常は最後に置きます。

permitAllとauthenticatedの使い分け

permitAll()は、認証していないユーザーを含めてアクセスを許可する指定です。authenticated()は、認証済みユーザーに限定する指定です。

  • permitAll():認証なしでアクセスを許可する
  • authenticated():認証済みユーザーに限定する

ログイン画面やログイン処理に必要なURL、CSS、JavaScript、画像などを認証必須にすると、ログイン画面が表示できなかったり、画面の見た目が崩れたりします。公開対象を確認したうえで、必要なURLだけにpermitAll()を指定します。

hasRoleとROLE_ADMINの関係

hasRole("ADMIN")は、通常はROLE_ADMINという権限に対応します。つまり、hasRole("ADMIN")を指定した場合、ユーザーにROLE_ADMINが付与されている構成が一般的です。

.requestMatchers("/admin/**").hasRole("ADMIN")

ただし、ROLE_という接頭辞の扱いは設定によって変更できます。常に固定された仕様として扱うのではなく、GrantedAuthorityDefaultsなどの設定や、ユーザーへ付与している権限名を確認してください。

UserDetailsServiceでユーザー情報を設定する方法

認証では、送信されたユーザー名に対応するユーザー情報を取得し、パスワードや権限を確認します。入門用の固定ユーザーであれば、InMemoryUserDetailsManagerを使ってUserDetailsServiceを定義できます。

実際のアプリケーションでは、データベースなどからユーザーを取得する独自のUserDetailsServiceを実装することが一般的です。

PasswordEncoderでパスワードを扱う方法

パスワードは平文のまま保存せず、ハッシュ化して扱います。Spring Securityでは、PasswordEncoderを利用してパスワードを検証します。

入門用のコードでは、次のようにPasswordEncoderをBeanとして定義できます。

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

withDefaultPasswordEncoder()のような簡略化された記述は、学習用の例として使われることがあります。本番環境では、パスワードの保存方法やハッシュ方式を確認し、適切に管理してください。

認証設定まで含めたSecurityFilterChainのコード例

次の例では、公開URL、管理者専用URL、認証必須URL、フォームログイン、ユーザー情報、パスワードエンコーダーを1つの構成から確認できます。Spring Bootのバージョンや、すでに定義しているBeanによっては調整が必要です。

package com.example.demo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // パスワードのエンコードと検証に使うエンコーダー
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

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

        UserDetails admin = User.withUsername("admin")
                .password(passwordEncoder.encode("admin-password"))
                .roles("USER", "ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // 誰でもアクセスできるURL
                .requestMatchers("/", "/about", "/css/**", "/js/**").permitAll()

                // ADMINロールを持つユーザーだけがアクセスできるURL
                .requestMatchers("/admin/**").hasRole("ADMIN")

                // それ以外はログイン必須
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());

        return http.build();
    }
}

roles("ADMIN")は、通常ROLE_ADMINに相当する権限をユーザーへ付与します。そのため、この例のadminユーザーは/admin/**へアクセスできます。実際のアプリケーションでは、ユーザー情報をメモリに固定せず、データベースなどから取得する構成へ置き換えます。

formLoginとhttpBasicの使い分け

formLoginとhttpBasicは、どちらもユーザー名とパスワードを使う認証方式ですが、認証情報の送信方法や認証失敗時の応答、セッションとの関係が異なります。

formLoginの認証方式と画面遷移

formLoginは、HTMLフォームからユーザー名とパスワードを送信して認証する方式です。保護されたページへ未認証でアクセスした場合、設定に応じてログイン画面へリダイレクトし、認証成功後に元のページへ戻る流れを作れます。

http
    .formLogin(form -> form
        .loginPage("/login")
        .permitAll()
    );

自前のログイン画面を使う場合は、その画面やログイン処理に必要なURLを公開します。ログイン画面を指定しない場合の表示や処理は、設定や利用バージョンによって異なります。

httpBasicの認証方式とHTTPレスポンス

httpBasicは、HTTP Basic認証を利用する方式です。クライアントはリクエストのAuthorizationヘッダーに認証情報を付けて送信します。認証できない場合は、通常401の応答やWWW-Authenticateヘッダーによって、認証が必要であることをクライアントへ伝えます。

http
    .httpBasic(Customizer.withDefaults());

通信を暗号化しないHTTPでBasic認証を使うと、認証情報が安全に保護されません。実際に利用する場合は、HTTPSを前提に通信経路を保護します。

画面向け・API向けだけで判断しない使い分け

formLoginを画面向け、httpBasicをAPI向けと覚えるだけでは不十分です。両者は、認証方式、認証失敗時の応答、リダイレクトの有無、認証状態をセッションで維持するかどうかなどを含めて判断します。

フォームログインは、ブラウザで画面遷移を行うアプリケーションと組み合わせやすく、セッションに認証状態を保持する構成がよく使われます。一方、HTTP Basicはリクエストごとに認証情報を送る方式で、APIクライアントや内部ツールから扱いやすい場合があります。

ただし、APIであれば必ずHTTP Basic、画面であれば必ずフォームログインという決まりはありません。Cookieを使うのか、セッションを使うのか、トークンを使うのか、クライアントがリダイレクトを処理できるのかを確認して選択します。

ログイン画面へのリダイレクトと401 / 403の違い

未認証のユーザーが保護されたリソースへアクセスした場合は、認証が必要な状態です。フォームログインではログイン画面へのリダイレクトとして表れることがあり、APIでは401が返る構成が考えられます。

一方、認証は済んでいるものの必要な権限を持っていない場合は、通常403で表します。

  • 未認証:認証が必要。401やログイン画面へのリダイレクトなど
  • 認証済みだが権限不足:アクセスを許可できない。403

実際の応答は、認証方式やAuthenticationEntryPoint、AccessDeniedHandlerなどの設定によって変わります。画面とAPIで異なる応答を返したい場合は、リクエストの種類に応じた例外処理を設計します。

CSRFとセッション管理の考え方

CSRFとセッション管理は、画面を持つWebアプリケーションとAPIで判断が分かれやすい部分です。単に開発中だから無効化するのではなく、認証情報がどのように送信される構成なのかを確認します。

CSRF保護が必要になる条件

CSRFは、ユーザーがログイン済みであることを利用し、ユーザーの意図しないリクエストを別のサイトから送信させる攻撃です。ブラウザがCookieなどの認証情報を自動的に送信する構成では、CSRF対策が問題になります。

Spring Securityでは、画面を持つセッション認証のアプリケーションに対して、POST、PUT、DELETEなどの状態を変更するリクエストを中心にCSRF保護が適用されます。具体的な対象や設定は、利用バージョンと構成を確認してください。

セッション認証のWebアプリでCSRFを扱う方法

フォームログインでCookieを使ったセッション認証を行うWebアプリでは、CSRF保護を有効にしたまま利用するのが基本です。フォームから状態変更リクエストを送信する場合は、CSRFトークンをリクエストに含めます。

Thymeleafなどのテンプレートエンジンや、Spring Securityと連携する仕組みを使う場合は、フォームへCSRFトークンを組み込めることがあります。具体的な記述方法は、使用しているテンプレートやSpring Securityの構成に合わせて確認してください。

ステートレスAPIで無効化を検討する条件

ステートレスAPIでは、CSRF保護を無効化する構成が検討されることがあります。ただし、「APIだから不要」「セッションを使わないから必ず安全」とは断定できません。

少なくとも、次の点を確認します。

  • 認証状態をHTTPセッションやCookieで保持していないか
  • ブラウザがCookieを自動送信する構成ではないか
  • HTTP Basicなど、ブラウザが認証情報を自動的に送信し得る方式ではないか
  • Authorizationヘッダーのトークンをクライアントが明示的に付ける構成か
  • 認証情報の保存場所やCORSなど、別の保護策が適切か

条件を確認したうえで、CSRF保護を無効化する場合は、設定理由をコードや設計書に残します。

理由を確認せずCSRFを無効化しない注意点

開発中に403を回避するため、次のような設定を追加したくなることがあります。

http
    .csrf(csrf -> csrf.disable());

しかし、CSRFトークンの扱いに問題があるだけなら、無効化ではなくフォームやクライアント側の送信方法を確認すべき場合があります。セッションやCookieを使うWebアプリで理由なく無効化すると、CSRFへの保護を失う可能性があります。

SecurityContextとHTTPセッションの関係

フォームログインなどの構成では、認証結果を次のリクエストでも利用できるように、セキュリティコンテキストをHTTPセッションと関連付けて保持する構成がよく使われます。

ただし、SecurityContextが常にHTTPセッションへ保存されるわけではありません。SecurityContextRepositoryやセッション管理の設定によって保持方法は変わり、ステートレスな構成ではHTTPセッションを利用しない場合もあります。

APIでSessionCreationPolicy.STATELESSを設定する例は次のとおりです。これはセッションを使わない認証方式を採用する場合の設定例であり、設定すれば認証そのものが自動的に完成するわけではありません。

import org.springframework.security.config.http.SessionCreationPolicy;

http
    .sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );

セッションを使うのか、各リクエストでトークンなどを検証するのかを先に決め、その方針に合わせてCSRFとセッション管理を設計します。

CSRFの無効化は、画面の有無だけで決めません。認証情報がCookieやHTTP Basicなどでブラウザから自動送信される可能性と、セッションを利用するかどうかを確認して判断してください。

Spring Securityでよくあるつまずき

すべてのURLに認証がかかる原因

Spring Bootのセキュリティ自動構成が有効な状態で、独自の認可ルールを定義していない場合、Webアプリケーションのエンドポイントが保護されることがあります。また、anyRequest().authenticated()を設定すると、先に公開指定したURLを除くすべてのリクエストで認証が必要になります。

公開したいトップページ、ログイン画面、静的ファイルなどにpermitAll()を指定し、URLパターンが実際のリクエストと合っているか確認します。

ログイン画面が表示される理由

ブラウザで保護されたURLへアクセスしたときにログイン画面が表示されるのは、フォームログインが有効になっている構成で、未認証リクエストがログイン処理へ誘導されている可能性があります。

ただし、ブラウザアクセスでは常にformLogin、APIアクセスでは常にhttpBasicになると決まっているわけではありません。実際の挙動は、明示したSecurityFilterChain、Spring Bootの自動構成、リクエストのAcceptヘッダーなどによって異なります。

permitAllを設定してもアクセスできない場合

permitAll()を指定してもアクセスできない場合は、認可ルールだけでなく、次の点を確認します。

  • requestMatchersのURLパターンが実際のパスと一致しているか
  • コンテキストパスや末尾のスラッシュの影響がないか
  • 別のSecurityFilterChainがリクエストを処理していないか
  • 認証後に別のURLへリダイレクトされていないか
  • Controllerや静的リソース側で別のエラーが起きていないか

CSSやJavaScriptが読み込めない場合

ログイン画面やページ自体は表示されるものの、CSSやJavaScriptが読み込めない場合、静的ファイルのURLが認証対象になっている可能性があります。

http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/login", "/css/**", "/js/**", "/images/**")
            .permitAll()
        .anyRequest().authenticated()
    );

ブラウザの開発者ツールなどで、実際に403やリダイレクトが発生しているURLを確認すると、対象パターンを特定しやすくなります。

403が返るときに確認する権限設定

ログイン済みなのに403が返る場合は、認証ではなく認可で拒否されている可能性があります。hasRole("ADMIN")を使っているなら、ユーザーへ通常ROLE_ADMINに対応する権限が付与されているか確認します。

また、roles("ADMIN")とauthorities("ADMIN")では権限名の扱いが異なるため、ユーザー情報の設定と認可ルールを照らし合わせます。ROLE_接頭辞を変更する設定がないかも確認してください。

まとめ

Spring Securityの入門では、次の関係を押さえると設定を読み解きやすくなります。

  • 認証はユーザーが誰かを確認する仕組み
  • 認可は認証済みユーザーに何を許可するか決める仕組み
  • SecurityFilterChainで認可ルールや認証方式を設定する
  • requestMatchersは具体的なルールを先に書き、通常はanyRequest()を最後に置く
  • permitAll()は公開、authenticated()はログイン必須を表す
  • hasRole("ADMIN")は通常ROLE_ADMINに対応するが、設定によって変更できる
  • formLoginとhttpBasicは、認証方式、応答、リダイレクト、セッション利用を含めて使い分ける
  • CSRFはセッションやCookieなど、認証情報の送信方法を確認してから判断する
  • 401は未認証、403は認証済みだが権限不足という整理が基本

まずは公開URLと保護URLを分け、ユーザー情報とパスワードの扱いを確認しながら、アプリケーションの認証方式を決めていくと、Spring Securityの設定を段階的に理解できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次