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

このガイドは、最も基本的な問題以上のものを解決するためのマニュアルまたはレシピとして意図されたものではありません(詳細なリファレンスはこちらにあります)が、初心者と専門家の両方に役立つ可能性があります。Spring Boot は、安全なアプリケーションのデフォルトの動作を提供するため、頻繁に参照されます。Spring Boot は、アーキテクチャ全体にどのように適合するかを理解できます。

メモ
すべての原則は、Spring Boot を使用しないアプリケーションにも同様に適用されます。

認証とアクセス制御

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

認証

認証の主な戦略インターフェースは AuthenticationManager であり、これには 1 つのメソッドしかありません。

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

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

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

  • 入力が無効なプリンシパルを表していると思われる場合は、AuthenticationException をスローします。

  • 決定できない場合は 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 アプリケーションのセキュリティの実装方法の詳細については、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 つの実装があり、AuthenticationProviders への ProviderManager デリゲートと少し似ている、AccessDecisionVoter インスタンスのチェーンへの 3 つのデリゲートすべてがあります。

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 はインターフェースです。メソッドは 1 つしかないため(非常に一般的で String を返します)、これらの文字列は、リソースの所有者のインテンションを何らかの方法でエンコードし、アクセスを許可するユーザーに関するルールを表します。一般的な ConfigAttribute は、ユーザーロールの名前(ROLE_ADMIN や ROLE_AUDIT など)であり、多くの場合、特別な形式(ROLE_ プレフィックスなど)を持っているか、評価が必要な式を表します。

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

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

Web セキュリティ

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

Filter chain delegating to a Servlet

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

Spring Security はチェーンに単一の Filter としてインストールされ、その具体的なタイプは FilterChainProxy です。これについては、後で説明します。Spring Boot アプリケーションでは、セキュリティフィルターは ApplicationContext の @Bean であり、すべてのリクエストに適用されるようにデフォルトでインストールされます。これは、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 である必要はありません。プロキシは FilterChainProxy に委譲します。FilterChainProxy は常に @Bean であり、通常は 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 を使用してユーザーが制御できます)最後のチェーンは、キャッチオールパス(/**)と一致し、認証、認可、例外処理、セッション処理、ヘッダー書き込みのロジックを含む、よりアクティブです。、等々。このチェーンにはデフォルトで合計 11 個のフィルターがありますが、通常、ユーザーはどのフィルターがいつ使用されるかを気にする必要はありません。

メモ
Spring Security の内部にあるすべてのフィルターがコンテナーに認識されないという事実は重要です。特に、デフォルトでタイプ Filter のすべての @Beans がコンテナーに自動的に登録される Spring Boot アプリケーションでは重要です。セキュリティチェーンにカスタムフィルターを追加する場合は、それを @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("/match1/**")
     ...;
  }
}

この 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("/match1/**")
      .authorizeRequests()
        .antMatchers("/match1/user").hasRole("USER")
        .antMatchers("/match1/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

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

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

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

アプリケーションのセキュリティルールをアクチュエーターのエンドポイントに適用する場合は、アクチュエーターのエンドポイントよりも前にオーダーされ、すべてのアクチュエーターのエンドポイントを含むリクエストマッチャーを備えたフィルターチェーンを追加できます。アクチュエーターのエンドポイントのデフォルトのセキュリティ設定を使用する場合は、次のように、アクチュエーターのフィルターよりも遅く、フォールバックよりも前に独自のフィルターを追加するのが最も簡単です(たとえば、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 または SpringWeb スタックの残りの部分には関連付けられていないため、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 は、現在の認証済みプリンシパルをさまざまなダウンストリームコンシューマーが利用できるようにする必要があるため、基本的にスレッドにバインドされています。基本的な構成要素は SecurityContext であり、Authentication が含まれている場合があります(ユーザーがログインすると、明示的に authenticated である Authentication になります)。SecurityContextHolder の静的な便利なメソッドを介して SecurityContext にいつでもアクセスして操作できます。これにより、ThreadLocal が操作されます。次の例は、そのような配置を示しています。

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));
  }

}