9. アーキテクチャと実装

ネームスペース構成ベースのアプリケーションの設定と実行に慣れたら、フレームワークがネームスペースファサードの背後で実際にどのように機能するかについての理解を深めることをお勧めします。ほとんどのソフトウェアと同様に、Spring Security には、フレームワーク全体で一般的に使用される特定の中央インターフェース、クラス、概念抽象があります。リファレンスガイドのこのパートでは、これらのいくつかを見て、Spring Security 内で認証とアクセス制御をサポートするためにそれらがどのように連携するかを見ていきます。

9.1 技術概要

9.1.1 ランタイム環境

Spring Security 5.2.8.BUILD-SNAPSHOT には、Java 8 ランタイム環境以降が必要です。Spring Security は自己完結型の方法で動作することを目的としているため、Java ランタイム環境に特別な構成ファイルを配置する必要はありません。特に、特別な Java Authentication and Authorization Service(JAAS)ポリシーファイルを構成したり、Spring Security を共通のクラスパスの場所に配置したりする必要はありません。

同様に、EJB コンテナーまたはサーブレットコンテナーを使用している場合は、特別な構成ファイルをどこかに配置する必要も、Spring Security をサーバークラスローダーに含める必要もありません。必要なファイルはすべてアプリケーションに含まれます。

この設計は、あるシステムから別のシステムにターゲットアーティファクト(JAR、WAR、または EAR)をコピーするだけですぐに機能するため、デプロイ時間の柔軟性が最大になります。

9.1.2 コアコンポーネント

Spring Security 3.0 の時点で、spring-security-core jar の内容は最小限に抑えられました。Web アプリケーションのセキュリティ、LDAP、名前空間の構成に関連するコードは含まれなくなりました。ここでは、コアモジュールにあるいくつかの Java 型を見ていきます。これらはフレームワークのビルドブロックを表すため、単純な名前空間の構成を超える必要がある場合は、実際に直接対話する必要がない場合でも、それらが何であるかを理解することが重要です。

SecurityContextHolder、SecurityContext、および認証オブジェクト

最も基本的なオブジェクトは SecurityContextHolder です。これは、現在アプリケーションを使用しているプリンシパルの詳細を含む、アプリケーションの現在のセキュリティコンテキストの詳細を格納する場所です。デフォルトでは、SecurityContextHolder は ThreadLocal を使用してこれらの詳細を保存します。つまり、セキュリティコンテキストがそれらのメソッドへの引数として明示的に渡されない場合でも、セキュリティコンテキストは常に同じ実行スレッドのメソッドで利用できます。この方法で ThreadLocal を使用することは、現在のプリンシパルのリクエストが処理された後にスレッドをクリアするように注意を払えば、非常に安全です。もちろん、Spring Security がこれを自動的に処理するため、心配する必要はありません。

一部のアプリケーションは、スレッドを処理する特定の方法のため、ThreadLocal の使用に完全には適していません。例: Swing クライアントは、Java 仮想マシンのすべてのスレッドに同じセキュリティコンテキストを使用させたい場合があります。SecurityContextHolder は、起動時の戦略で構成して、コンテキストの保存方法を指定できます。スタンドアロンアプリケーションの場合は、SecurityContextHolder.MODE_GLOBAL 戦略を使用します。他のアプリケーションでは、セキュアスレッドによって生成されたスレッドも同じセキュリティ ID を想定する場合があります。これは、SecurityContextHolder.MODE_INHERITABLETHREADLOCAL を使用して実現されます。2 つの方法で、デフォルトの SecurityContextHolder.MODE_THREADLOCAL からモードを変更できます。1 つ目はシステムプロパティを設定すること、2 つ目は SecurityContextHolder の静的メソッドを呼び出すことです。ほとんどのアプリケーションはデフォルトから変更する必要はありませんが、変更する場合は、SecurityContextHolder の JavaDoc を参照して詳細を確認してください。

現在のユーザーに関する情報を取得する

SecurityContextHolder 内には、現在アプリケーションと対話しているプリンシパルの詳細が格納されます。Spring Security は、Authentication オブジェクトを使用してこの情報を表します。通常、Authentication オブジェクトを自分で作成する必要はありませんが、ユーザーが Authentication オブジェクトを照会することはかなり一般的です。次のコードブロックを使用して、アプリケーションのどこからでも、現在認証されているユーザーの名前を取得できます(例:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getContext() の呼び出しによって返されるオブジェクトは、SecurityContext インターフェースのインスタンスです。これは、スレッドローカルストレージに保持されるオブジェクトです。以下で説明するように、Spring Security 内のほとんどの認証メカニズムは、プリンシパルとして UserDetails のインスタンスを返します。

UserDetailsService

上記のコードフラグメントから注意すべきもう 1 つの項目は、Authentication オブジェクトからプリンシパルを取得できることです。プリンシパルは単なる Object です。ほとんどの場合、これは UserDetails オブジェクトにキャストできます。UserDetails は、Spring Security のコアインターフェースです。これはプリンシパルを表しますが、拡張可能でアプリケーション固有の方法です。UserDetails は、独自のユーザーデータベースと Spring Security が SecurityContextHolder 内で必要とするものとの間のアダプターと考えてください。独自のユーザーデータベースからの何かの表現であるため、アプリケーションが提供した元のオブジェクトに UserDetails をキャストすることがよくあるため、ビジネス固有のメソッド(getEmail()getEmployeeNumber() など)を呼び出すことができます。

おそらく疑問に思っているため、いつ UserDetails オブジェクトを提供するのですか? それ、どうやったら出来るの? このことは宣言的であり、Java コードを書く必要はないと言っていたと思います。簡単な答えは、UserDetailsService と呼ばれる特別なインターフェースがあるということです。このインターフェースの唯一のメソッドは、String ベースのユーザー名引数を受け入れ、UserDetails を返します。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

これは、Spring Security 内のユーザーの情報をロードするための最も一般的なアプローチであり、ユーザーに関する情報が必要なときはいつでもフレームワーク全体で使用されることがわかります。

認証に成功すると、UserDetails を使用して、SecurityContextHolder に格納されている Authentication オブジェクトを作成します(これについては以下で詳しく説明します)。幸いなことに、メモリ内マップを使用するもの(InMemoryDaoImpl)と JDBC を使用するもの(JdbcDaoImpl)を含む、多数の UserDetailsService 実装を提供しています。ただし、ほとんどのユーザーは独自に記述する傾向がありますが、多くの場合、実装は従業員、顧客、またはアプリケーションの他のユーザーを表す既存のデータアクセスオブジェクト(DAO)の上に単純に置かれます。UserDetailsService が返すものはすべて、上記のコードフラグメントを使用して SecurityContextHolder から常に取得できるという利点を思い出してください。

[Note] メモ

UserDetailsService についてはしばしば混乱があります。これは純粋にユーザーデータの DAO であり、そのデータをフレームワーク内の他のコンポーネントに提供する以外の機能は実行しません。特に、AuthenticationManager によって実行されるユーザーの認証は行いません。多くの場合、カスタム認証プロセスが必要な場合は、AuthenticationProvider を直接実装する方が理にかなっています。

GrantedAuthority

プリンシパルに加えて、Authentication が提供する別の重要なメソッドは getAuthorities() です。このメソッドは、GrantedAuthority オブジェクトの配列を提供します。GrantedAuthority は、当然のことながら、プリンシパルに付与される権限です。そのような権限は通常、ROLE_ADMINISTRATOR や ROLE_HR_SUPERVISOR などの「ロール」です。これらのロールは、後で Web 認可、メソッド認可、ドメインオブジェクト認可用に構成されます。Spring Security の他の部分は、これらの権限を解釈でき、それらが存在することを期待しています。GrantedAuthority オブジェクトは、通常 UserDetailsService によってロードされます。

通常、GrantedAuthority オブジェクトはアプリケーション全体の権限です。特定のドメインオブジェクトに固有ではありません。Employee オブジェクト番号 54 へのアクセス許可を表す GrantedAuthority を持っている可能性は低いでしょう。なぜなら、そのような権限が何千もあると、すぐにメモリ不足になるからです(または、少なくとも、アプリケーションに時間がかかるからです)。ユーザーを認証する時間)。もちろん、Spring Security はこの一般的な要件を処理するように明示的に設計されていますが、代わりにプロジェクトのドメインオブジェクトセキュリティ機能をこの目的に使用します。

要約

要約すると、これまで見てきた Spring Security の主要な構成要素は次のとおりです。

  • SecurityContextHolderSecurityContext へのアクセスを提供します。
  • SecurityContextAuthentication および場合によってはリクエスト固有のセキュリティ情報を保持します。
  • Authentication。Spring セキュリティ固有の方法でプリンシパルを表します。
  • GrantedAuthority。プリンシパルに付与されたアプリケーション全体の許可を反映します。
  • UserDetails。アプリケーションの DAO またはその他のセキュリティデータソースから認証オブジェクトを構築するために必要な情報を提供します。
  • UserDetailsService は、String ベースのユーザー名(または証明書 ID など)で渡されたときに UserDetails を作成します。

これらの繰り返し使用されるコンポーネントについて理解できたため、認証のプロセスを詳しく見てみましょう。

9.1.3 認証

Spring Security は、さまざまな認証環境に参加できます。認証には Spring Security を使用し、既存のコンテナー管理認証とは統合しないことをお勧めしますが、独自の認証システムとの統合と同様に、サポートされています。

Spring Security の認証とは何ですか?

誰もが知っている標準的な認証シナリオを考えてみましょう。

  1. ユーザーは、ユーザー名とパスワードでログインするよう求められます。
  2. システムは(正常に)パスワードがユーザー名に対して正しいことを確認します。
  3. そのユーザーのコンテキスト情報が取得されます(ロールのリストなど)。
  4. ユーザーのセキュリティコンテキストが確立されます
  5. ユーザーは、現在のセキュリティコンテキスト情報に対して操作に必要な権限をチェックするアクセス制御メカニズムによって潜在的に保護されている操作を実行する可能性があります。

最初の 4 つの項目は認証プロセスを構成するため、これらが Spring Security 内でどのように行われるかを見ていきます。

  1. ユーザー名とパスワードが取得され、UsernamePasswordAuthenticationToken のインスタンス(前に見た Authentication インターフェースのインスタンス)に結合されます。
  2. トークンは、検証のために AuthenticationManager のインスタンスに渡されます。
  3. AuthenticationManager は、認証に成功すると、完全に読み込まれた Authentication インスタンスを返します。
  4. セキュリティコンテキストは、SecurityContextHolder.getContext().setAuthentication(…​) を呼び出して、返された認証オブジェクトを渡すことによって確立されます。

その時点から、ユーザーは認証されたと見なされます。例としていくつかのコードを見てみましょう。

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
    System.out.println("Please enter your username:");
    String name = in.readLine();
    System.out.println("Please enter your password:");
    String password = in.readLine();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
    } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
    }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
            SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
}
}

ここでは、ユーザーにユーザー名とパスワードの入力を要求し、上記のシーケンスを実行する小さなプログラムを作成しました。ここで実装した AuthenticationManager は、ユーザー名とパスワードが同じユーザーを認証します。すべてのユーザーに単一のロールを割り当てます。上記の出力は次のようになります。

Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: \
org.springframew[email protected] (英語)  441d0230: \
Principal: bob; Password: [PROTECTED]; \
Authenticated: true; Details: null; \
Granted Authorities: ROLE_USER

通常、このようなコードを記述する必要はありません。このプロセスは通常、たとえば Web 認証フィルターなどの内部で発生します。Spring Security で実際に認証を構成するものの質問には非常に簡単な答えがあることを示すために、ここにコードを含めました。SecurityContextHolder に完全に読み込まれた Authentication オブジェクトが含まれている場合、ユーザーは認証されます。

SecurityContextHolder のコンテンツを直接設定する

実際、Spring Security は、Authentication オブジェクトを SecurityContextHolder 内に配置する方法を気にしません。唯一の重要な要件は、SecurityContextHolder に Authentication が含まれていることです。Authentication は、AbstractSecurityInterceptor (後で詳しく説明します)がユーザー操作を認可する前にプリンシパルを表します。

独自のフィルターまたは MVC コントローラーを作成して、Spring Security に基づかない認証システムとの相互運用性を提供できます(そして多くのユーザーが行います)。例: ThreadLocal または JNDI の場所から現在のユーザーを利用可能にするコンテナー管理認証を使用している可能性があります。または、従来の独自の認証システムを使用している会社に作業している場合があります。これは、ほとんど管理できない企業の「標準」です。このような状況では、Spring Security を動作させることは非常に簡単ですが、それでも認証機能を提供します。必要なのは、場所からサードパーティのユーザー情報を読み取り、Spring セキュリティ固有の Authentication オブジェクトを作成し、それを SecurityContextHolder に入れるフィルター(または同等の)を書くことです。この場合、通常は組み込みの認証インフラストラクチャによって自動的に処理されるものについても考慮する必要があります。例: リクエストをクライアント [1] に書き込む前に、リクエスト間のコンテキストキャッシュするために先制的に HTTP セッションを作成する必要がある場合があります。

AuthenticationManager が実際の例でどのように実装されているのか疑問に思っている場合は、コアサービスの章で説明します。

9.1.4 Web アプリケーションでの認証

次に、Spring Security を Web アプリケーションで使用している状況を調べてみましょう(web.xml セキュリティが有効になっていない場合)。ユーザーはどのように認証され、セキュリティコンテキストが確立されますか?

典型的な Web アプリケーションの認証プロセスを検討してください。

  1. ホームページにアクセスして、リンクをクリックします。
  2. リクエストがサーバーに送信され、サーバーは保護されたリソースをリクエストしたと判断します。
  3. 現在認証されていないため、サーバーは認証が必要であることを示すレスポンスを返します。レスポンスは、HTTP レスポンスコード、または特定の Web ページへのリダイレクトのいずれかです。
  4. 認証メカニズムに応じて、ブラウザーはフォームに入力できるように特定の Web ページにリダイレクトするか、何らかの方法で(BASIC 認証ダイアログボックス、Cookie、X.509 証明書などを介して ID を取得します) )。
  5. ブラウザーはサーバーにレスポンスを返します。これは、記入したフォームの内容を含む HTTP POST、または認証の詳細を含む HTTP ヘッダーのいずれかです。
  6. 次に、サーバーは提示された資格情報が有効かどうかを判断します。有効であれば、次のステップが実行されます。それらが無効な場合、通常、ブラウザーは再試行するように求められます(したがって、上記の手順 2 に戻ります)。
  7. 認証プロセスを引き起こすために行った元のリクエストが再試行されます。保護されたリソースにアクセスするために十分な許可された権限で認証されていることを願っています。十分なアクセス権があれば、リクエストは成功します。それ以外の場合は、「禁止」を意味する HTTP エラーコード 403 が返されます。

Spring Security には、上記のほとんどのステップを担当する別個のクラスがあります。主な参加者は(使用される順序で) ExceptionTranslationFilterAuthenticationEntryPoint、「認証メカニズム」であり、前のセクションで見た AuthenticationManager を呼び出します。

ExceptionTranslationFilter

ExceptionTranslationFilter は、スローされる Spring Security 例外を検出する責任を持つ Spring Security フィルターです。通常、このような例外は、認可サービスのメインプロバイダーである AbstractSecurityInterceptor によってスローされます。AbstractSecurityInterceptor については次のセクションで説明しますが、今のところは、Java 例外を生成し、HTTP またはプリンシパルの認証メソッドについて何も知らないことを知る必要があります。代わりに、ExceptionTranslationFilter はこのサービスを提供し、エラーコード 403 を返す(プリンシパルが認証されたため、上記の手順 7 のように十分なアクセス権がない場合)か、AuthenticationEntryPoint を起動する(プリンシパルが認証されておらず、ステップ 3 を開始する必要があります。

AuthenticationEntryPoint

AuthenticationEntryPoint は、上記リストのステップ 3 を担当します。ご想像のとおり、各 Web アプリケーションにはデフォルトの認証戦略があります(まあ、これは Spring Security の他のほぼすべてのように構成できますが、ここでは簡単にしましょう)。各主要認証システムには独自の AuthenticationEntryPoint 実装があり、通常はステップ 3 で説明したアクションのいずれかを実行します。

認証メカニズム

ブラウザーが(HTTP フォームポストまたは HTTP ヘッダーとして)認証資格情報を送信すると、サーバー上にこれらの認証詳細を「収集」する何かが必要になります。これで、上記のリストのステップ 6 になりました。Spring Security には、ユーザーエージェント(通常は Web ブラウザー)から認証の詳細を収集する機能の特別な名前があり、これを「認証メカニズム」と呼びます。例は、フォームベースのログインおよび基本認証です。認証の詳細がユーザーエージェントから収集されると、Authentication 「リクエスト」オブジェクトが構築され、AuthenticationManager に提示されます。

認証メカニズムは、完全に読み込まれた Authentication オブジェクトを受信した後、リクエストを有効とみなし、Authentication を SecurityContextHolder に入れて、元のリクエストを再試行させます(上記のステップ 7)。一方、AuthenticationManager がリクエストを拒否した場合、認証メカニズムはユーザーエージェントに再試行をリクエストします(上記のステップ 2)。

リクエスト間の SecurityContext の保存

アプリケーションの種類によっては、ユーザー操作の間にセキュリティコンテキストを保存するための戦略が必要になる場合があります。典型的な Web アプリケーションでは、ユーザーは 1 回ログインすると、その後セッション ID によって識別されます。サーバーは、継続時間セッションのプリンシパル情報をキャッシュします。Spring Security では、リクエスト間で SecurityContext を格納する責任は SecurityContextPersistenceFilter にあり、デフォルトで HTTP リクエスト間の HttpSession 属性としてコンテキストを格納します。リクエストごとに SecurityContextHolder にコンテキストを復元し、決定的に、リクエストが完了すると SecurityContextHolder をクリアします。セキュリティ上の理由から、HttpSession と直接やり取りしないでください。そうする理由はまったくありません - 代わりに常に SecurityContextHolder を使用してください。

他の多くのタイプのアプリケーション(たとえば、ステートレス RESTful Web サービス)は HTTP セッションを使用せず、リクエストごとに再認証します。ただし、SecurityContextPersistenceFilter がチェーンに含まれていて、各リクエストの後に SecurityContextHolder が確実にクリアされるようにすることが重要です。

[Note] メモ

単一のセッションで同時リクエストを受信するアプリケーションでは、同じ SecurityContext インスタンスがスレッド間で共有されます。ThreadLocal が使用されていても、各スレッドの HttpSession から取得されるのは同じインスタンスです。これは、スレッドが実行されているコンテキストを一時的に変更する場合に意味があります。SecurityContextHolder.getContext() を使用して、返されたコンテキストオブジェクトで setAuthentication(anAuthentication) を呼び出す場合、Authentication オブジェクトは、同じ SecurityContext インスタンスを共有するすべての同時スレッドで変更されます。SecurityContextPersistenceFilter の動作をカスタマイズして、リクエストごとにまったく新しい SecurityContext を作成し、あるスレッドの変更が別のスレッドに影響を与えないようにすることができます。または、一時的にコンテキストを変更した時点で新しいインスタンスを作成できます。メソッド SecurityContextHolder.createEmptyContext() は常に新しいコンテキストインスタンスを返します。

9.1.5 Spring Security のアクセス制御(認可)

Spring Security でアクセス制御の決定を行う主なインターフェースは AccessDecisionManager です。アクセスをリクエストするプリンシパルを表す Authentication オブジェクト、「セキュアオブジェクト」(以下を参照)、およびオブジェクトに適用されるセキュリティメタデータ属性のリスト(アクセスに必要なロールのリストなど)を取得する decide メソッドがあります。付与されます)。

セキュリティと AOP のアドバイス

AOP に精通している場合は、前、後、スローなど、さまざまな種類のアドバイスがあることに気づくでしょう。アドバイザーはメソッド呼び出しを続行するかどうか、レスポンスを変更するかどうか、例外をスローするかどうかを選択できるため、around アドバイスは非常に便利です。Spring Security は、メソッドの呼び出しと Web リクエストに関するアラウンドアドバイスを提供します。Spring の標準 AOP サポートを使用してメソッド呼び出しの around アドバイスを達成し、標準フィルターを使用して Web リクエストの around アドバイスを達成します。

AOP に慣れていない人にとって、理解すべき重要なポイントは、Spring Security を使用すると、メソッド呼び出しと Web リクエストを保護できることです。ほとんどの人は、サービス層でメソッド呼び出しを保護することに関心があります。これは、サービスレイヤーが、ほとんどのビジネスロジックが現在の世代の Java EE アプリケーションに存在するためです。サービス層でメソッド呼び出しを保護する必要がある場合は、Spring の標準 AOP で十分です。ドメインオブジェクトを直接保護する必要がある場合は、AspectJ を検討する価値があると思われるでしょう。

AspectJ または Spring AOP を使用してメソッド認証を実行するか、フィルターを使用して Web リクエスト認証を実行するかを選択できます。これらのアプローチを 0、1、2、または 3 つ一緒に使用できます。主流の使用パターンは、サービスレイヤーで Spring AOP メソッド呼び出し認可と組み合わせて、Web リクエスト認可を実行することです。

セキュアオブジェクトと AbstractSecurityInterceptor

「安全なオブジェクトは、」何ですか? Spring Security は、この用語を使用して、セキュリティ(認可決定など)を適用できるオブジェクトを指します。最も一般的な例は、メソッド呼び出しと Web リクエストです。

サポートされる各セキュアオブジェクト型には、AbstractSecurityInterceptor のサブクラスである独自のインターセプタークラスがあります。重要なことに、AbstractSecurityInterceptor が呼び出されるまでに、プリンシパルが認証されている場合、SecurityContextHolder には有効な Authentication が含まれます。

AbstractSecurityInterceptor は、安全なオブジェクトリクエストを処理するための一貫したワークフローを提供します。通常は次のとおりです。

  1. 現在のリクエストに関連付けられている「構成属性」を検索する
  2. 認可決定のために、セキュアオブジェクト、現在の Authentication および構成属性を AccessDecisionManager に送信する
  3. オプションで、呼び出しが行われる Authentication を変更する
  4. セキュアオブジェクトの呼び出しを続行できるようにする (アクセスが許可されたと仮定する)
  5. 呼び出しが返されたら、構成されている場合は AfterInvocationManager を呼び出します。呼び出しで例外が発生した場合、AfterInvocationManager は呼び出されません。
構成属性とは何ですか?

「構成属性」は、AbstractSecurityInterceptor が使用するクラスにとって特別な意味を持つストリングと考えることができます。これらは、フレームワーク内のインターフェース ConfigAttribute で表されます。それらは、AccessDecisionManager 実装の高度さに応じて、単純なロール名またはより複雑な意味を持つ場合があります。AbstractSecurityInterceptor は、セキュアオブジェクトの属性を検索するために使用する SecurityMetadataSource で構成されます。通常、この構成はユーザーには表示されません。構成属性は、protected メソッドのアノテーションとして、または保護された URL のアクセス属性として入力されます。例: 名前空間の導入で <intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/> のようなものを見たとき、これは構成属性 ROLE_A および ROLE_B が特定のパターンに一致する Web リクエストに適用されることを意味しています。実際には、デフォルトの AccessDecisionManager 構成では、これは、これら 2 つの属性のいずれかに一致する GrantedAuthority を持つすべてのユーザーがアクセスを認可されることを意味します。厳密に言えば、これらは単なる属性であり、解釈は AccessDecisionManager の実装に依存しています。接頭辞 ROLE_ の使用は、これらの属性がロールであり、Spring Security の RoleVoter によって消費される必要があることを示すマーカーです。これは、投票者ベースの AccessDecisionManager が使用されている場合にのみ関係します。認可の章で AccessDecisionManager の実装方法を確認します。

RunAsManager

AccessDecisionManager がリクエストを許可することを決定すると、AbstractSecurityInterceptor は通常、リクエストを続行します。そうは言っても、まれに、ユーザーは SecurityContext 内の Authentication を別の Authentication に置き換えたい場合があります。これは、RunAsManager を呼び出す AccessDecisionManager によって処理されます。これは、サービスレイヤーメソッドがリモートシステムを呼び出して異なる ID を提示する必要がある場合など、かなり異常な状況で役立つ場合があります。Spring Security は 1 つのサーバーから別のサーバーにセキュリティ ID を自動的に伝達するため(適切に構成された RMI または HttpInvoker リモーティングプロトコルクライアントを使用している場合)、これは便利です。

AfterInvocationManager

セキュアなオブジェクトの呼び出しの進行に続いて戻る(メソッド呼び出しの補完またはフィルターチェーンの進行を意味する場合があります) AbstractSecurityInterceptor は、呼び出しを処理する最後の機会を取得します。この段階で、AbstractSecurityInterceptor は戻りオブジェクトを変更する可能性があります。安全なオブジェクト呼び出しの「途中」で認可の決定を下すことができなかったため、これを実現したい場合があります。プラグ可能なため、AbstractSecurityInterceptor は AfterInvocationManager に制御を渡し、必要に応じてオブジェクトを実際に変更します。このクラスは、オブジェクトを完全に置き換えたり、例外をスローしたり、選択した方法で変更したりすることさえできません。呼び出し後のチェックは、呼び出しが成功した場合にのみ実行されます。例外が発生した場合、追加のチェックはスキップされます。

AbstractSecurityInterceptor およびその関連オブジェクトは図 9.1: “ セキュリティインターセプターと「セキュアオブジェクト」モデル ” に表示されます

図 9.1: セキュリティインターセプターと「セキュアオブジェクト」モデル

Abstract Security Interceptor

セキュアオブジェクトモデルの拡張

安全なオブジェクトを直接使用する必要があるのは、リクエストをインターセプトおよび承認するまったく新しい方法を検討している開発者のみです。例: メッセージングシステムへの呼び出しをセキュリティで保護するために、新しいセキュリティで保護されたオブジェクトを構築することが可能です。セキュリティを必要とし、呼び出しをインターセプトする方法を提供するもの(アドバイスセマンティクスに関連する AOP など)は、安全なオブジェクトにすることができます。とはいえ、ほとんどの Spring アプリケーションは、現在サポートされている 3 つのセキュアオブジェクト型(AOP Alliance MethodInvocation、AspectJ JoinPoint および Web リクエスト FilterInvocation)を完全に透過的に使用するだけです。

9.2 コアサービス

Spring Security アーキテクチャとそのコアクラスの概要がわかったため、コアインターフェースとその実装の 1 つまたは 2 つ、特に AuthenticationManagerUserDetailsServiceAccessDecisionManager を詳しく見ていきましょう。これらは、このドキュメントの残りの部分全体で定期的に発生するため、構成方法と動作方法を理解しておくことが重要です。

9.2.1 AuthenticationManager、ProviderManager、および AuthenticationProvider

AuthenticationManager は単なるインターフェースであるため、実装は任意に選択できますが、実際にはどのように機能しますか? 複数の認証データベース、またはデータベースや LDAP サーバーなどの異なる認証サービスの組み合わせを確認する必要がある場合はどうなるでしょうか?

Spring Security のデフォルトの実装は ProviderManager と呼ばれ、認証リクエスト自体を処理するのではなく、構成済みの AuthenticationProvider のリストに委譲します。各リストは、認証を実行できるかどうかを順番に照会します。各プロバイダーは、例外をスローするか、完全に読み込まれた Authentication オブジェクトを返します。私たちの親友である UserDetails と UserDetailsService を覚えていますか? そうでない場合は、前の章に戻ってメモリをリフレッシュします。認証リクエストを検証するための最も一般的なアプローチは、対応する UserDetails をロードし、ロードされたパスワードをユーザーが入力したパスワードと照合することです。これは、DaoAuthenticationProvider で使用されるアプローチです(以下を参照)。ロードされた UserDetails オブジェクト(特にそれに含まれる GrantedAuthority)は、認証が成功して返されて SecurityContext に格納された完全に読み込まれた Authentication オブジェクトを構築するときに使用されます。

名前空間を使用している場合、ProviderManager のインスタンスが内部で作成および保守され、名前空間認証プロバイダー要素を使用してプロバイダーを追加します(名前空間の章を参照)。この場合、アプリケーションコンテキストで ProviderManager Bean を宣言しないでください。ただし、名前空間を使用していない場合は、次のように宣言します。

<bean id="authenticationManager"
        class="org.springframework.security.authentication.ProviderManager">
    <constructor-arg>
        <list>
            <ref local="daoAuthenticationProvider"/>
            <ref local="anonymousAuthenticationProvider"/>
            <ref local="ldapAuthenticationProvider"/>
        </list>
    </constructor-arg>
</bean>

上記の例では、3 つのプロバイダーがあります。それらは示された順序(List の使用によって暗示される)で試行され、各プロバイダーは認証を試みるか、単に null を返すことで認証をスキップできます。すべての実装が null を返す場合、ProviderManager は ProviderNotFoundException をスローします。プロバイダーのチェーンについて詳しく知りたい場合は、ProviderManager Javadoc を参照してください。

Web フォームログイン処理フィルターなどの認証メカニズムには、ProviderManager への参照が挿入され、それを呼び出して認証リクエストを処理します。必要なプロバイダーは、認証メカニズムと互換性がある場合がありますが、特定の認証メカニズムに依存する場合もあります。例: DaoAuthenticationProvider および LdapAuthenticationProvider は、単純なユーザー名 / パスワード認証リクエストを送信するメカニズムと互換性があるため、フォームベースのログインまたは HTTP 基本認証で動作します。一方、一部の認証メカニズムは、単一型の AuthenticationProvider によってのみ解釈できる認証リクエストオブジェクトを作成します。これの例は JA-SIG CAS で、これはサービスチケットの概念を使用するため、CasAuthenticationProvider によってのみ認証されます。適切なプロバイダーの登録を忘れた場合、認証の試行が行われたときに ProviderNotFoundException を受け取るだけなので、これについてあまり心配する必要はありません。

認証成功時の資格情報の消去

デフォルトでは(Spring Security 3.1 以降)、ProviderManager は、成功した認証リクエストによって返される Authentication オブジェクトから機密情報をクリアしようとします。これにより、パスワードなどの情報が必要以上に長く保持されるのを防ぎます。

これにより、たとえば、ステートレスアプリケーションのパフォーマンスを向上させるために、ユーザーオブジェクトのキャッシュを使用している場合に問題が発生する可能性があります。Authentication がキャッシュ内のオブジェクト(UserDetails インスタンスなど)への参照を含み、これの資格情報が削除されている場合、キャッシュされた値に対して認証することはできなくなります。キャッシュを使用している場合は、これを考慮する必要があります。明らかな解決策は、キャッシュの実装、または返された Authentication オブジェクトを作成する AuthenticationProvider のいずれかで、最初にオブジェクトのコピーを作成することです。または、ProviderManager の eraseCredentialsAfterAuthentication プロパティを無効にすることができます。詳細については、Javadoc を参照してください。

DaoAuthenticationProvider

Spring Security によって実装される最も単純な AuthenticationProvider は DaoAuthenticationProvider であり、これもフレームワークで最も早くサポートされているものの 1 つです。ユーザー名、パスワード、GrantedAuthority を検索するために、UserDetailsService を(DAO として)利用します。UsernamePasswordAuthenticationToken で送信されたパスワードを UserDetailsService によってロードされたパスワードと比較するだけで、ユーザーを認証します。プロバイダーの構成は非常に簡単です。

<bean id="daoAuthenticationProvider"
    class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

PasswordEncoder はオプションです。PasswordEncoder は、構成された UserDetailsService から返される UserDetails オブジェクトで提示されるパスワードのエンコードとデコードを提供します。これについては、以下でさらに詳しく説明します。

9.2.2 UserDetailsService の実装

このリファレンスガイドで前述したように、ほとんどの認証プロバイダーは UserDetails および UserDetailsService インターフェースを利用しています。UserDetailsService の契約は単一のメソッドであることを思い出してください。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返される UserDetails は、getter を提供するインターフェースです。getter は、ユーザー名、パスワード、付与された権限、ユーザーアカウントが有効か無効かなどの認証情報の非 NULL プロビジョニングを保証します。ユーザー名とパスワードが実際に認証決定の一部として使用されない場合でも、ほとんどの認証プロバイダーは UserDetailsService を使用します。他のシステム(LDAP、X.509、CAS など)が実際に資格情報を検証する責任を引き受けたため、返された UserDetails オブジェクトをその GrantedAuthority 情報だけに使用できます。

UserDetailsService の実装は非常に簡単であるため、ユーザーが選択した永続性戦略を使用して認証情報を簡単に取得できるはずです。そうは言っても、Spring Security にはいくつかの便利な基本実装が含まれています。これについては以下で説明します。

インメモリ認証

使いやすく、選択した永続エンジンから情報を抽出するカスタム UserDetailsService 実装を作成しますが、多くのアプリケーションではそのような複雑さは必要ありません。これは、データベースの構成や UserDetailsService 実装の作成に時間をかけたくないときに、プロトタイプアプリケーションを構築する場合、または Spring Security の統合を開始する場合に特に当てはまります。この種の状況では、簡単なオプションはセキュリティ名前空間の user-service 要素を使用することです:

<user-service id="userDetailsService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
in samples easier. Normally passwords should be hashed using BCrypt -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>

これは、外部プロパティファイルの使用もサポートします。

<user-service id="userDetailsService" properties="users.properties"/>

プロパティファイルには、フォームのエントリが含まれている必要があります

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

次に例を示します

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

JdbcDaoImpl

Spring Security には、JDBC データソースから認証情報を取得できる UserDetailsService も含まれています。内部的には Spring JDBC が使用されているため、ユーザーの詳細を保存するだけで、フル機能のオブジェクトリレーショナルマッパー(ORM)の複雑さを回避できます。アプリケーションで ORM ツールを使用している場合は、カスタム UserDetailsService を作成して、おそらくすでに作成したマッピングファイルを再利用することをお勧めします。JdbcDaoImpl に戻ると、構成の例を以下に示します。

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

上記の DriverManagerDataSource を変更することにより、さまざまなリレーショナルデータベース管理システムを使用できます。他の Spring 構成と同様に、JNDI から取得したグローバルデータソースを使用することもできます。

権限グループ

デフォルトでは、JdbcDaoImpl は、権限がユーザーに直接マップされるという前提で、単一ユーザーの権限をロードします(データベーススキーマの付録を参照)。別のアプローチは、権限をグループに分割し、グループをユーザーに割り当てることです。ユーザーの権利を管理する手段としてこのアプローチを好む人もいます。グループ権限の使用を有効にする方法の詳細については、JdbcDaoImpl Javadoc を参照してください。グループスキーマも付録に含まれています。



[1] レスポンスがコミットされると、セッションを作成することはできません。

現行バージョンへ切り替える