このガイドは、Spring Security の入門書であり、フレームワークの設計と基本的な構成要素についてのインサイトを提供します。アプリケーションセキュリティの非常に基本的な部分のみを取り上げますが、そうすることで、Spring Security を使用する開発者が経験した混乱の一部を解消できます。これを行うために、フィルターを使用し、より一般的にはメソッドアノテーションを使用して、Web アプリケーションでセキュリティが適用される方法を見ていきます。このガイドは、安全なアプリケーションがどのように機能し、どのようにカスタマイズできるかを高レベルで理解する必要がある場合、またはアプリケーションのセキュリティについて考える方法を学ぶ必要がある場合に使用します。

このガイドは、最も基本的な問題を解決するためのマニュアルやレシピを目的とするものではありません(他のソースもあります)が、初心者にも専門家にも役立つ可能性があります。Spring Boot は、セキュリティで保護されたアプリケーションのデフォルトの動作を提供し、それがアーキテクチャ全体にどのように適合するかを理解するのに役立つため、多くの言及もされています。すべての原則は、Spring Boot を使用しないアプリケーションにも同様に適用されます。

認証とアクセス制御

アプリケーションのセキュリティは、2 つの多かれ少なかれ独立した問題に要約されます。認証(誰ですか?)と認可(何を認可されていますか?)。時々、混乱を招く可能性のある「認可」の代わりに「アクセス制御」と言われますが、「認可」は他の場所でオーバーロードになるため、そのように考えると役立つ場合があります。Spring Security には、認証と認可を分離するように設計されたアーキテクチャがあり、両方の戦略と拡張ポイントがあります。

認証

認証の主な戦略インターフェースは AuthenticationManager であり、1 つのメソッドのみがあります。

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

AuthenticationManager は、authenticate() メソッドで次の 3 つのことのいずれかを実行できます。

  1. 入力が有効なプリンシパルを表していることを検証できる場合は、Authentication (通常は authenticated=true を使用)を返します。

  2. 入力が無効なプリンシパルを表すと考えられる場合は、AuthenticationException をスローします。

  3. 判断できない場合は null を返します。

AuthenticationException はランタイム例外です。通常、アプリケーションのスタイルや目的に応じて、一般的な方法でアプリケーションによって処理されます。言い換えれば、ユーザーコードは通常、キャッチして処理することは想定されていません。例:Web UI は、認証に失敗したことを示すページを表示し、バックエンド HTTP サービスは、コンテキストに応じて WWW-Authenticate ヘッダーの有無にかかわらず 401 レスポンスを送信します。

AuthenticationManager の最も一般的に使用される実装は ProviderManager であり、AuthenticationProvider インスタンスのチェーンに委譲します。AuthenticationProvider は AuthenticationManager に少し似ていますが、特定の Authentication タイプをサポートするかどうかを呼び出し側が照会できるようにする追加のメソッドがあります。

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);

}

supports() メソッドの Class<?> 引数は、実際には Class<? extends Authentication> です(authenticate() メソッドに渡されるものをサポートするかどうかだけが尋ねられます)。ProviderManager は、AuthenticationProviders のチェーンに委譲することにより、同じアプリケーションで複数の異なる認証メカニズムをサポートできます。ProviderManager が特定の Authentication インスタンスタイプを認識しない場合、スキップされます。

ProviderManager にはオプションの親があり、すべてのプロバイダーが null を返すかどうかを調べることができます。親が利用できない場合、nullAuthentication は AuthenticationException になります。

アプリケーションには、保護されたリソースの論理グループ(パスパターン /api/** に一致するすべての Web リソースなど)があり、各グループが独自の専用 AuthenticationManager を持つことができます。多くの場合、これらはそれぞれ ProviderManager であり、親を共有しています。この場合、親は一種の「グローバル」リソースであり、すべてのプロバイダーのフォールバックとして機能します。

ProviderManagers with a common parent
図 1: ProviderManager を使用した AuthenticationManager 階層

認証マネージャーのカスタマイズ

Spring Security は、一般的な認証マネージャー機能をアプリケーションにすばやく設定するためのいくつかの構成ヘルパーを提供します。最も一般的に使用されるヘルパーは AuthenticationManagerBuilder です。これは、インメモリ、JDBC または LDAP ユーザーの詳細のセットアップ、またはカスタム UserDetailsService の追加に最適です。グローバル(親) AuthenticationManager を構成するアプリケーションの例を次に示します。

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

この例は Web アプリケーションに関連していますが、AuthenticationManagerBuilder の使用はより広く適用できます(Web アプリケーションセキュリティの実装方法の詳細については、以下を参照してください)。AuthenticationManagerBuilder は @Bean のメソッドへの @Autowired であることに注意してください - それがグローバル(親) AuthenticationManager を構築するものです。対照的に、この方法で行った場合:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public void configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

(コンフィギュレーターでメソッドの @Override を使用)、AuthenticationManagerBuilder は、グローバルな AuthenticationManager の子である「ローカル」 AuthenticationManager を構築するためにのみ使用されます。Spring Boot アプリケーションでは、グローバルなものを別の Bean に @Autowired できますが、自分で明示的に公開しない限り、ローカルなものではできません。

Spring Boot は、タイプ AuthenticationManager の独自の Bean を提供して先取りしない限り、デフォルトのグローバル AuthenticationManager (1 人のユーザーのみ)を提供します。デフォルトは、カスタムグローバル AuthenticationManager を積極的に必要としない限り、それ自体をあまり心配する必要がないほど十分に安全です。AuthenticationManager を構築する構成を行う場合、保護しているリソースに対してローカルで行うことができ、グローバルなデフォルトを心配する必要はありません。

認可またはアクセス制御

認証が成功したら、認可に移ることができます。ここでの中核となる戦略は AccessDecisionManager です。フレームワークによって提供される 3 つの実装と AccessDecisionVoter のチェーンへの 3 つのデリゲートすべてがあり、AuthenticationProviders への ProviderManager デリゲートのようなビットです。

AccessDecisionVoter は、Authentication (プリンシパルを表す)と、ConfigAttributes で装飾された安全な Object を考慮します。

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

Object は、AccessDecisionManager および AccessDecisionVoter の署名において完全に汎用的です - ユーザーがアクセスする可能性のあるものをすべて表します(Web リソースまたは Java クラスのメソッドは、最も一般的な 2 つのケースです)。ConfigAttributes もかなり汎用的であり、アクセスに必要な許可のレベルを決定するいくつかのメタデータを持つセキュア Object の装飾を表します。ConfigAttribute はインターフェースですが、非常に汎用的で String を返すメソッドが 1 つしかないため、これらの文字列は何らかの方法でリソースの所有者のインテンションをエンコードし、誰がアクセスできるかについてのルールを表現します。典型的な ConfigAttribute はユーザーロールの名前(ROLE_ADMIN や ROLE_AUDIT など)であり、多くの場合、特別な形式(ROLE_ プレフィックスなど)を持っているか、評価する必要がある式を表します。

ほとんどの人は、AffirmativeBased であるデフォルトの AccessDecisionManager を使用します(投票者が肯定的に戻ると、アクセスが許可されます)。新しいものを追加するか、既存のものが機能する方法を変更するかのいずれかで、投票者はカスタマイズを行う傾向があります。

isFullyAuthenticated() && hasRole('FOO') など、Spring Expression Language(SpEL)式である ConfigAttributes を使用することは非常に一般的です。これは、式を処理し、それらのコンテキストを作成できる AccessDecisionVoter によってサポートされています。処理できる式の範囲を継承するには、SecurityExpressionRoot および場合によっては SecurityExpressionHandler のカスタム実装が必要です。

Web セキュリティ

Web 層の Spring Security(UI および HTTP バックエンド用)は、サーブレット Filters に基づいているため、一般に最初に Filters のロールを確認すると役立ちます。次の図は、単一の HTTP リクエストのハンドラーの典型的な階層化を示しています。

Filter chain delegating to a Servlet

クライアントはアプリケーションにリクエストを送信し、コンテナーはリクエスト URI のパスに基づいて、どのフィルターとどのサーブレットがそれに適用されるかを決定します。多くの場合、1 つのサーブレットで 1 つのリクエストを処理できますが、フィルターはチェーンを形成するため、フィルターは順序付けされます。実際、フィルターは、チェーン自体でリクエストを処理したい場合、残りのチェーンを拒否できます。フィルターは、下流のフィルターやサーブレットで使用されるリクエストやレスポンスを変更することもできます。フィルターチェーンの順序は非常に重要であり、Spring Boot はこれを 2 つのメカニズムで管理します。1 つは、Filter タイプの @Beans は @Order を持つか、Ordered を実装できるということです。もう 1 つは、FilterRegistrationBean の一部になることができ、FilterRegistrationBean 自体はその API の一部として順序を持ちます。市販のフィルターの中には、互いの相対的な順序を示すために独自の定数を定義するものがあります (たとえば、Spring Session の SessionRepositoryFilter には Integer.MIN_VALUE + 50 の DEFAULT_ORDER が搭載されており、チェーンの初期段階であることを示していますが、その前にある他のフィルターも除外していません)。

Spring Security はチェーンに単一の Filter としてインストールされ、その具体的なタイプは FilterChainProxy です。理由はすぐに明らかになります。Spring Boot アプリでは、セキュリティフィルターは ApplicationContext の @Bean であり、すべてのリクエストに適用されるようにデフォルトでインストールされます。SecurityProperties.DEFAULT_FILTER_ORDER で定義された位置にインストールされます。SecurityProperties.DEFAULT_FILTER_ORDER は、FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER (Spring Boot アプリがリクエストをラップしてその動作を変更した場合にフィルターが持つと予想される最大順序)によって固定されます。ただし、それだけではありません。コンテナーの観点からは、Spring Security は単一のフィルターですが、内部には追加のフィルターがあり、それぞれが特別なロールを果たします。これが写真です:

Spring Security Filter
図 2: Spring Security は単一の物理 Filter ですが、内部フィルターのチェーンに処理を委譲する

実際、セキュリティフィルターにはもう 1 つの間接層もあります。通常は、DelegatingFilterProxy としてコンテナーにインストールされますが、Spring @Bean である必要はありません。プロキシは、常に @Bean である FilterChainProxy に委譲します。通常、springSecurityFilterChain の固定名を使用します。チェーン(またはチェーン)のフィルターとして内部的に配置されたすべてのセキュリティロジックを含むのは FilterChainProxy です。すべてのフィルターには同じ API があり(すべてサーブレット仕様から Filter インターフェースを実装しています)、すべてのフィルターにチェーンの残りを拒否する機会があります。

複数のフィルターチェーンがあり、それらはすべて同じ最上位 FilterChainProxy の Spring Security によって管理され、すべてコンテナーに認識されません。Spring Security フィルターには、フィルターチェーンのリストが含まれており、それに一致する最初のチェーンにリクエストをディスパッチします。以下の図は、リクエストパスの一致に基づいて発生するディスパッチを示しています(/foo/** は /** の前に一致します)。これは非常に一般的ですが、リクエストを照合する唯一の方法ではありません。このディスパッチプロセスの最も重要な機能は、1 つのチェーンだけがリクエストを処理することです。

Security Filter Dispatch
図 3: Spring Security FilterChainProxy は、一致する最初のチェーンにリクエストをディスパッチします。

カスタムセキュリティ構成のないバニラ Spring Boot アプリケーションには、いくつかの(n と呼ぶ)フィルターチェーンがあります(通常 n = 6)。最初の(n-1)チェーンは、/css/** や /images/** などの静的リソースパターン、およびエラービュー /error を無視するためだけにあります(SecurityProperties 構成 Bean から security.ignored を使用して、ユーザーがパスを制御できます)。最後のチェーンは catch all パス /** と一致し、よりアクティブで、認証、認可、例外処理、セッション処理、ヘッダー書き込みなどのロジックを含んでいます。このチェーンにはデフォルトで合計 11 個のフィルターがありますが、通常はそうではありませんユーザーがどのフィルターをいつ使用するかを気にする必要があります。

メモ
Spring Security の内部のすべてのフィルターがコンテナーにとって未知であるという事実は、特に Spring Boot アプリケーションでは重要です。このアプリケーションでは、タイプ Filter のすべての @Beans がデフォルトでコンテナーに自動的に登録されます。セキュリティチェーンにカスタムフィルターを追加する場合は、@Bean にしないか、明示的にコンテナー登録を無効にする FilterRegistrationBean にラップする必要があります。

フィルターチェーンの作成とカスタマイズ

Spring Boot アプリ(/** リクエストマッチャーを持つもの)のデフォルトのフォールバックフィルターチェーンには、事前定義された SecurityProperties.BASIC_AUTH_ORDER の順序があります。security.basic.enabled=false を設定して完全にオフにすることも、フォールバックとして使用して、他のルールをより低い順序で定義することもできます。それを行うには、タイプ WebSecurityConfigurerAdapter (または WebSecurityConfigurer)の @Bean を追加し、クラスを @Order で装飾します。例:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}

この Bean により、Spring Security は新しいフィルターチェーンを追加し、フォールバックの前にそれをオーダーします。

多くのアプリケーションでは、あるリソースセットと別のリソースセットでアクセスルールがまったく異なります。たとえば、UI とバッキング API をホストするアプリケーションは、UI パーツのログインページへのリダイレクトを使用した Cookie ベースの認証と、API パーツの未認証リクエストに対する 401 レスポンスを使用したトークンベースの認証をサポートします。リソースの各セットには、一意の順序と独自のリクエストマッチャーを持つ独自の WebSecurityConfigurerAdapter があります。一致するルールが重複する場合、最も早い順序のフィルターチェーンが勝ちます。

ディスパッチと認可のマッチングのリクエスト

セキュリティフィルターチェーン(または同等に WebSecurityConfigurerAdapter)には、HTTP リクエストに適用するかどうかを決定するために使用されるリクエストマッチャーがあります。特定のフィルターチェーンを適用する決定が行われると、他のフィルターは適用されません。ただし、チェーンフィルター内では、HttpSecurity 構成機能で追加のマッチャーを設定することにより、認可をよりきめ細かく制御できます。例:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
      .authorizeRequests()
        .antMatchers("/foo/bar").hasRole("BAR")
        .antMatchers("/foo/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

Spring Security の設定で最も簡単な間違いの 1 つは、これらのマッチャーが異なるプロセスに適用されることを忘れることです。1 つはフィルターチェーン全体のリクエストマッチャーであり、もう 1 つは適用するアクセスルールのみを選択することです。

アプリケーションセキュリティルールとアクチュエータールールの組み合わせ

管理エンドポイントに Spring Boot Actuator を使用している場合、おそらくセキュアにしたいでしょう。デフォルトではセキュアになります。実際、アクチュエーターを安全なアプリケーションに追加するとすぐに、アクチュエーターエンドポイントにのみ適用される追加のフィルターチェーンが得られます。アクチュエーターエンドポイントのみに一致するリクエストマッチャーで定義され、デフォルトの SecurityProperties フォールバックフィルターより 5 少ない ManagementServerProperties.BASIC_AUTH_ORDER の順序があるため、フォールバックの前に調べられます。

アプリケーションセキュリティルールをアクチュエーターエンドポイントに適用する場合は、アクチュエーターチェーンよりも早くオーダーし、すべてのアクチュエーターエンドポイントを含むリクエストマッチャーでオーダーしたフィルターを追加できます。アクチュエーターエンドポイントのデフォルトのセキュリティ設定を希望する場合、最も簡単な方法は、アクチュエーターのフィルターよりも後で、フォールバックよりも早く独自のフィルターを追加することです(例: ManagementServerProperties.BASIC_AUTH_ORDER + 1)。例:

@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}
メモ
Web 層の Spring Security は現在サーブレット API に関連付けられているため、組み込みまたはその他のサーブレットコンテナーでアプリを実行する場合にのみ実際に適用できます。ただし、Spring MVC または Spring Web スタックの残りの部分には結び付けられていないため、たとえば JAX-RS を使用するサーブレットアプリケーションなどで使用できます。

メソッドのセキュリティ

Spring Security は、Web アプリケーションを保護するためのサポートに加えて、Java メソッドの実行にアクセスルールを適用するためのサポートを提供します。Spring Security の場合、これは異なるタイプの「保護されたリソース」です。ユーザーにとって、アクセスルールは同じ形式の ConfigAttribute 文字列(ロールや式など)を使用して宣言されますが、コードの別の場所で宣言されます。最初のステップは、たとえばアプリのトップレベル設定でメソッドのセキュリティを有効にすることです:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

次に、メソッドリソースを直接装飾できます。

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

このサンプルは、安全な方法を使用したサービスです。Spring がこのタイプの @Bean を作成する場合、プロキシ化され、メソッドが実際に実行される前に呼び出し元はセキュリティインターセプターを通過する必要があります。アクセスが拒否された場合、呼び出し元は実際のメソッド結果の代わりに AccessDeniedException を取得します。

メソッドで使用できるセキュリティ制約、特に @PreAuthorize と @PostAuthorize を使用して、メソッドパラメーターと戻り値への参照をそれぞれ含む式を作成できるアノテーションがあります。

ヒント
Web セキュリティとメソッドセキュリティを組み合わせることは珍しくありません。フィルターチェーンは、認証やログインページへのリダイレクトなどのユーザーエクスペリエンス機能を提供し、メソッドセキュリティはより詳細なレベルで保護を提供します。

スレッドの使用

Spring Security は、現在の認証済みプリンシパルをさまざまなダウンストリームコンシューマーが利用できるようにする必要があるため、基本的にスレッドバインドされています。基本的なビルドブロックは、Authentication を含むことができる SecurityContext です(ユーザーがログインすると、明示的に authenticated である Authentication になります)。SecurityContext には、TheadLocal を操作するだけの SecurityContextHolder の静的な簡易メソッドを使用して、いつでもアクセスおよび操作できます。例 :

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

ユーザーアプリケーションコードでこれを行うことは一般的ではありませんが、たとえば、カスタム認証フィルターを記述する必要がある場合に役立ちます(ただし、Spring Security には、必要のない場所で使用できる基本クラスがあります) SecurityContextHolder を使用してください)。

Web エンドポイントで現在認証されているユーザーにアクセスする必要がある場合は、@RequestMapping でメソッドパラメーターを使用できます。例 :

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}

このアノテーションは、現在の Authentication を SecurityContext から引き出し、getPrincipal() メソッドを呼び出してメソッドパラメーターを生成します。Authentication の Principal のタイプは、認証の検証に使用される AuthenticationManager に依存するため、これはユーザーデータへのタイプセーフな参照を取得するための便利な小さなトリックになります。

Spring Security が使用されている場合、HttpServletRequest からの Principal は Authentication タイプになるため、直接使用することもできます。

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

これは、Spring Security が使用されていないときに機能するコードを記述する必要がある場合に役立つことがあります(Authentication クラスのロードについては、より防御する必要があります)。

セキュアなメソッドを非同期的に処理する

SecurityContext はスレッドバインドされているため、セキュアなメソッドを呼び出すバックグラウンド処理を実行する場合は、@Async では、コンテキストが伝播されることを確認する必要があります。これは、バックグラウンドで実行されるタスク(RunnableCallable など)で SecurityContext をラップすることに要約されます。Spring Security は、Runnable および Callable のラッパーなど、これを簡単にするヘルパーを提供します。SecurityContext メソッドを @Async メソッドに伝搬するには、AsyncConfigurer を指定し、Executor が正しいタイプであることを確認する必要があります。

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }

}