Spring Security は、一般的な攻撃に対する認証、認可、および保護を提供するフレームワークです。命令型アプリケーションとリアクティブ型アプリケーションの両方に対するファーストクラスのサポートにより、Spring ベースのアプリケーションを保護するための事実上の標準になっています。

序文

このセクションでは、Spring Security のロジスティクスについて説明します。

1. 前提条件

Spring Security には、Java 8 以上のランタイム環境が必要です。

Spring Security は自己完結型の動作を目的としているため、Java ランタイム環境に特別な構成ファイルを配置する必要はありません。特に、特別な Java Authentication and Authorization Service(JAAS)ポリシーファイルを構成したり、Spring Security を一般的なクラスパスの場所に配置したりする必要はありません。

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

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

2. Spring Security コミュニティ

Spring Security コミュニティへようこそ!このセクションでは、広大なコミュニティを最大限に活用する方法について説明します。

2.1. ヘルプの利用

Spring Security のサポートが必要な場合は、こちらからサポートいたします。以下は、ヘルプを得るための最良の方法の一部です。

2.2. 参加する

Spring Security プロジェクトへの参加を歓迎します。StackOverflow に関する質問への回答、新しいコードの作成、既存のコードの改善、ドキュメントの支援、サンプルやチュートリアルの開発、バグの報告、または単に提案を行うなど、貢献する方法は多数あります。詳細については、貢献する (GitHub) ドキュメントを参照してください。

2.3. ソースコード

Spring Security のソースコードは、https://github.com/spring-projects/spring-security/ (英語) の GitHub にあります。

2.4. Apache 2 ライセンス

Spring Security は、Apache 2.0 ライセンス (英語) でリリースされたオープンソースソフトウェアです。

2.5. ソーシャルメディア

Twitter で @SpringSecurity (英語) Spring Security チーム (英語) をフォローして、最新のニュースを入手できます。@SpringCentral (英語) をフォローして、Spring ポートフォリオ全体を最新の状態に保つこともできます。

3. Spring Security 5.3 の新機能

Spring Security 5.3 は多くの新機能を提供します。以下は、リリースのハイライトです。

3.1. ドキュメントの更新

ドキュメントを書き直す努力を続けます。

このリリースで表示される内容は次のとおりです。

3.4. RSocket

3.5. 追加の更新

3.6. 変更をビルド

4. Spring Security の入手

このセクションでは、Spring Security バイナリの取得について知っておく必要のあるすべてについて説明します。ソースコードの入手方法については、ソースコードを参照してください。

4.1. リリース番号

Spring Security バージョンは、次のような MAJOR.MINOR.PATCH としてフォーマットされます。

  • メジャーバージョンには、重大な変更が含まれる場合があります。通常、これらは最新のセキュリティ慣行に合わせてセキュリティを改善するために行われます。

  • マイナーバージョンには拡張機能が含まれていますが、パッシブアップデートと見なされます

  • PATCH レベルは、バグを修正する変更の例外を除いて、完全に互換性があり、前方および後方にある必要があります。

4.2. Maven での使用

ほとんどのオープンソースプロジェクトとして、Spring Security は依存関係を Maven アーティファクトとしてデプロイします。このセクションのトピックでは、Maven を使用するときに Spring Security を使用する方法について詳しく説明します。

4.2.1. Spring Boot と Maven

Spring Boot は、Spring セキュリティ関連の依存関係を一緒に集約する spring-boot-starter-security スターターを提供します。スターターを使用する最も簡単で推奨する方法は、IDE 統合(Eclipse (英語) IntelliJNetBeans (GitHub) )または https://start.spring.io (英語) を使用して Spring Initializr (英語) を使用することです。

または、次の例に示すように、スターターを手動で追加できます。

例 1: pom.xml
<dependencies>
    <!-- ... other dependency elements ... -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

Spring Boot は依存バージョンを管理するために Maven BOM を提供するため、バージョンを指定する必要はありません。Spring Security バージョンをオーバーライドする場合は、次の例に示すように、Maven プロパティを指定することでオーバーライドできます。

例 2: pom.xml
<properties>
    <!-- ... -->
    <spring-security.version>5.3.5.RELEASE</spring-security.version>
</dependencies>

Spring Security はメジャーリリースでのみ重大な変更を行うため、Spring Boot で Spring Security の新しいバージョンを使用しても安全です。ただし、場合によっては、Spring Framework のバージョンも更新する必要があります。これを行うには、次の例に示すように、Maven プロパティを追加します。

例 3: pom.xml
<properties>
    <!-- ... -->
    <spring.version>5.2.9.RELEASE</spring.version>
</dependencies>

追加の機能(LDAP、OpenID など)を使用する場合は、適切なプロジェクトモジュールも含める必要があります。

4.2.2. Spring Boot なしの Maven

Spring Boot なしで Spring Security を使用する場合、Spring Security の BOM を使用して、プロジェクト全体で Spring Security の一貫したバージョンが使用されるようにすることをお勧めします。次の例は、その方法を示しています。

例 4: pom.xml
<dependencyManagement>
    <dependencies>
        <!-- ... other dependency elements ... -->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-bom</artifactId>
            <version>{spring-security-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

依存関係の最小限の Spring Security Maven セットは通常、次のようになります。

例 5: pom.xml
<dependencies>
    <!-- ... other dependency elements ... -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
    </dependency>
</dependencies>

追加の機能(LDAP、OpenID など)を使用する場合は、適切なプロジェクトモジュールも含める必要があります。

Spring Security は Spring Framework 5.2.9.RELEASE に対してビルドされますが、一般的には新しいバージョンの Spring Framework 5.x. で動作するはずです。多くのユーザーは、Spring Security の推移的な依存関係が Spring Framework 5.2.9.RELEASE, を解決し、奇妙なクラスパスの問題を引き起こす可能性があるという事実に反する可能性があります。これを解決する最も簡単な方法は、次の例に示すように、pom.xml の <dependencyManagement> セクション内で spring-framework-bom を使用することです。

例 6: pom.xml
<dependencyManagement>
    <dependencies>
        <!-- ... other dependency elements ... -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>5.2.9.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

上記の例は、Spring Security のすべての推移的な依存関係が Spring 5.2.9.RELEASE モジュールを使用することを保証します。

このアプローチは、Maven の「部品表」(BOM)コンセプトを使用しており、Maven 2.0.9+. でのみ使用できます。依存関係の解決方法の詳細については、Maven の依存性メカニズムドキュメントの導入 (Apache) を参照してください。

4.2.3. Maven リポジトリ

すべての GA リリース(つまり、.RELEASE で終わるバージョン)は Maven セントラルにデプロイされるため、pom で追加の Maven リポジトリを宣言する必要はありません。

SNAPSHOT バージョンを使用する場合、次の例に示すように、Spring スナップショットリポジトリが定義されていることを確認する必要があります。

例 7: pom.xml
<repositories>
    <!-- ... possibly other repository elements ... -->
    <repository>
        <id>spring-snapshot</id>
        <name>Spring Snapshot Repository</name>
        <url>https://repo.spring.io/snapshot</url>
    </repository>
</repositories>

マイルストーンまたはリリース候補バージョンを使用する場合、次の例に示すように、Spring マイルストーンリポジトリが定義されていることを確認する必要があります。

例 8: pom.xml
<repositories>
    <!-- ... possibly other repository elements ... -->
    <repository>
        <id>spring-milestone</id>
        <name>Spring Milestone Repository</name>
        <url>https://repo.spring.io/milestone</url>
    </repository>
</repositories>

4.3. Gradle

ほとんどのオープンソースプロジェクトとして、Spring Security はその依存関係を Maven アーティファクトとしてデプロイします。これにより、一流の Gradle サポートが可能になります。以下のトピックでは、Gradle を使用するときに Spring Security を使用する方法について詳しく説明します。

4.3.1. Spring Boot と Gradle

Spring Boot は、Spring Security 関連の依存関係を一緒に集約する spring-boot-starter-security スターターを提供します。スターターを使用する最も簡単で推奨する方法は、IDE 統合(Eclipse (英語) IntelliJNetBeans (GitHub) )を使用するか、https://start.spring.io (英語) を使用して Spring Initializr (英語) を使用することです。

または、次の例に示すように、スターターを手動で追加できます。

例 9: build.gradle
dependencies {
    compile "org.springframework.boot:spring-boot-starter-security"
}

Spring Boot は依存バージョンを管理するために Maven BOM を提供するため、バージョンを指定する必要はありません。Spring Security バージョンをオーバーライドする場合は、次の例に示すように、Gradle プロパティを提供することでオーバーライドできます。

例 10: build.gradle
ext['spring-security.version']='5.3.5.RELEASE'

Spring Security はメジャーリリースでのみ重大な変更を行うため、Spring Boot で Spring Security の新しいバージョンを使用しても安全です。ただし、場合によっては、Spring Framework のバージョンも更新する必要があります。これを行うには、次の例に示すように、Gradle プロパティを追加します。

例 11: build.gradle
ext['spring.version']='5.2.9.RELEASE'

追加の機能(LDAP、OpenID など)を使用する場合は、適切なプロジェクトモジュールも含める必要があります。

4.3.2. Spring Boot なしの Gradle

Spring Boot なしで Spring Security を使用する場合、Spring Security の BOM を使用して、プロジェクト全体で Spring Security の一貫したバージョンが使用されるようにすることをお勧めします。次の例に示すように、依存関係管理プラグイン (GitHub) を使用してこれを行うことができます。

例 12: build.gradle
plugins {
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework.security:spring-security-bom:5.3.5.RELEASE'
    }
}

依存関係の最小限の Spring Security Maven セットは通常、次のようになります。

例 13: build.gradle
dependencies {
    compile "org.springframework.security:spring-security-web"
    compile "org.springframework.security:spring-security-config"
}

追加の機能(LDAP、OpenID など)を使用する場合は、適切なプロジェクトモジュールも含める必要があります。

Spring Security は Spring Framework 5.2.9.RELEASE に対してビルドされますが、一般的には新しいバージョンの Spring Framework 5.x. で動作するはずです。多くのユーザーは、Spring Security の推移的な依存関係が Spring Framework 5.2.9.RELEASE, を解決し、奇妙なクラスパスの問題を引き起こす可能性があるという事実に反する可能性があります。これを解決する最も簡単な方法は、pom.xml の <dependencyManagement> セクション内で spring-framework-bom を使用することです。これを行うには、次の例に示すように、依存関係管理プラグイン (GitHub) を使用します。

例 14: build.gradle
plugins {
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
}

dependencyManagement {
    imports {
        mavenBom 'org.springframework:spring-framework-bom:5.2.9.RELEASE'
    }
}

上記の例は、Spring Security のすべての推移的な依存関係が Spring 5.2.9.RELEASE モジュールを使用することを保証します。

4.3.3. Gradle リポジトリ

GA リリース(つまり、.RELEASE で終わるバージョン)はすべて Maven セントラルにデプロイされるため、GA リリースには mavenCentral() リポジトリを使用するだけで十分です。次の例は、その方法を示しています。

例 15: build.gradle
repositories {
    mavenCentral()
}

SNAPSHOT バージョンを使用する場合、次の例に示すように、Spring スナップショットリポジトリが定義されていることを確認する必要があります。

例 16: build.gradle
repositories {
    maven { url 'https://repo.spring.io/snapshot' }
}

マイルストーンまたはリリース候補バージョンを使用する場合、次の例に示すように、Spring マイルストーンリポジトリが定義されていることを確認する必要があります。

例 17: build.gradle
repositories {
    maven { url 'https://repo.spring.io/milestone' }
}

5. 機能

Spring Security は、認証、認可、および一般的な悪用に対する保護の包括的なサポートを提供します。また、他のライブラリとの統合を提供して、その使用箇所を簡素化します。

5.1. 認証

Spring Security は、認証 (英語) の包括的なサポートを提供します。認証とは、特定のリソースにアクセスしようとしているユーザーの身元を確認する方法です。ユーザーを認証する一般的な方法は、ユーザーにユーザー名とパスワードの入力を要求することです。認証が実行されると、ID が認識され、認可を実行できます。

5.1.1. 認証サポート

Spring Security は、ユーザー認証の組み込みサポートを提供します。各スタックでサポートされるものの詳細については、サーブレットおよび WebFlux の認証に関するセクションを参照してください。

5.1.2. パスワード保存

Spring Security の PasswordEncoder インターフェースは、パスワードの安全な保存を可能にするために、パスワードの一方向変換を実行するために使用されます。PasswordEncoder は一方向の変換であるため、パスワード変換を双方向にする必要がある場合(つまり、データベースへの認証に使用される資格情報を保存する場合)は意図されていません。通常、PasswordEncoder は、認証時にユーザーが指定したパスワードと比較する必要があるパスワードを保存するために使用されます。

パスワード保存履歴

長年にわたって、パスワードを保存するための標準的なメカニズムが進化してきました。最初は、パスワードはプレーンテキストで保存されていました。パスワードは、パスワードにアクセスするために必要な資格情報で保存されているため、パスワードは安全であると想定されていました。しかし、悪意のあるユーザーは、SQL インジェクションなどの攻撃を使用して、ユーザー名とパスワードの大きな「データダンプ」を取得する方法を見つけることができました。ますます。多くのユーザー資格情報がパブリックセキュリティの専門家になるにつれて、ユーザーのパスワードを保護するためにより多くのことを行う必要があることに気付きました。

開発者は、SHA-256 などの一方向ハッシュを介してパスワードを実行した後、パスワードを保存することが推奨されました。ユーザーが認証を試行すると、ハッシュされたパスワードは、入力したパスワードのハッシュと比較されます。つまり、システムはパスワードの一方向ハッシュを保存するだけで済みました。違反が発生した場合、パスワードの一方向ハッシュのみが公開されました。ハッシュは一方向であり、ハッシュが与えられたパスワードを推測することは計算上困難であったため、システム内の各パスワードを把握する努力は価値がありません。この新しいシステムを無効にするために、悪意のあるユーザーはレインボーテーブル (英語) として知られるルックアップテーブルを作成することにしました。各パスワードを毎回推測する作業を行うのではなく、パスワードを一度計算してルックアップテーブルに保存しました。

レインボーテーブルの有効性を緩和するために、開発者はソルトパスワードを使用することが推奨されます。ハッシュ関数への入力としてパスワードだけを使用する代わりに、ランダムなバイト(ソルトと呼ばれる)がすべてのユーザーのパスワードに対して生成されます。ソルトとユーザーのパスワードは、一意のハッシュを生成するハッシュ関数を介して実行されます。ソルトは、ユーザーのパスワードとともにクリアテキストで保存されます。次に、ユーザーが認証を試みると、ハッシュされたパスワードは、保存されたソルトのハッシュと入力したパスワードと比較されます。独自のソルトは、ソルトとパスワードの組み合わせごとにハッシュが異なるため、レインボーテーブルが効果的ではなくなったことを意味します。

現代では、暗号化ハッシュ(SHA-256 など)はもはや安全ではないことがわかります。その理由は、最新のハードウェアを使用すると、1 秒間に何十億ものハッシュ計算を実行できるからです。これは、各パスワードを簡単に個別に解読できることを意味します。

開発者は、適応型一方向機能を活用してパスワードを保存することが推奨されています。適応型一方向機能によるパスワードの検証は、意図的にリソース(CPU、メモリなど)を集中的に使用します。適応型一方向機能を使用すると、ハードウェアが改善されるにつれて大きくなる「作業要素」を構成できます。システムのパスワードを確認するのに約 1 秒かかるように「作業要素」を調整することをお勧めします。このトレードオフは、攻撃者がパスワードを解読することを困難にすることですが、それほど高価ではなく、あなた自身のシステムに過度の負担をかけます。Spring Security は「作業要素」の適切な出発点を提供しようとしましたが、システムごとにパフォーマンスが大きく異なるため、ユーザーは自分のシステムの「作業要素」をカスタマイズすることをお勧めします。使用する必要がある適応型一方向関数の例には、bcryptPBKDF2scrypt、および argon2 が含まれます。

適応型一方向機能は意図的にリソースを集中的に使用するため、すべてのリクエストに対してユーザー名とパスワードを検証すると、アプリケーションのパフォーマンスが大幅に低下します。検証リソースを集中的に使用することでセキュリティが得られるため、Spring Security(または他のライブラリ)がパスワードの検証を高速化するためにできることはありません。ユーザーは、長期資格情報(つまり、ユーザー名とパスワード)を短期資格情報(つまり、セッション、OAuth トークンなど)と交換することをお勧めします。セキュリティを損なうことなく、短期間の資格情報を迅速に検証できます。

DelegatingPasswordEncoder

Spring Security 5.0 以前は、デフォルトの PasswordEncoder は NoOpPasswordEncoder で、プレーンテキストのパスワードが必要でした。パスワード履歴セクションに基づいて、デフォルトの PasswordEncoder が BCryptPasswordEncoder のようになっていることを期待するかもしれません。ただし、これは 3 つの実際の問題を無視します。

  • 簡単に移行できない古いパスワードエンコーディングを使用する多くのアプリケーションがあります

  • パスワード保存のベストプラクティスは再び変更されます。

  • フレームワーク Spring Security が頻繁に重大な変更を加えることができないため

代わりに、Spring Security は DelegatingPasswordEncoder を導入します。

  • 現在のパスワードストレージの推奨事項を使用してパスワードが確実にエンコードされるようにする

  • 最新およびレガシー形式のパスワードの検証を許可する

  • 将来的にエンコーディングをアップグレードできるようにする

PasswordEncoderFactories を使用して DelegatingPasswordEncoder のインスタンスを簡単に構築できます。

例 18: デフォルトの DelegatingPasswordEncoder を作成
PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

または、独自のカスタムインスタンスを作成することもできます。例:

例 19: カスタム DelegatingPasswordEncoder を作成
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
パスワード保存形式

パスワードの一般的な形式は次のとおりです。

例 20: DelegatingPasswordEncoder ストレージ形式
{id}encodedPassword

id は、どの PasswordEncoder を使用すべきかを調べるために使用される識別子であり、encodedPassword は選択された PasswordEncoder の元のエンコードされたパスワードです。id はパスワードの先頭にあり、{ で始まり } で終わる必要があります。id が見つからない場合、id は null になります。例:以下は、異なる id を使用してエンコードされたパスワードのリストです。元のパスワードはすべて「パスワード」です。

例 21: DelegatingPasswordEncoder エンコードされたパスワードの例
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
1 最初のパスワードは、PasswordEncoder id が bcrypt で、encodedPassword が $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG になります。一致すると、BCryptPasswordEncoder に委譲されます。
22 番目のパスワードには、noop の PasswordEncoder id と password の encodedPassword があります。一致すると、NoOpPasswordEncoder に委譲されます。
33 番目のパスワードの PasswordEncoder id は pbkdf2 で、encodedPassword は 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc です。一致すると、Pbkdf2PasswordEncoder に委譲されます。
44 番目のパスワードは、PasswordEncoder id が scrypt で、encodedPassword が $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= になります。一致すると、SCryptPasswordEncoder に委譲されます。
5 最終パスワードには、sha256 の PasswordEncoder id と 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 の encodedPassword があります。一致すると、StandardPasswordEncoder に委譲されます。

一部のユーザーは、ストレージ形式が潜在的なハッカーに提供されていることを懸念する場合があります。パスワードの保存はアルゴリズムが秘密であることに依存していないため、これは懸念事項ではありません。さらに、攻撃者がプレフィックスなしで簡単に把握できる形式はほとんどあります。例:BCrypt パスワードは、多くの場合 $2a$ で始まります。

パスワードエンコーディング

コンストラクターに渡される idForEncode は、パスワードのエンコードに使用される PasswordEncoder を決定します。上記で作成した DelegatingPasswordEncoder では、password をエンコードした結果が BCryptPasswordEncoder に委譲され、接頭辞として {bcrypt} が付けられます。最終結果は次のようになります。

例 22: DelegatingPasswordEncoder エンコードの例
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
パスワード照合

マッチングは、コンストラクターで提供される {id} および id から PasswordEncoder へのマッピングに基づいて行われます。パスワード保存形式の例は、これがどのように行われるかの実例を提供します。デフォルトでは、パスワードとマッピングされていない id (nullID を含む)で matches(CharSequence, String) を呼び出した結果は IllegalArgumentException になります。この動作は、DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) を使用してカスタマイズできます。

id を使用することにより、任意のパスワードエンコーディングを照合できますが、最新のパスワードエンコーディングを使用してパスワードをエンコードします。暗号化とは異なり、パスワードハッシュはプレーンテキストを回復する簡単な方法がないように設計されているため、これは重要です。平文を復元する方法がないため、パスワードの移行が難しくなります。ユーザーが NoOpPasswordEncoder を移行するのは簡単ですが、開始時の操作を簡単にするためにデフォルトで含めることを選択しました。

はじめに

デモやサンプルをまとめる場合、ユーザーのパスワードをハッシュするのに時間がかかるのは少し面倒です。これを簡単にする便利なメカニズムがありますが、これはまだ本番用ではありません。

例 23: withDefaultPasswordEncoder の例
User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

複数のユーザーを作成している場合は、ビルダーを再利用することもできます。

例 24: withDefaultPasswordEncoder ビルダーの再利用
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();

これにより、保存されているパスワードがハッシュされますが、パスワードはメモリおよびコンパイルされたソースコードで公開されます。本番環境ではまだ安全とは見なされません。本番環境では、パスワードを外部でハッシュする必要があります

Spring Boot CLI でエンコードする

パスワードを適切にエンコードする最も簡単な方法は、Spring Boot CLI を使用することです。

例:以下は、DelegatingPasswordEncoder で使用するために password のパスワードをエンコードします。

例 25: Spring Boot CLI encodepassword の例
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
トラブルシューティング

パスワード保存形式に従って、保存されているパスワードの 1 つに id がない場合、次のエラーが発生します。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

エラーを解決する最も簡単な方法は、パスワードがエンコードされている PasswordEncoder を明示的に提供するように切り替えることです。これを解決する最も簡単な方法は、パスワードが現在どのように保存されているかを把握し、正しい PasswordEncoder を明示的に提供することです。

Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。

または、すべてのパスワードの前に正しい ID を付けて、引き続き DelegatingPasswordEncoder を使用できます。例:BCrypt を使用している場合、次のようなものからパスワードを移行します。

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

マッピングの完全なリストについては、PasswordEncoderFactories(Javadoc) の Javadoc を参照してください。

BCryptPasswordEncoder

BCryptPasswordEncoder 実装は、広くサポートされている bcrypt (英語) アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングに対する耐性を高めるために、bcrypt は意図的に低速になっています。他のアダプティブ一方向関数と同様に、システムのパスワードを検証するのに約 1 秒かかるように調整する必要があります。BCryptPasswordEncoder のデフォルト実装では、BCryptPasswordEncoder(Javadoc) の Javadoc で述べられているように、強度 10 を使用します。パスワードの確認に約 1 秒かかるように、ご使用のシステムで強度パラメーターを調整およびテストすることをお勧めします。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Argon2PasswordEncoder

Argon2PasswordEncoder 実装は、Argon2 (英語) アルゴリズムを使用してパスワードをハッシュします。Argon2 はパスワードハッシュコンペティション (英語) の勝者です。カスタムハードウェアでのパスワードクラッキングを無効にするために、Argon2 は大量のメモリを必要とする意図的に遅いアルゴリズムです。他の適応型一方向機能と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。Argon2PasswordEncoder が BouncyCastle を必要とする場合の現在の実装。

// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 実装は、PBKDF2 (英語) アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングを無効にするため、PBKDF2 は意図的に遅いアルゴリズムです。他の適応型一方向機能と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。このアルゴリズムは、FIPS 認定が必要な場合に適しています。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
SCryptPasswordEncoder

SCryptPasswordEncoder 実装は、暗号化 (英語) アルゴリズムを使用してパスワードをハッシュします。カスタムハードウェアでのパスワードクラッキングを無効にするために、scrypt は大量のメモリを必要とする意図的に遅いアルゴリズムです。他の適応型一方向機能と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
その他 PasswordEncoders

下位互換性のために完全に存在する他の PasswordEncoder 実装が多数あります。これらはすべて、もはや安全であると見なされないことを示すために非推奨です。ただし、既存のレガシーシステムを移行することは難しいため、削除する計画はありません。

パスワード保存設定

Spring Security はデフォルトで DelegatingPasswordEncoder を使用します。ただし、PasswordEncoder を Spring Bean として公開することにより、これをカスタマイズできます。

Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。

NoOpPasswordEncoder に戻すことは安全とは見なされません。代わりに、DelegatingPasswordEncoder の使用に移行して、安全なパスワードエンコーディングをサポートする必要があります。

例 26: NoOpPasswordEncoder
Java
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
XML
<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
Kotlin
@Bean
fun passwordEncoder(): PasswordEncoder {
    return NoOpPasswordEncoder.getInstance();
}

XML 構成では、NoOpPasswordEncoder Bean 名が passwordEncoder であることが必要です。

5.2. エクスプロイトに対する保護

Spring Security は、一般的な悪用に対する保護を提供します。可能な限り、保護はデフォルトで有効になっています。以下に、Spring Security が保護するさまざまなエクスプロイトの概要を示します。

5.2.1. クロスサイトリクエストフォージェリ (CSRF)

Spring は、クロスサイトリクエストフォージェリ (CSRF) (英語) 攻撃から保護するための包括的なサポートを提供します。以下のセクションでは、以下を検討します。

ドキュメントのこのパートでは、CSRF 保護の一般的なトピックについて説明します。サーブレットおよび WebFlux ベースのアプリケーションの CSRF 保護に関する特定の情報については、関連するセクションを参照してください。

CSRF 攻撃とは何ですか?

CSRF 攻撃を理解する最良の方法は、具体例を見ることです。

銀行の Web サイトが、現在ログインしているユーザーから別の銀行口座に送金できるフォームを提供していると仮定します。例:転送フォームは次のようになります。

例 27: 転送フォーム
<form method="post"
    action="/transfer">
<input type="text"
    name="amount"/>
<input type="text"
    name="routingNumber"/>
<input type="text"
    name="account"/>
<input type="submit"
    value="Transfer"/>
</form>

対応する HTTP リクエストは次のようになります。

例 28: HTTP リクエストを転送する
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

次に、銀行の Web サイトに認証したふりをして、ログアウトせずに悪の Web サイトにアクセスします。邪悪な Web サイトには、次の形式の HTML ページが含まれています。

例 29: 悪意のある転送フォーム
<form method="post"
    action="https://bank.example.com/transfer">
<input type="hidden"
    name="amount"
    value="100.00"/>
<input type="hidden"
    name="routingNumber"
    value="evilsRoutingNumber"/>
<input type="hidden"
    name="account"
    value="evilsAccountNumber"/>
<input type="submit"
    value="Win Money!"/>
</form>

お金を獲得したいため、送信ボタンをクリックします。このプロセスでは、意図せずに悪意のあるユーザーに 100 ドルを送金しました。これは、悪の Web サイトがあなたの Cookie を見ることができない一方で、銀行に関連付けられた Cookie がリクエストとともに送信されるためです。

最悪の場合、このプロセス全体が JavaScript を使用して自動化された可能性があります。つまり、ボタンをクリックする必要さえありませんでした。さらに、XSS 攻撃 (英語) の被害者である正直なサイトにアクセスすると、同じように簡単に発生する可能性があります。では、このような攻撃からユーザーをどのように保護するのでしょうか?

CSRF 攻撃からの保護

CSRF 攻撃が可能な理由は、被害者の Web サイトからの HTTP リクエストと攻撃者の Web サイトからのリクエストがまったく同じです。これは、悪の Web サイトからのリクエストを拒否し、銀行の Web サイトからのリクエストを許可する方法がないことを意味します。CSRF 攻撃から保護するために、2 つのリクエストを区別できるように、悪意のあるサイトが提供できないリクエストに何かがあることを確認する必要があります。

Spring は、CSRF 攻撃から保護するための 2 つのメカニズムを提供します。

安全なメソッドはべき等でなければなりません

CSRF に対する保護を機能させるには、アプリケーションで「安全な」HTTP メソッドはべき等です (英語) を確保する必要があります。これは、HTTP メソッド GETHEADOPTIONS および TRACE を使用したリクエストがアプリケーションの状態を変更しないことを意味します。

シンクロナイザートークンパターン

CSRF 攻撃から保護するための支配的で最も包括的な方法は、シンクロナイザートークンパターン (英語) を使用することです。この解決策は、セッション Cookie に加えて、各 HTTP リクエストで、CSRF トークンと呼ばれる安全なランダム生成値が HTTP リクエストに存在する必要があることを確認することです。

HTTP リクエストが送信されると、サーバーは予想される CSRF トークンを検索し、HTTP リクエストの実際の CSRF トークンと比較する必要があります。値が一致しない場合、HTTP リクエストは拒否されます。

この動作の鍵は、実際の CSRF トークンが、ブラウザーによって自動的に含まれない HTTP リクエストの一部にある必要があることです。例:HTTP パラメーターまたは HTTP ヘッダーに実際の CSRF トークンをリクエストすると、CSRF 攻撃から保護されます。Cookie はブラウザによって HTTP リクエストに自動的に含まれるため、Cookie で実際の CSRF トークンをリクエストすることは機能しません。

アプリケーションの状態を更新する各 HTTP リクエストに対して実際の CSRF トークンのみをリクエストするように期待を緩和できます。それが機能するためには、アプリケーションは安全な HTTP メソッドがべき等であることを保証する必要があります。これにより、外部サイトからのリンクを使用して Web サイトへのリンクを許可するため、使いやすさが向上します。さらに、ランダムトークンを HTTP GET に含めないようにします。これにより、トークンがリークする可能性があります。

Synchronizer Token Pattern を使用した場合のがどのように変化するかを見てみましょう。実際の CSRF トークンは、_csrf という名前の HTTP パラメーターに含まれている必要があると仮定します。アプリケーションの転送フォームは次のようになります。

例 30: シンクロナイザートークンフォーム
<form method="post"
    action="/transfer">
<input type="hidden"
    name="_csrf"
    value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
    name="amount"/>
<input type="text"
    name="routingNumber"/>
<input type="hidden"
    name="account"/>
<input type="submit"
    value="Transfer"/>
</form>

フォームには、CSRF トークンの値を持つ非表示の入力が含まれています。外部サイトは CSRF トークンを読み取ることができません。同じ発信元ポリシーにより、悪サイトはレスポンスを読み取れないためです。

送金に対応する HTTP リクエストは次のようになります。

例 31: シンクロナイザートークンリクエスト
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721

HTTP リクエストに、安全なランダム値を持つ _csrf パラメーターが含まれていることに気付くでしょう。悪の Web サイトは、_csrf パラメーター(悪の Web サイトで明示的に提供する必要があります)に正しい値を提供できず、サーバーが実際の CSRF トークンと予想される CSRF トークンを比較すると、転送は失敗します。

SameSite 属性

CSRF 攻撃から保護する新しい方法は、Cookie で SameSite 属性 (英語) を指定することです。サーバーは、Cookie を設定するときに SameSite 属性を指定して、外部サイトから来るときに Cookie を送信しないように指定できます。

Spring Security はセッション Cookie の作成を直接制御しないため、SameSite 属性のサポートは提供しません。Spring Session は、サーブレットベースのアプリケーションで SameSite 属性のサポートを提供します。Spring Framework の CookieWebSessionIdResolver(Javadoc) は、WebFlux ベースのアプリケーションの SameSite 属性をすぐにサポートします。

たとえば、SameSite 属性を持つ HTTP レスポンスヘッダーは次のようになります。

例 32: SameSite HTTP レスポンス
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax

SameSite 属性の有効な値は次のとおりです。

  • Strict - 指定すると、同じサイト (英語) からのリクエストには Cookie が含まれます。それ以外の場合、Cookie は HTTP リクエストに含まれません。

  • Lax - 同じサイト (英語) から来るとき、またはトップレベルのナビゲーションからリクエストが来て、メソッドがべき等であるときに、指定されたクッキーが送信されるとき。それ以外の場合、Cookie は HTTP リクエストに含まれません。

SameSite 属性を使用してこの例を保護する方法を見てみましょう。銀行アプリケーションは、セッション Cookie で SameSite 属性を指定することにより、CSRF から保護できます。

セッション Cookie に SameSite 属性が設定されていると、ブラウザーは、銀行の Web サイトからのリクエストとともに JSESSIONID Cookie を送信し続けます。ただし、悪意のある Web サイトからの転送リクエストを含む JSESSIONID Cookie はブラウザーから送信されなくなります。悪の Web サイトからの転送リクエストにはセッションが存在しないため、アプリケーションは CSRF 攻撃から保護されます。

SameSite 属性を使用して CSRF 攻撃から保護する場合に注意すべき重要な考慮事項 (英語) がいくつかあります。

SameSite 属性を Strict に設定すると、強力な防御が提供されますが、ユーザーを混乱させる可能性があります。https://social.example.com (英語) でホストされているソーシャルメディアサイトにログインしたままのユーザーを考えます。ユーザーは、https://email.example.org (英語) でソーシャルメディアサイトへのリンクを含むメールを受信します。ユーザーがリンクをクリックすると、ソーシャルメディアサイトへの認証が期待されます。ただし、SameSite 属性が Strict の場合、Cookie は送信されないため、ユーザーは認証されません。

gh-7537 (GitHub) を実装することにより、CSRF 攻撃に対する SameSite 保護の保護と使いやすさを改善できます。

もう 1 つの明らかな考慮事項は、SameSite 属性がユーザーを保護するために、ブラウザーが SameSite 属性をサポートする必要があるということです。最新のブラウザのほとんどは、SameSite 属性をサポートしています (英語) 。ただし、まだ使用されている古いブラウザはそうではない場合があります。

このため、CSRF 攻撃に対する唯一の保護ではなく、SameSite 属性を徹底的な防御として使用することをお勧めします。

CSRF 保護を使用する場合

いつ CSRF 保護を使用する必要がありますか?通常のユーザーがブラウザで処理できるリクエストには CSRF 保護を使用することをお勧めします。ブラウザ以外のクライアントが使用するサービスのみを作成する場合は、CSRF 保護を無効にすることをお勧めします。

CSRF 保護と JSON

よくある質問は、「javascript によって作成された JSON リクエストを保護する必要がありますか?」です。短い答えは、依存します。ただし、JSON リクエストに影響を与える可能性のある CSRF エクスプロイトがあるため、非常に注意する必要があります。例:悪意のあるユーザーは次の形式を使用した JSON を使用した CSRF (英語) を作成できます:

例 33: JSON 形式の CSRF
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
    <input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
    <input type="submit"
        value="Win Money!"/>
</form>

これにより、次の JSON 構造が生成されます

例 34: JSON リクエストを使用した CSRF
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

アプリケーションが Content-Type を検証していない場合、このエクスプロイトにさらされます。セットアップに応じて、Content-Type を検証する Spring MVC アプリケーションは、以下に示すように .json で終わるように URL サフィックスを更新することにより、依然として悪用される可能性があります。

例 35: JSON Spring MVC フォームを使用した CSRF
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
    <input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
    <input type="submit"
        value="Win Money!"/>
</form>
CSRF およびステートレスブラウザーアプリケーション

アプリケーションがステートレスの場合はどうなるでしょうか?それは必ずしも保護されているという意味ではありません。実際、ユーザーが特定のリクエストに対して Web ブラウザーでアクションを実行する必要がない場合、ユーザーは CSRF 攻撃に対して依然として脆弱です。

例:JSESSIONID の代わりに、認証のためにすべての状態を含むカスタム Cookie を使用するアプリケーションを検討します。CSRF 攻撃が行われると、前の例で JSESSIONID Cookie が送信されたのと同じ方法で、リクエストとともにカスタム Cookie が送信されます。このアプリケーションは、CSRF 攻撃に対して脆弱です。

基本認証を使用するアプリケーションも CSRF 攻撃に対して脆弱です。前の例で JSESSIONID Cookie が送信されたのと同じ方法で、ブラウザがリクエストにユーザー名とパスワードを自動的に含めるため、アプリケーションは脆弱です。

CSRF の考慮事項

CSRF 攻撃に対する保護を実装する際に考慮すべき特別な考慮事項がいくつかあります。

ログイン

ログインリクエストの偽造 (英語) を防ぐには、HTTP リクエスト (英語) のログインを CSRF 攻撃から保護する必要があります。悪意のあるユーザーが被害者の機密情報を読み取れないように、ログインリクエストの偽造に対する保護が必要です。攻撃は次によって実行されます。

  • 悪意のあるユーザーは、悪意のあるユーザーの資格情報を使用して CSRF ログインを実行します。これで、被害者は悪意のあるユーザーとして認証されます。

  • 悪意のあるユーザーは、標的の Web サイトにアクセスして機密情報を入力するように被害者をだます

  • 情報は悪意のあるユーザーのアカウントに関連付けられているため、悪意のあるユーザーは自分の資格情報でログインし、被害者の機密情報を表示できます

ログイン HTTP リクエストが CSRF 攻撃から保護されるようにするための複雑な問題は、リクエストが拒否される原因となるセッションタイムアウトがユーザーに発生する可能性があることです。セッションタイムアウトは、ログインするためにセッションを必要としないユーザーにとっては驚きです。詳細については、CSRF およびセッションタイムアウトを参照してください。

ログアウト

ログアウトリクエストの偽造を防ぐには、ログアウト HTTP リクエストを CSRF 攻撃から保護する必要があります。悪意のあるユーザーが被害者の機密情報を読み取れないように、ログアウトリクエストの偽造に対する保護が必要です。攻撃の詳細については、このブログ投稿を参照して (英語) ください。

ログアウト HTTP リクエストが CSRF 攻撃から保護されることを確実にするための複雑な問題は、リクエストが拒否される原因となるセッションタイムアウトがユーザーに発生する可能性があることです。セッションタイムアウトは、ログアウトするためにセッションを持つ必要がないと考えているユーザーにとっては驚くべきことです。詳細については、CSRF およびセッションタイムアウトを参照してください。

CSRF およびセッションタイムアウト

多くの場合、予想される CSRF トークンはセッションに保存されます。これは、セッションが期限切れになるとすぐに、サーバーが期待される CSRF トークンを見つけられず、HTTP リクエストを拒否することを意味します。タイムアウトを解決するための多くのオプションがあり、それぞれにトレードオフがあります。

  • タイムアウトを軽減する最善の方法は、JavaScript を使用してフォーム送信時に CSRF トークンをリクエストすることです。その後、フォームは CSRF トークンで更新され、送信されます。

  • 別のオプションは、セッションが期限切れになることをユーザーに知らせる JavaScript を用意することです。ユーザーはボタンをクリックしてセッションを続行し、リフレッシュできます。

  • 最後に、期待される CSRF トークンを Cookie に保存できます。これにより、予想される CSRF トークンがセッションより長く存続できます。

    予想される CSRF トークンがデフォルトで Cookie に保存されない理由を確認するかもしれません。これは、ヘッダー(Cookie の指定)を別のドメインで設定できる既知のエクスプロイトがあるためです。これは、Rails 上の Ruby がヘッダー X-Requested-With が存在するときに CSRF チェックをスキップしなくなっ (英語) たのと同じ理由です。エクスプロイトの実行メソッドの詳細については、この webappsec.org スレッド (英語) を参照してください。もう 1 つの欠点は、状態(タイムアウト)を削除すると、トークンが侵害された場合にトークンを強制的に終了する機能が失われることです。

マルチパート (ファイルアップロード)

CSRF 攻撃からマルチパートリクエスト(ファイルのアップロード)を保護すると、鶏と卵 (英語) の問題が発生します。CSRF 攻撃の発生を防ぐには、HTTP リクエストの本文を読み取って実際の CSRF トークンを取得する必要があります。ただし、本文を読むことは、ファイルがアップロードされることを意味し、外部サイトがファイルをアップロードできることを意味します。

multipart/form-data で CSRF 保護を使用するには、2 つのオプションがあります。各オプションにはトレードオフがあります。

Spring Security の CSRF 保護をマルチパートファイルアップロードと統合する前に、CSRF 保護なしでアップロードできることを確認してください。Spring でのマルチパートフォームの使用に関する詳細情報は、Spring リファレンスの 1.1.11. マルチパートリゾルバーセクションおよび MultipartFilter javadoc 内にあります。

CSRF トークンを本文に配置する

最初のオプションは、リクエストの本文に実際の CSRF トークンを含めることです。CSRF トークンを本文に配置することにより、認証が実行される前に本文が読み取られます。つまり、誰でもサーバーに一時ファイルを置くことができます。ただし、認可されたユーザーのみが、アプリケーションで処理されるファイルを送信できます。一時ファイルのアップロードはほとんどのサーバーにほとんど影響を与えないため、これは一般的に推奨されるアプローチです。

URL に CSRF トークンを含める

認可されていないユーザーが一時ファイルをアップロードすることを認可しない場合は、フォームのアクション属性にクエリパラメーターとして予想される CSRF トークンを含めることもできます。このアプローチの欠点は、クエリパラメーターがリークする可能性があることです。より一般的には、機密データが漏れないように、本文またはヘッダー内に機密データを配置することをお勧めします。追加情報は URI の機密情報をエンコードする RFC 2616 セクション 15.1.3 (英語) にあります。

HiddenHttpMethodFilter

一部のアプリケーションでは、HTTP メソッドをオーバーライドするためにフォームパラメーターを使用できます。例:以下の形式は、HTTP メソッドを post ではなく delete として扱うために使用できます。

例 36: CSRF 非表示 HTTP メソッドフォーム
<form action="/process"
    method="post">
    <!-- ... -->
    <input type="hidden"
        name="_method"
        value="delete"/>
</form>

HTTP メソッドのオーバーライドはフィルターで発生します。そのフィルターは、Spring Security のサポートの前に配置する必要があります。オーバーライドは post でのみ発生するため、実際に問題が発生する可能性はほとんどありません。ただし、Spring Security のフィルターの前に配置することをお勧めします。

5.2.2. セキュリティ HTTP レスポンスヘッダー

ドキュメントのこのパートでは、セキュリティ HTTP レスポンスヘッダーの一般的なトピックについて説明します。Security HTTP Response Headers サーブレットおよび WebFlux ベースのアプリケーションに関する特定の情報については、関連するセクションを参照してください。

Web アプリケーションのセキュリティを強化するために使用できる多くの HTTP レスポンスヘッダー (英語) があります。このセクションは、Spring Security が明示的にサポートするさまざまな HTTP レスポンスヘッダー専用です。必要に応じて、Spring Security を構成してカスタムヘッダーを提供することもできます。

デフォルトのセキュリティヘッダー

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

Spring Security は、セキュリティ関連の HTTP レスポンスヘッダーのデフォルトセットを提供して、安全なデフォルトを提供します。

Spring Security のデフォルトでは、次のヘッダーが含まれます。

例 37: デフォルトのセキュリティ HTTP レスポンスヘッダー
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Strict-Transport-Security は HTTPS リクエストでのみ追加されます

デフォルトがニーズを満たさない場合、これらのデフォルトからヘッダーを簡単に削除、変更、または追加できます。これらの各ヘッダーの詳細については、対応するセクションを参照してください。

キャッシュ制御

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

Spring Security のデフォルトでは、キャッシュを無効にしてユーザーのコンテンツを保護します。

ユーザーが機密情報を表示するために認証してからログアウトする場合、悪意のあるユーザーが戻るボタンをクリックして機密情報を表示できないようにする必要があります。デフォルトで送信されるキャッシュ制御ヘッダーは次のとおりです。

例 38: デフォルトのキャッシュ制御 HTTP レスポンスヘッダー
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

デフォルトでセキュアにするために、Spring Security はデフォルトでこれらのヘッダーを追加します。ただし、アプリケーションが独自のキャッシュ制御ヘッダーを提供する場合、Spring Security は邪魔になりません。これにより、アプリケーションは、CSS や JavaScript などの静的リソースを確実にキャッシュできます。

コンテンツタイプオプション

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

歴史的に、Internet Explorer を含むブラウザは、コンテンツスニッフィング (英語) を使用してリクエストのコンテンツタイプを推測しようとしました。これにより、ブラウザーは、コンテンツタイプを指定していないリソースのコンテンツタイプを推測することで、ユーザーエクスペリエンスを向上させることができました。例:ブラウザーが、コンテンツタイプが指定されていない JavaScript ファイルを検出した場合、コンテンツタイプを推測して実行できます。

コンテンツのアップロードを許可する場合、行うべき追加事項が多数あります(つまり、ドキュメントを個別のドメインでのみ表示する、Content-Type ヘッダーが設定されていることを確認する、ドキュメントをサニタイズするなど)。ただし、これらの手段は Spring Security が提供するものの範囲外です。また、コンテンツスニッフィングを無効にする場合に指摘することも重要です。適切に機能するには、コンテンツタイプを指定する必要があります。

コンテンツスニッフィングの問題は、悪意のあるユーザーがポリグロット(つまり、複数のコンテンツタイプとして有効なファイル)を使用して XSS 攻撃を実行できることです。例:一部のサイトでは、ユーザーが有効なポストスクリプトドキュメントを Web サイトに送信して表示できる場合があります。悪意のあるユーザーは、有効な JavaScript ファイルでもあるポストスクリプトドキュメント (英語) を作成し、XSS 攻撃を実行する可能性があります。

Spring Security は、次のヘッダーを HTTP レスポンスに追加することにより、デフォルトでコンテンツスニッフィングを無効にします。

例 39: nosniff HTTP レスポンスヘッダー
X-Content-Type-Options: nosniff
HTTP 厳格なトランスポートセキュリティ (HSTS)

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

銀行の Web サイトに入力するときに、mybank.example.com と入力しますか、それとも https://mybank.example.com (英語) と入力しますか? https プロトコルを省略すると、潜在的に中間者攻撃 (英語) に対して脆弱になります。Web サイトが https://mybank.example.com (英語) へのリダイレクトを実行した場合でも、悪意のあるユーザーは最初の HTTP リクエストをインターセプトしてレスポンスを操作できます(つまり、https://mibank.example.com (英語) にリダイレクトして資格情報を盗みます)。

多くのユーザーが https プロトコルを省略しているため、HTTP 厳格なトランスポートセキュリティ (HSTS) (英語) が作成されます。mybank.example.com が HSTS ホスト (英語) として追加されると、ブラウザは mybank.example.com へのリクエストが https://mybank.example.com (英語) として解釈されるべきであることを事前に知ることができます。これにより、中間者攻撃が発生する可能性が大幅に減少します。

RFC6797 (英語) に従って、HSTS ヘッダーは HTTPS レスポンスにのみ挿入されます。ブラウザーがヘッダーを確認するには、ブラウザーは最初に、SSL 証明書だけでなく、接続に使用する SSL 証明書に署名した CA を信頼する必要があります。

サイトを HSTS ホストとしてマークする 1 つのメソッドは、ホストをブラウザーにプリロードすることです。もう 1 つのメソッドは、Strict-Transport-Security ヘッダーをレスポンスに追加することです。例:Spring Security のデフォルトの動作は、ブラウザにドメインを 1 年間 HSTS ホストとして扱うよう指示する次のヘッダーを追加することです(1 年で約 31536000 秒あります)。

例 40: 厳密なトランスポートセキュリティ HTTP レスポンスヘッダー
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload

オプションの includeSubDomains ディレクティブは、サブドメイン(つまり、secure.mybank.example.com)も HSTS ドメインとして扱う必要があることをブラウザに指示します。

オプションの preload ディレクティブは、ブラウザーに HSTS ドメインとしてドメインをプリロードするよう指示します。HSTS プリロードの詳細については、https://hstspreload.org (英語) を参照してください。

HTTP 公開キーの固定 (HPKP)

パッシブを維持するために、Spring Security はサーブレット環境で HPKP をサポートしていますが、上記の理由により、HPKP はセキュリティチームによって推奨されなくなりました。

HTTP 公開キーの固定 (HPKP) (英語) は、偽造された証明書による中間者(MITM)攻撃を防ぐために特定の Web サーバーで使用する公開鍵を Web クライアントに指定します。HPKP を正しく使用すると、侵害された証明書に対する保護層を追加できます。ただし、HPKP は複雑であるため、多くの専門家は、HPKP と Chrome はサポートを削除しました (英語) の使用を推奨していません。

HPKP が推奨されなくなった理由の詳細については、 HTTP 公開キーのピン留めは無効ですか? (英語) および HPKP をあきらめています (英語) を参照してください。

X-Frame-Options

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

Web サイトをフレームに追加できるようにすることは、セキュリティ上の課題になる可能性があります。例:賢い CSS スタイリングを使用すると、ユーザーはだまされて意図しないものをクリックする可能性があります(ビデオデモ (英語) )。例:銀行にログインしているユーザーは、他のユーザーにアクセスを許可するボタンをクリックする場合があります。この種の攻撃はクリックジャッキング (英語) として知られています。

クリックジャッキングに対処する別の現代的なアプローチは、コンテンツセキュリティポリシー (CSP) を使用することです。

クリックジャック攻撃を緩和する方法はいくつかあります。例:クリックジャッキング (英語) 攻撃からレガシーブラウザを保護するために、フレーム破壊コードを使用できます (英語) 。完璧ではありませんが、フレームブレークコードはレガシーブラウザでできる最善の方法です。

クリックジャックに対処するためのより現代的なアプローチは、X-Frame-Options (英語) ヘッダーを使用することです。デフォルトでは、Spring Security は次のヘッダーを使用して iframe 内のページのレンダリングを無効にします。

X-Frame-Options: DENY
X-XSS-Protection

関連セクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方のデフォルトをカスタマイズする方法を確認してください。

一部のブラウザには、リフレクションされた XSS 攻撃 (英語) を除外するためのサポートが組み込まれています。これは決して絶対確実ではありませんが、XSS 保護を支援します。

通常、フィルタリングはデフォルトで有効になっているため、ヘッダーを追加すると、通常は有効になり、XSS 攻撃が検出されたときにブラウザに何をするかが指示されます。例:フィルターは、コンテンツを最も侵襲性の低い方法で変更して、すべてをレンダリングしようとする場合があります。時々、このタイプの交換は XSS 自体の脆弱性 (英語) になることがあります。代わりに、コンテンツを修正するのではなく、ブロックすることをお勧めします。デフォルトでは、Spring Security は次のヘッダーを使用してコンテンツをブロックします。

X-XSS-Protection: 1; mode=block
コンテンツセキュリティポリシー (CSP)

関連するセクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方を設定する方法を確認してください。

コンテンツセキュリティポリシー (CSP) (英語) は、クロスサイトスクリプティング(XSS)などのコンテンツインジェクションの脆弱性を軽減するために Web アプリケーションが活用できるメカニズムです。CSP は、Web アプリケーションの作成者が Web アプリケーションがリソースをロードすることを期待しているソースを宣言し、最終的にクライアント(ユーザーエージェント)に通知する機能を提供する宣言型ポリシーです。

コンテンツセキュリティポリシーは、すべてのコンテンツインジェクションの脆弱性を解決するためのものではありません。代わりに、CSP を活用して、コンテンツインジェクション攻撃による被害を軽減することができます。防衛の最前線として、Web アプリケーションの作成者は入力を検証し、出力をエンコードする必要があります。

Web アプリケーションは、次の HTTP ヘッダーのいずれかをレスポンスに含めることにより、CSP の使用を採用できます。

  • Content-Security-Policy

  • Content-Security-Policy-Report-Only

これらの各ヘッダーは、セキュリティポリシーをクライアントに配信するメカニズムとして使用されます。セキュリティポリシーには、特定のリソース表現の制限を宣言するセキュリティポリシーディレクティブのセットが含まれています。

例:Web アプリケーションは、レスポンスに次のヘッダーを含めることにより、特定の信頼できるソースからスクリプトを読み込むことを宣言できます。

例 41: コンテンツセキュリティポリシーの例
Content-Security-Policy: script-src https://trustedscripts.example.com

script-src ディレクティブで宣言されているもの以外の別のソースからスクリプトをロードしようとすると、ユーザーエージェントによってブロックされます。さらに、report-uri (英語) ディレクティブがセキュリティポリシーで宣言されている場合、ユーザーエージェントによって、宣言された URL に違反が報告されます。

例:Web アプリケーションが宣言されたセキュリティポリシーに違反する場合、次のレスポンスヘッダーは、ポリシーの report-uri ディレクティブで指定された URL に違反レポートを送信するようユーザーエージェントに指示します。

例 42: report-uri を使用したコンテンツセキュリティポリシー
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

違反レポート (英語) は、Web アプリケーションの独自の API または https://report-uri.io/ (英語) などのパブリックにホストされた CSP 違反レポートサービスのいずれかによってキャプチャーできる標準の JSON 構造です。

Content-Security-Policy-Report-Only ヘッダーは、Web アプリケーションの作成者および管理者がセキュリティポリシーを実施するのではなく、監視する機能を提供します。通常、このヘッダーは、サイトのセキュリティポリシーを実験および / または開発するときに使用されます。ポリシーが有効であると見なされる場合、代わりに Content-Security-Policy ヘッダーフィールドを使用してポリシーを施行できます。

次のレスポンスヘッダーを指定すると、ポリシーは、2 つの可能なソースのいずれかからスクリプトをロードできることを宣言します。

例 43: コンテンツセキュリティポリシーレポートのみ
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

サイトがこのポリシーに違反する場合、evil.com からスクリプトをロードしようとすると、ユーザーエージェントは、report-uri ディレクティブで指定された宣言された URL に違反レポートを送信しますが、それでも違反リソースはロードできます。

Web アプリケーションへのコンテンツセキュリティポリシーの適用は、多くの場合、重要な作業です。次のリソースは、サイトの効果的なセキュリティポリシーを開発する際にさらに役立つ場合があります。

リファラーポリシー

関連するセクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方を設定する方法を確認してください。

リファラーポリシー (英語) は、Web アプリケーションがリファラーフィールドを管理するために活用できるメカニズムです。リファラーフィールドには、ユーザーが最後にアクセスしたページが含まれます。

Spring Security のアプローチは、異なるポリシー (英語) を提供するリファラーポリシー (英語) ヘッダーを使用することです。

例 44: リファラーポリシーの例
Referrer-Policy: same-origin

Referrer-Policy レスポンスヘッダーは、ユーザーが以前いたソースを宛先に知らせるようにブラウザーに指示します。

機能ポリシー

関連するセクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方を設定する方法を確認してください。

機能ポリシー (英語) は、Web 開発者がブラウザの特定の API および Web 機能の動作を選択的に有効化、無効化、および変更できるようにするメカニズムです。

例 45: 機能ポリシーの例
Feature-Policy: geolocation 'self'

機能ポリシーを使用すると、開発者はブラウザの一連の「ポリシー」をオプトインして、サイト全体で使用される特定の機能に適用できます。これらのポリシーは、サイトがアクセスできる API を制限したり、特定の機能に対するブラウザのデフォルトの動作を変更したりします。

サイトデータを消去する

関連するセクションを参照して、サーブレットベースのアプリケーションと webflux ベースのアプリケーションの両方を設定する方法を確認してください。

サイトデータを消去する (英語) は、HTTP レスポンスに次のヘッダーが含まれている場合に、ブラウザ側のデータ(Cookie、ローカルストレージなど)を削除できるメカニズムです。

Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"

これは、ログアウト時に実行するクリーンアップアクションです。

カスタムヘッダー

関連するセクションを参照して、両方のサーブレットベースのアプリケーションを構成する方法を確認してください。

Spring Security には、より一般的なセキュリティヘッダーをアプリケーションに追加するのに便利なメカニズムがあります。ただし、カスタムヘッダーの追加を可能にするフックも提供します。

5.2.3. HTTP

フレームワークとして、Spring Security は HTTP 接続を処理しないため、HTTPS を直接サポートしません。ただし、HTTPS の使用に役立つ多くの機能を提供します。

HTTPS にリダイレクト

クライアントが HTTP を使用する場合、Spring Security は、サーブレット環境と WebFlux 環境の両方を HTTPS にリダイレクトするように構成できます。

厳格な輸送セキュリティ

Spring Security は厳格な輸送セキュリティをサポートし、デフォルトで有効にします。

プロキシサーバー構成

プロキシサーバーを使用する場合は、アプリケーションが正しく構成されていることを確認することが重要です。例 : 多くのアプリケーションには、https://example.com/ (英語) のリクエストに応答するロードバランサがあり、https://192.168.1:8080 (英語) のアプリケーションサーバーにリクエストを転送します。適切に構成しないと、アプリケーションサーバーはロードバランサが存在することを認識せず、クライアントから https://192.168.1:8080 (英語) がリクエストされたかのようにリクエストを処理します。

これを修正するには、RFC 7239 (英語) を使用して、ロードバランサーが使用されていることを指定します。アプリケーションがこれを認識できるようにするには、X-Forwarded ヘッダーを認識するようにアプリケーションサーバーを構成する必要があります。たとえば、Tomcat は RemoteIpValve (Apache) を使用し、Jetty は ForwardedRequestCustomizer (英語) を使用します。または、Spring ユーザーは ForwardedHeaderFilter (GitHub) を活用できます。

Spring Boot ユーザーは、server.use-forward-headers プロパティを使用してアプリケーションを構成できます。詳細については、Spring Boot のドキュメントを参照してください。

6. プロジェクトモジュール

Spring Security 3.0, では、コードベースが別々の jar に細分され、異なる機能領域とサードパーティの依存関係をより明確に分離しました。Maven を使用してプロジェクトをビルドする場合、これらは pom.xml に追加する必要があるモジュールです。Maven を使用しない場合でも、pom.xml ファイルを参照して、サードパーティの依存関係とバージョンを把握することをお勧めします。別の良いアイデアは、サンプルアプリケーションに含まれているライブラリを調べることです。

6.1. コア  — spring-security-core.jar

このモジュールには、コア認証とアクセス制御のクラスとインターフェース、リモートサポート、および基本的なプロビジョニング API が含まれています。Spring Security を使用するすべてのアプリケーションで必要です。スタンドアロンアプリケーション、リモートクライアント、メソッド(サービスレイヤー)セキュリティ、および JDBC ユーザープロビジョニングをサポートしています。次のトップレベルパッケージが含まれています。

  • org.springframework.security.core

  • org.springframework.security.access

  • org.springframework.security.authentication

  • org.springframework.security.provisioning

6.2. リモーティング  — spring-security-remoting.jar

このモジュールは、Spring Remoting との統合を提供します。Spring Remoting を使用するリモートクライアントを作成していない限り、これは必要ありません。メインパッケージは org.springframework.security.remoting です。

6.3. Web — spring-security-web.jar

このモジュールには、フィルターと関連する Web セキュリティインフラストラクチャコードが含まれています。サーブレット API に依存するものがすべて含まれています。Spring Security Web 認証サービスと URL ベースのアクセス制御が必要な場合に必要です。メインパッケージは org.springframework.security.web です。

6.4. 構成  — spring-security-config.jar

このモジュールには、セキュリティ名前空間解析コードと Java 構成コードが含まれています。Spring Security XML 名前空間を構成に使用する場合、または Spring Security の Java 構成サポートを使用する場合に必要です。メインパッケージは org.springframework.security.config です。どのクラスも、アプリケーションで直接使用するためのものではありません。

6.5. LDAP — spring-security-ldap.jar

このモジュールは、LDAP 認証およびプロビジョニングコードを提供します。LDAP 認証を使用するか、LDAP ユーザーエントリを管理する必要がある場合に必要です。最上位パッケージは org.springframework.security.ldap です。

6.6. OAuth 2.0 コア  — spring-security-oauth2-core.jar

spring-security-oauth2-core.jar には、OAuth 2.0 認可フレームワークおよび OpenID Connect Core 1.0 のサポートを提供するコアクラスとインターフェースが含まれています。クライアント、リソースサーバー、認証サーバーなど、OAuth 2.0 または OpenID Connect Core 1.0 を使用するアプリケーションに必要です。最上位パッケージは org.springframework.security.oauth2.core です。

6.7. OAuth 2.0 クライアント  — spring-security-oauth2-client.jar

spring-security-oauth2-client.jar には、OAuth 2.0 認可フレームワークおよび OpenID Connect Core 1.0 に対する Spring Security のクライアントサポートが含まれています。OAuth 2.0 ログインまたは OAuth クライアントサポートを使用するアプリケーションに必要です。最上位パッケージは org.springframework.security.oauth2.client です。

6.8. OAuth 2.0 JOSE — spring-security-oauth2-jose.jar

spring-security-oauth2-jose.jar には、Spring Security による JOSE(Javascript Object Signing and Encryption)フレームワークのサポートが含まれています。JOSE フレームワークは、当事者間でクレームを安全に転送する方法を提供することを目的としています。仕様のコレクションから構築されています。

  • JSON Web トークン (JWT)

  • JSON Web 署名 (JWS)

  • JSON Web 暗号化 (JWE)

  • JSON Web キー (JWK)

次のトップレベルパッケージが含まれています。

  • org.springframework.security.oauth2.jwt

  • org.springframework.security.oauth2.jose

6.9. OAuth 2.0 リソースサーバー  — spring-security-oauth2-resource-server.jar

spring-security-oauth2-resource-server.jar には、OAuth 2.0 リソースサーバーに対する Spring Security のサポートが含まれています。OAuth 2.0 ベアラートークンを介して API を保護するために使用されます。最上位パッケージは org.springframework.security.oauth2.server.resource です。

6.10. ACL — spring-security-acl.jar

このモジュールには、特殊なドメインオブジェクト ACL 実装が含まれています。アプリケーション内の特定のドメインオブジェクトインスタンスにセキュリティを適用するために使用されます。最上位パッケージは org.springframework.security.acls です。

6.11. CAS — spring-security-cas.jar

このモジュールには、Spring Security の CAS クライアント統合が含まれています。CAS シングルサインオンサーバーで Spring Security Web 認証を使用する場合は、これを使用する必要があります。最上位パッケージは org.springframework.security.cas です。

6.12. OpenID — spring-security-openid.jar

このモジュールには、OpenID Web 認証サポートが含まれています。外部 OpenID サーバーに対してユーザーを認証するために使用されます。最上位パッケージは org.springframework.security.openid です。OpenID4Java が必要です。

6.13. テスト  — spring-security-test.jar

このモジュールには、Spring Security によるテストのサポートが含まれています。

7. サンプル

Spring Security には、多くのサンプル (GitHub) アプリケーションが含まれています。

サーブレットアプリケーション

Spring Security は、標準のサーブレット Filter を使用してサーブレットコンテナーと統合します。つまり、サーブレットコンテナーで実行されるすべてのアプリケーションで動作します。より具体的には、Spring Security を利用するために、サーブレットベースのアプリケーションで Spring を使用する必要はありません。

8. Hello Spring Security

このセクションでは、Spring Security を Spring Boot で使用するための最小セットアップについて説明します。

完成したアプリケーションは、で最小限の Spring Boot + Spring Security アプリケーションをダウンロードすることができ、あなたの便宜上 samples/boot/helloworld (GitHub) で見つけることができますこちらをクリック

8.1. 依存関係の更新

必要な唯一の手順は、Maven または Gradle を使用して依存関係を更新することです。

8.2. Hello Spring Security Boot の開始

Maven プラグインの run ゴールを使用して、Spring Boot アプリケーションを実行できるようになりました。次の例は、その方法を示しています(そして、その結果の出力の始まり)。

例 46: Spring Boot アプリケーションの実行
$ ./mvn spring-boot:run
...
INFO 23689 --- [  restartedMain] .s.s.UserDetailsServiceAutoConfiguration :

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

...

8.3. Spring Boot 自動構成

Spring Boot が自動的に:

  • Spring Security のデフォルト構成を有効にします。これにより、サーブレット Filter が springSecurityFilterChain という名前の Bean として作成されます。この Bean は、アプリケーション内のすべてのセキュリティ(アプリケーション URL の保護、送信されたユーザー名とパスワードの検証、ログインフォームへのリダイレクトなど)を担当します。

  • user のユーザー名と、コンソールに記録されるランダムに生成されたパスワードを使用して、UserDetailsService Bean を作成します。

  • Filter を springSecurityFilterChain という名前の Bean に登録し、リクエストごとにサーブレットコンテナーに登録します。

Spring Boot はあまり構成していませんが、多くのことを行います。機能の概要は次のとおりです。

9. サーブレットのセキュリティ : 全体像

このセクションでは、サーブレットベースのアプリケーション内での Spring Security の高レベルアーキテクチャについて説明します。リファレンスの認証認可エクスプロイトに対する保護セクション内のこの高レベルの理解の上に構築します。

9.1. Filter のレビュー

Spring Security のサーブレットサポートは、サーブレット Filter に基づいているため、一般に最初に Filter のロールを確認すると役立ちます。以下の図は、単一の HTTP リクエストのハンドラーの典型的な階層化を示しています。

filterchain
図 1: FilterChain

クライアントはアプリケーションにリクエストを送信し、コンテナーは、リクエスト URI のパスに基づいて HttpServletRequest を処理する Filter および Servlet を含む FilterChain を作成します。Spring MVC アプリケーションでは、Servlet は DispatcherServlet のインスタンスです。最大 1 つの Servlet が単一の HttpServletRequest および HttpServletResponse を処理できます。ただし、複数の Filter を使用して以下を実行できます。

  • ダウンストリーム Filter または Servlet が呼び出されないようにします。この場合、Filter は通常 HttpServletResponse を書き込みます。

  • ダウンストリーム Filter および Servlet が使用する HttpServletRequest または HttpServletResponse を変更する

Filter のパワーは、それに渡される FilterChain から得られます。

例 47: FilterChain の使用例
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}

Filter は下流の Filter と Servlet にのみ影響を与えるため、各 Filter が呼び出される順序は非常に重要です。

9.2. DelegatingFilterProxy

Spring は、サーブレットコンテナーのライフサイクルと Spring の ApplicationContext の間のブリッジングを可能にする DelegatingFilterProxy(Javadoc) という名前の Filter 実装を提供します。サーブレットコンテナーでは、独自の標準を使用して Filter を登録できますが、Spring 定義の Bean を認識しません。DelegatingFilterProxy は標準のサーブレットコンテナーメカニズムを介して登録できますが、すべての作業を Filter を実装する Spring Bean に委譲します。

DelegatingFilterProxy が Filter および FilterChain にどのように適合するかの写真です。

delegatingfilterproxy
図 2: DelegatingFilterProxy

DelegatingFilterProxy は ApplicationContext から Bean フィルター 0 を検索し、その後 Bean フィルター 0 を起動ます。DelegatingFilterProxy の擬似コードは以下のとおりです。

例 48: DelegatingFilterProxy 擬似コード
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // Lazily get Filter that was registered as a Spring Bean
    // For the example in DelegatingFilterProxydelegateis an instance of Bean Filter0
    Filter delegate = getFilterBean(someBeanName);
    // delegate work to the Spring Bean
    delegate.doFilter(request, response);
}

DelegatingFilterProxy のもう 1 つの利点は、Filter Bean インスタンスの表示を遅らせることができることです。コンテナーは、コンテナーを起動する前に Filter インスタンスを登録する必要があるため、これは重要です。ただし、Spring は通常 ContextLoaderListener を使用して Spring Bean をロードしますが、これは Filter インスタンスの登録が必要になるまで実行されません。

9.3. FilterChainProxy

Spring Security のサーブレットサポートは FilterChainProxy に含まれています。FilterChainProxy は、SecurityFilterChain を介して多くの Filter インスタンスに委譲できる、Spring Security によって提供される特別な Filter です。FilterChainProxy は Bean であるため、通常は DelegatingFilterProxy にラップされます。

filterchainproxy
図 3: FilterChainProxy

9.4. SecurityFilterChain

SecurityFilterChain(Javadoc) は、このリクエストに対してどの Spring Security Filter を呼び出す必要があるかを判別するために FilterChainProxy によって使用されます。

securityfilterchain
図 4: SecurityFilterChain

SecurityFilterChain のセキュリティフィルターは通常 Beans ですが、DelegatingFilterProxy ではなく FilterChainProxy に登録されています。FilterChainProxy は、サーブレットコンテナーまたは DelegatingFilterProxy に直接登録することに対して多くの利点を提供します。まず、Spring Security のすべてのサーブレットサポートの開始点を提供します。そのため、Spring Security のサーブレットサポートのトラブルシューティングを試みる場合は、FilterChainProxy にデバッグポイントを追加することから始めるのが最適です。

第二に、FilterChainProxy は Spring Security の使用の中心であるため、オプションとして表示されないタスクを実行できます。例: SecurityContext をクリアして、メモリリークを回避します。また、Spring Security の HttpFirewall を適用して、特定の種類の攻撃からアプリケーションを保護します。

さらに、SecurityFilterChain を呼び出すタイミングをより柔軟に決定できます。サーブレットコンテナーでは、Filter は URL のみに基づいて呼び出されます。ただし、FilterChainProxy は、RequestMatcher インターフェースを活用することにより、HttpServletRequest の内容に基づいて呼び出しを判別できます。

実際、FilterChainProxy を使用して、どの SecurityFilterChain を使用するかを決定できます。これにより、アプリケーションの場合、異なるスライスにまったく別の構成を提供できます。

multi securityfilterchain
図 5: 複数の SecurityFilterChain

複数の SecurityFilterChain 図では、FilterChainProxy はどの SecurityFilterChain を使用するかを決定します。一致する最初の SecurityFilterChain のみが呼び出されます。/api/messages/ の URL がリクエストされた場合、SecurityFilterChain0 のパターン /api/** で最初に一致するため、SecurityFilterChainn でも一致しますが、SecurityFilterChain0 のみが呼び出されます。/messages/ の URL がリクエストされた場合、SecurityFilterChain0 の /api/** のパターンと一致しないため、FilterChainProxy は各 SecurityFilterChain の試行を続けます。他のものがないと仮定すると、SecurityFilterChain インスタンスは SecurityFilterChainn と一致します。

SecurityFilterChain0 には 3 つのセキュリティ Filter インスタンスのみが構成されていることに注意してください。ただし、SecurityFilterChainn には 4 つのセキュリティ Filter が構成されています。各 SecurityFilterChain は一意であり、個別に構成できることに注意することが重要です。実際、アプリケーションが Spring Security に特定のリクエストを無視させたい場合、SecurityFilterChain のセキュリティ Filter はゼロになる可能性があります。

9.5. セキュリティフィルター

セキュリティフィルターは、SecurityFilterChain API を使用して FilterChainProxy に挿入されます。Filter の順序は重要です。通常、Spring Security の Filter の順序を知る必要はありません。ただし、順序を知ることが有益な場合があります

以下は、Spring Security フィルターの順序の包括的なリストです。

  • ChannelProcessingFilter

  • ConcurrentSessionFilter

  • WebAsyncManagerIntegrationFilter

  • SecurityContextPersistenceFilter

  • HeaderWriterFilter

  • CorsFilter

  • CsrfFilter

  • LogoutFilter

  • OAuth2AuthorizationRequestRedirectFilter

  • Saml2WebSsoAuthenticationRequestFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • CasAuthenticationFilter

  • OAuth2LoginAuthenticationFilter

  • Saml2WebSsoAuthenticationFilter

  • UsernamePasswordAuthenticationFilter

  • ConcurrentSessionFilter

  • OpenIDAuthenticationFilter

  • DefaultLoginPageGeneratingFilter

  • DefaultLogoutPageGeneratingFilter

  • DigestAuthenticationFilter

  • BearerTokenAuthenticationFilter

  • BasicAuthenticationFilter

  • RequestCacheAwareFilter

  • SecurityContextHolderAwareRequestFilter

  • JaasApiIntegrationFilter

  • RememberMeAuthenticationFilter

  • AnonymousAuthenticationFilter

  • OAuth2AuthorizationCodeGrantFilter

  • SessionManagementFilter

  • ExceptionTranslationFilter

  • FilterSecurityInterceptor

  • SwitchUserFilter

9.6. セキュリティ例外の処理

ExceptionTranslationFilter(Javadoc) では、AccessDeniedException(Javadoc) および AuthenticationException(Javadoc) を HTTP レスポンスに変換できます。

ExceptionTranslationFilter は、セキュリティフィルターの 1 つとして FilterChainProxy に挿入されます。

exceptiontranslationfilter
  •  番号 1 最初に、ExceptionTranslationFilter は FilterChain.doFilter(request, response) を呼び出して、アプリケーションの残りを呼び出します。

  • 2 番 ユーザーが認証されていないか、AuthenticationException である場合、認証開始します。

    • SecurityContextHolder はクリアされます

    • HttpServletRequest は RequestCache(Javadoc) に保存されます。ユーザーが正常に認証されると、RequestCache を使用して元のリクエストが再生されます。

    • AuthenticationEntryPoint は、クライアントから資格情報をリクエストするために使用されます。例:ログインページにリダイレクトするか、WWW-Authenticate ヘッダーを送信する場合があります。

  •  番号 3 それ以外の場合、AccessDeniedException の場合、アクセスは拒否されます。AccessDeniedHandler は、拒否されたアクセスを処理するために呼び出されます。

アプリケーションが AccessDeniedException または AuthenticationException をスローしない場合、ExceptionTranslationFilter は何もしません。

ExceptionTranslationFilter の擬似コードは次のようになります。

ExceptionTranslationFilter 擬似コード
try {
    filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException e) {
    if (!authenticated || e instanceof AuthenticationException) {
        startAuthentication(); (2)
    } else {
        accessDenied(); (3)
    }
}
1Filter のレビューから、FilterChain.doFilter(request, response) を呼び出すことは、アプリケーションの残りを呼び出すことと同等であることを思い出してください。これは、アプリケーションの別の部分(つまり、FilterSecurityInterceptor またはメソッドセキュリティ)が AuthenticationException または AccessDeniedException をスローすると、ここでキャッチされて処理されることを意味します。
2 ユーザーが認証されていないか、AuthenticationException である場合、認証開始します。
3 それ以外の場合、アクセスは拒否されました

10. 認証

Spring Security は、認証の包括的なサポートを提供します。このセクションでは以下について説明します。

アーキテクチャコンポーネント

このセクションでは、サーブレット認証で使用される Spring Security の主要なアーキテクチャコンポーネントについて説明します。これらの部品がどのように組み合わされるかを説明する具体的なフローが必要な場合は、認証メカニズム固有のセクションを参照してください。

  • SecurityContextHolderSecurityContextHolder は、Spring Security が認証された人の詳細を格納する場所です。

  • SecurityContextSecurityContextHolder から取得され、現在認証されているユーザーの Authentication が含まれています。

  • 認証 AuthenticationManager への入力として、ユーザーが認証のために提供した資格情報、または SecurityContext からの現在のユーザーを提供できます。

  • GrantedAuthorityAuthentication のプリンシパルに付与される権限 (すなわち、ロール、スコープなど。)

  • AuthenticationManager - Spring Security のフィルターが認証を実行する方法を定義する API。

  • ProviderManagerAuthenticationManager の最も一般的な実装。

  • AuthenticationProviderProviderManager が特定のタイプの認証を実行するために使用します。

  • AuthenticationEntryPoint を使用した資格情報のリクエスト - クライアントから資格情報をリクエストするために使用 (つまり、ログインページへのリダイレクト、WWW-Authenticate レスポンスの送信など。)

  • AbstractAuthenticationProcessingFilter - 認証に使用されるベース Filter。また、これにより、認証の高レベルのフローと各部分がどのように連携するかについての良いアイデアが得られます。

認証メカニズム

10.1. SecurityContextHolder

Spring Security の認証モデルの中核は SecurityContextHolder です。SecurityContext が含まれています。

securitycontextholder

SecurityContextHolder は、Spring Security が認証された人の詳細を格納する場所です。Spring Security は、SecurityContextHolder の実装方法を気にしません。値が含まれている場合、現在認証されているユーザーとして使用されます。

ユーザーが認証されていることを示す最も簡単な方法は、SecurityContextHolder を直接設定することです。

例 49: SecurityContextHolder の設定
SecurityContext context = SecurityContextHolder.createEmptyContext(); (1)
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); (2)
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); (3)
1 空の SecurityContext を作成することから始めます。SecurityContextHolder.getContext().setAuthentication(authentication) を使用する代わりに新しい SecurityContext インスタンスを作成して、複数のスレッド間で競合状態を回避することが重要です。
2 次に、新しい Authentication オブジェクトを作成します。Spring Security は、どのタイプの Authentication 実装が SecurityContext に設定されているかを気にしません。ここでは、非常に単純であるため、TestingAuthenticationToken を使用しています。より一般的な本番シナリオは UsernamePasswordAuthenticationToken(userDetails, password, authorities) です。
3 最後に、SecurityContext を SecurityContextHolder に設定します。Spring Security はこの情報を認可に使用します。

認証されたプリンシパルに関する情報を取得する場合は、SecurityContextHolder にアクセスして取得できます。

例 50: 現在認証されているユーザーにアクセスする
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

デフォルトでは、SecurityContextHolder は ThreadLocal を使用してこれらの詳細を保存します。つまり、SecurityContext がそれらのメソッドの引数として明示的に渡されない場合でも、SecurityContext は常に同じ実行スレッドのメソッドで使用できます。この方法で ThreadLocal を使用することは、現在のプリンシパルのリクエストが処理された後にスレッドをクリアするように注意を払えば、非常に安全です。Spring Security の FilterChainProxy は、SecurityContext が常にクリアされるようにします。

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

10.2. SecurityContext

SecurityContext(Javadoc) SecurityContextHolder から取得されます。SecurityContext には認証オブジェクトが含まれています。

10.3. 認証

Authentication(Javadoc) は、Spring Security 内で 2 つの主な目的を果たします。

  • ユーザーが認証のために提供した資格情報を提供する AuthenticationManager への入力。このシナリオで使用すると、isAuthenticated() は false を返します。

  • 現在認証されているユーザーを表します。現在の Authentication は SecurityContext から取得できます。

Authentication には以下が含まれます。

  • principal - ユーザーを識別します。ユーザー名 / パスワードで認証する場合、これは多くの場合 UserDetails のインスタンスです。

  • credentials - 多くの場合、パスワード。多くの場合、これはユーザーが認証された後にクリアされ、リークされないようにします。

  • authorities - GrantedAuthority は、ユーザーに付与される高レベルのアクセス許可です。いくつかの例は、ロールまたはスコープです。

10.4. GrantedAuthority

GrantedAuthoritys(Javadoc) は、ユーザーに付与される高レベルのアクセス許可です。いくつかの例は、ロールまたはスコープです。

GrantedAuthority は、Authentication.getAuthorities() メソッドから取得できます。このメソッドは、GrantedAuthority オブジェクトの Collection を提供します。GrantedAuthority は、当然のことながら、プリンシパルに付与される権限です。このような権限は通常、ROLE_ADMINISTRATOR や ROLE_HR_SUPERVISOR などの「ロール」です。これらのロールは、後で Web 認可、メソッド認可、およびドメインオブジェクト認可用に構成されます。Spring Security の他の部分は、これらの権限を解釈でき、それらが存在することを期待しています。ユーザー名 / パスワードベースの認証を使用する場合、GrantedAuthority は通常 UserDetailsService によってロードされます。

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

10.5. AuthenticationManager

AuthenticationManager(Javadoc) は、Spring Security のフィルターが認証を実行する方法を定義する API です。返される Authentication は、AuthenticationManager を呼び出したコントローラー(つまり Spring Security の Filters)によって SecurityContextHolder に設定されます。Spring Security の Filters と統合しない場合は、SecurityContextHolder を直接設定でき、AuthenticationManager を使用する必要はありません。

AuthenticationManager の実装は何でもかまいませんが、最も一般的な実装は ProviderManager です。

10.6. ProviderManager

ProviderManager(Javadoc) は、AuthenticationManager の最も一般的に使用される実装です。ProviderManager は、AuthenticationProvider の List に委譲します。各 AuthenticationProvider には、認証が成功、失敗、または決定を下すことができず、ダウンストリーム AuthenticationProvider が決定できることを示す機会があります。構成された AuthenticationProvider のいずれも認証できない場合、ProviderManager が渡された Authentication のタイプをサポートするように構成されていないことを示す特別な AuthenticationException である ProviderNotFoundException で認証が失敗します。

providermanager

実際には、各 AuthenticationProvider は特定のタイプの認証を実行する方法を知っています。例:ある AuthenticationProvider はユーザー名 / パスワードを検証でき、別の AuthenticationProvider は SAML アサーションを認証できる可能性があります。これにより、各 AuthenticationProvider は非常に特殊なタイプの認証を行うことができますが、複数のタイプの認証をサポートし、単一の AuthenticationManager Bean のみを公開します。

ProviderManager では、AuthenticationProvider が認証を実行できない場合に参照されるオプションの親 AuthenticationManager を構成することもできます。親はどのタイプの AuthenticationManager でもかまいませんが、しばしば ProviderManager のインスタンスです。

providermanager parent

実際、複数の ProviderManager インスタンスが同じ親 AuthenticationManager を共有する場合があります。これは、認証が共通する複数の SecurityFilterChain インスタンス(共有親 AuthenticationManager)が存在するシナリオでは多少一般的ですが、異なる認証メカニズム(異なる ProviderManager インスタンス)もあります。

providermanagers parent

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

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

10.7. AuthenticationProvider

複数の AuthenticationProviders(Javadoc) ProviderManager に注入できます。各 AuthenticationProvider は特定のタイプの認証を実行します。例:DaoAuthenticationProvider はユーザー名 / パスワードベースの認証をサポートし、JwtAuthenticationProvider は JWT トークンの認証をサポートします。

10.8. AuthenticationEntryPoint を使用した資格情報のリクエスト

AuthenticationEntryPoint(Javadoc) は、クライアントから資格情報をリクエストする HTTP レスポンスを送信するために使用されます。

クライアントは、リソースをリクエストするためにユーザー名 / パスワードなどの資格情報を積極的に含めることがあります。これらの場合、Spring Security は既に含まれているため、クライアントから資格情報をリクエストする HTTP レスポンスを提供する必要はありません。

その他の場合、クライアントは、アクセスが認可されていないリソースに対して認証されていないリクエストを行います。この場合、AuthenticationEntryPoint の実装を使用して、クライアントから資格情報をリクエストします。AuthenticationEntryPoint 実装は、ログインページへのリダイレクトを実行し、WWW 認証ヘッダーなどで応答します。

10.9. AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter(Javadoc) は、ユーザーの資格情報を認証するためのベース Filter として使用されます。資格情報を認証する前に、Spring Security は通常 AuthenticationEntryPoint を使用して資格情報をリクエストします。

次に、AbstractAuthenticationProcessingFilter は送信された認証リクエストを認証できます。

abstractauthenticationprocessingfilter

 番号 1 ユーザーが資格情報を送信すると、AbstractAuthenticationProcessingFilter は HttpServletRequest から Authentication を作成して認証します。作成される Authentication のタイプは、AbstractAuthenticationProcessingFilter のサブクラスに依存します。例:UsernamePasswordAuthenticationFilter は、HttpServletRequest で送信されたユーザー名パスワードから UsernamePasswordAuthenticationToken を作成します

2 番 次に、AuthenticationAuthenticationManager に渡されて認証されます。

 番号 3 認証が失敗した場合、失敗

  • SecurityContextHolder はクリアされます。

  • RememberMeServices.loginFail が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • AuthenticationFailureHandler が呼び出されます。

 番号 4 認証が成功した場合、成功

  • SessionAuthenticationStrategy に新しいログインが通知されます。

  • 認証SecurityContextHolder に設定されます。その後、SecurityContextPersistenceFilter は SecurityContext を HttpSession に保存します。

  • RememberMeServices.loginSuccess が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • ApplicationEventPublisher は InteractiveAuthenticationSuccessEvent を公開します。

10.10. ユーザー名 / パスワード認証

ユーザーを認証する最も一般的な方法の 1 つは、ユーザー名とパスワードを検証することです。そのため、Spring Security は、ユーザー名とパスワードによる認証の包括的なサポートを提供します。

ユーザー名とパスワードの読み取り

Spring Security は、HttpServletRequest からユーザー名とパスワードを読み取るための次の組み込みメカニズムを提供します。

ストレージメカニズム

ユーザー名とパスワードを読み取るためにサポートされている各メカニズムは、サポートされているストレージメカニズムのいずれかを活用できます。

10.10.1. フォームログイン

Spring Security は、html フォームで提供されるユーザー名とパスワードのサポートを提供します。このセクションでは、Spring Security 内でのフォームベース認証の動作について詳しく説明します。

フォームベースのログインが Spring Security 内でどのように機能するかを見てみましょう。まず、ユーザーがログインフォームにリダイレクトされる方法を確認します。

loginurlauthenticationentrypoint
図 6: ログインページへのリダイレクト

この図は、SecurityFilterChain ダイアグラムから構築されています。

 番号 1 最初に、ユーザーは、認可されていないリソース /private に対して非認証リクエストを行います。

2 番 Spring Security の FilterSecurityInterceptor は、認証されていないリクエストが AccessDeniedException をスローすることにより拒否されることを示します。

 番号 3 ユーザーは認証されないため、ExceptionTranslationFilter は認証の開始を開始し、構成された AuthenticationEntryPoint を使用してログインページにリダイレクトを送信します。ほとんどの場合、AuthenticationEntryPoint は LoginUrlAuthenticationEntryPoint(Javadoc) のインスタンスです。

 番号 4 ブラウザーは、リダイレクト先のログインページをリクエストします。

5 番 アプリケーション内の何かは、ログインページをレンダリングする必要があります

ユーザー名とパスワードが送信されると、UsernamePasswordAuthenticationFilter はユーザー名とパスワードを認証します。UsernamePasswordAuthenticationFilter は AbstractAuthenticationProcessingFilter を継承しているため、この図はかなり似ているはずです。

usernamepasswordauthenticationfilter
図 7: ユーザー名とパスワードの認証

この図は、SecurityFilterChain ダイアグラムから構築されています。

 番号 1 ユーザーがユーザー名とパスワードを送信すると、UsernamePasswordAuthenticationFilter は HttpServletRequest からユーザー名とパスワードを抽出することにより、Authentication の一種である UsernamePasswordAuthenticationToken を作成します。

2 番 次に、UsernamePasswordAuthenticationToken は AuthenticationManager に渡されて認証されます。AuthenticationManager がどのように見えるかの詳細は、ユーザー情報がどのように保存されるかに依存します。

 番号 3 認証が失敗した場合、失敗

  • SecurityContextHolder はクリアされます。

  • RememberMeServices.loginFail が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • AuthenticationFailureHandler が呼び出されます。

 番号 4 認証が成功した場合、成功

  • SessionAuthenticationStrategy に新しいログインが通知されます。

  • 認証SecurityContextHolder に設定されます。

  • RememberMeServices.loginSuccess が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • ApplicationEventPublisher は InteractiveAuthenticationSuccessEvent を公開します。

  • AuthenticationSuccessHandler が呼び出されます。通常、これはログインページにリダイレクトするときに ExceptionTranslationFilter によって保存されたリクエストにリダイレクトする SimpleUrlAuthenticationSuccessHandler です。

Spring Security フォームログインはデフォルトで有効になっています。ただし、サーブレットベースの構成が提供されるとすぐに、フォームベースのログインを明示的に提供する必要があります。最小限の明示的な Java 構成を以下に示します。

例 51: フォームログイン
Java
protected void configure(HttpSecurity http) {
    http
        // ...
        .formLogin(withDefaults());
}
XML
<http>
    <!-- ... -->
    <form-login />
</http>
Kotlin
fun configure(http: HttpSecurity) {
    http {
        // ...
        formLogin { }
    }
}

この構成では、Spring Security はデフォルトのログインページをレンダリングします。ほとんどの本番アプリケーションでは、カスタムログインフォームが必要です。

以下の構成は、カスタムログインフォームを提供する方法を示しています。

例 52: カスタムログインフォーム設定
Java
protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        );
}
XML
<http>
    <!-- ... -->
    <intercept-url pattern="/login" access="permitAll" />
    <form-login login-page="/login" />
</http>
Kotlin
fun configure(http: HttpSecurity) {
    http {
        // ...
        formLogin {
            loginPage = "/login"
            permitAll()
        }
    }
}

ログインページが Spring Security 構成で指定されている場合は、ページをレンダリングする責任があります。以下は、/login のログインページに準拠する HTML ログインフォームを生成する Thymeleaf (英語) テンプレートです。

例 53: ログインフォーム
src/main/resources/templates/login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
    <head>
        <title>Please Log In</title>
    </head>
    <body>
        <h1>Please Log In</h1>
        <div th:if="${param.error}">
            Invalid username and password.</div>
        <div th:if="${param.logout}">
            You have been logged out.</div>
        <form th:action="@{/login}" method="post">
            <div>
            <input type="text" name="username" placeholder="Username"/>
            </div>
            <div>
            <input type="password" name="password" placeholder="Password"/>
            </div>
            <input type="submit" value="Log in" />
        </form>
    </body>
</html>

デフォルトの HTML フォームにはいくつかの重要なポイントがあります。

  • フォームは post から /login を実行する必要があります

  • フォームには、Thymeleaf によって自動的に含まれる CSRF トークンを含める必要があります。

  • フォームは、username という名前のパラメーターでユーザー名を指定する必要があります

  • フォームは、password という名前のパラメーターでパスワードを指定する必要があります

  • HTTP パラメーターエラーが見つかった場合、ユーザーが有効なユーザー名 / パスワードの入力に失敗したことを示します

  • HTTP パラメーターログアウトが見つかった場合、ユーザーが正常にログアウトしたことを示します

多くのユーザーは、ログインページをカスタマイズするだけで十分です。ただし、必要に応じて、上記のすべてを追加の構成でカスタマイズできます。

Spring MVC を使用している場合は、作成したログインテンプレートに GET /login をマッピングするコントローラーが必要です。最小限のサンプル LoginController を以下に示します。

例 54: LoginController
src/main/java/example/LoginController.java
@Controller
class LoginController {
    @GetMapping("/login")
    String login() {
        return "login";
    }
}

10.10.2. 基本認証

このセクションでは、Spring Security がサーブレットベースのアプリケーションで基本 HTTP 認証 (英語) をどのようにサポートするかについて詳しく説明します。

Spring Security 内で HTTP 基本認証がどのように機能するかを見てみましょう。最初に、WWW 認証 (英語) ヘッダーが認証されていないクライアントに送り返されるのを確認します。

basicauthenticationentrypoint
図 8: WWW-Authenticate ヘッダーの送信

この図は、SecurityFilterChain ダイアグラムから構築されています。

 番号 1 最初に、ユーザーは、認可されていないリソース /private に対して非認証リクエストを行います。

2 番 Spring Security の FilterSecurityInterceptor は、認証されていないリクエストが AccessDeniedException をスローすることにより拒否されることを示します。

 番号 3 ユーザーは認証されていないため、ExceptionTranslationFilter認証の開始を開始します。構成された AuthenticationEntryPoint は、WWW-Authenticate ヘッダーを送信する BasicAuthenticationEntryPoint(Javadoc) のインスタンスです。RequestCache は通常、クライアントが最初にリクエストしたリクエストを再生できるため、リクエストを保存しない NullRequestCache です。

クライアントは、WWW-Authenticate ヘッダーを受信すると、ユーザー名とパスワードで再試行する必要があることを認識します。以下は、処理中のユーザー名とパスワードのフローです。

basicauthenticationfilter
図 9: ユーザー名とパスワードの認証

この図は、SecurityFilterChain ダイアグラムから構築されています。

 番号 1 ユーザーがユーザー名とパスワードを送信すると、BasicAuthenticationFilter は HttpServletRequest からユーザー名とパスワードを抽出することにより、Authentication の一種である UsernamePasswordAuthenticationToken を作成します。

2 番 次に、UsernamePasswordAuthenticationToken は AuthenticationManager に渡されて認証されます。AuthenticationManager がどのように見えるかの詳細は、ユーザー情報がどのように保存されるかに依存します。

 番号 3 認証が失敗した場合、失敗

  • SecurityContextHolder はクリアされます。

  • RememberMeServices.loginFail が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • AuthenticationEntryPoint が呼び出され、WWW-Authenticate が再度送信されます。

 番号 4 認証が成功した場合、成功

  • 認証SecurityContextHolder に設定されます。

  • RememberMeServices.loginSuccess が呼び出されます。設定されていないことを覚えていれば、これはノーオペレーションです。

  • BasicAuthenticationFilter は FilterChain.doFilter(request,response) を呼び出して、残りのアプリケーションロジックを続行します。

Spring Security の HTTP 基本認証のサポートはデフォルトで有効になっています。ただし、サーブレットベースの構成が提供されるとすぐに、HTTP Basic を明示的に提供する必要があります。

最小限の明示的な構成を以下に示します。

例 55: 明示的な HTTP 基本設定
Java
protected void configure(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
}
XML
<http>
    <!-- ... -->
    <http-basic />
</http>
Kotlin
fun configure(http: HttpSecurity) {
    http {
        // ...
        httpBasic { }
    }
}

10.10.3. ダイジェスト認証

このセクションでは、Spring Security が DigestAuthenticationFilter を提供するダイジェスト認証 (英語) のサポートを提供する方法について詳しく説明します。

最新のアプリケーションではダイジェスト認証を使用しないでください。安全とは見なされません。最も明らかな問題は、パスワードをプレーンテキスト、暗号化、または MD5 形式で保存する必要があることです。これらのストレージ形式はすべて安全ではないと見なされます。代わりに、ダイジェスト認証でサポートされていない一方向の適応パスワードハッシュ(つまり、bCrypt、PBKDF2、SCrypt など)を使用して資格情報を保存する必要があります。

ダイジェスト認証は基本認証の多くの弱点の解決を試みます。具体的には、資格情報がクリアテキストで送信されないようにします。多くのブラウザがダイジェスト認証をサポートしています (英語)

HTTP ダイジェスト認証を管理する標準は、RFC 2617 (英語) によって定義されます。RFC 2617 (英語) は、RFC 2069 (英語) によって規定されたダイジェスト認証標準の以前のバージョンを更新します。ほとんどのユーザーエージェントは RFC 2617 を実装します。Spring Security のダイジェスト認証サポートは、RFC 2617 で規定されている「auth」保護品質(qop)と互換性があり、RFC 2069 との下位互換性も提供します。暗号化されていない HTTP(つまり TLS/HTTPS を使用しない)を使用し、認証プロセスのセキュリティを最大限に高めるため。ただし、誰もが HTTPS を使用する必要があります。

ダイジェスト認証の中心は「ノンス」です。これは、サーバーが生成する値です。Spring Security のナンスは次の形式を採用しています。

例 56: ダイジェスト構文
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

NoOpPasswordEncoder` を使用して、安全でないプレーンテキストパスワード保存設定する必要があります。以下に、Java 構成でダイジェスト認証を構成する例を示します。

例 57: ダイジェスト認証
Java
@Autowired
UserDetailsService userDetailsService;

DigestAuthenticationEntryPoint entryPoint() {
    DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
    result.setRealmName("My App Relam");
    result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}

DigestAuthenticationFilter digestAuthenticationFilter() {
    DigestAuthenticationFilter result = new DigestAuthenticationFilter();
    result.setUserDetailsService(userDetailsService);
    result.setAuthenticationEntryPoint(entryPoint());
}

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
        .addFilterBefore(digestFilter());
}
XML
<b:bean id="digestFilter"
        class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter"
    p:userDetailsService-ref="jdbcDaoImpl"
    p:authenticationEntryPoint-ref="digestEntryPoint"
/>

<b:bean id="digestEntryPoint"
        class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint"
    p:realmName="My App Realm"
    p:key="3028472b-da34-4501-bfd8-a355c42bdf92"
/>

<http>
    <!-- ... -->
    <custom-filter ref="userFilter" position="DIGEST_AUTH_FILTER"/>
</http>

10.10.4. インメモリ認証

Spring Security の InMemoryUserDetailsManager は、UserDetailsService を実装して、メモリで取得されるユーザー名 / パスワードベースの認証をサポートします。InMemoryUserDetailsManager は、UserDetailsManager インターフェースを実装することにより、UserDetails の管理を提供します。UserDetails ベースの認証は、Spring Security が認証のためにユーザー名 / パスワードを受け入れますに構成されている場合に使用されます。

このサンプルでは、Spring Boot CLI を使用して password のパスワードをエンコードし、エンコードされた {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW のパスワードを取得します。

例 58: InMemoryUserDetailsManager Java 構成
Java
@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}
XML
<user-service>
    <user name="user"
        password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
        authorities="ROLE_USER" />
    <user name="admin"
        password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
        authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>
Kotlin
@Bean
fun users(): UserDetailsService {
    val user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build()
    val admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build()
    return InMemoryUserDetailsManager(user, admin)
}

上記のサンプルでは、パスワードは安全な形式で保存されていますが、使い始めの点からは多くのことが望まれています。

以下のサンプルでは、User.withDefaultPasswordEncoder を利用して、メモリに保存されているパスワードが保護されていることを確認します。ただし、ソースコードを逆コンパイルしてパスワードを取得することはできません。このため、User.withDefaultPasswordEncoder は「開始」にのみ使用する必要があり、本番用ではありません。

例 59: User.withDefaultPasswordEncoder を使用した InMemoryUserDetailsManager
Java
@Bean
public UserDetailsService users() {
    // The builder will ensure the passwords are encoded before saving in memory
    UserBuilder users = User.withDefaultPasswordEncoder();
    UserDetails user = users
        .username("user")
        .password("password")
        .roles("USER")
        .build();
    UserDetails admin = users
        .username("admin")
        .password("password")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}
Kotlin
@Bean
fun users(): UserDetailsService {
    // The builder will ensure the passwords are encoded before saving in memory
    val users = User.withDefaultPasswordEncoder()
    val user = users
        .username("user")
        .password("password")
        .roles("USER")
        .build()
    val admin = users
        .username("admin")
        .password("password")
        .roles("USER", "ADMIN")
        .build()
    return InMemoryUserDetailsManager(user, admin)
}

XML ベースの構成で User.withDefaultPasswordEncoder を使用する簡単な方法はありません。デモまたは開始する場合は、{noop} をパスワードの前に付けて、エンコードを使用しないことを示すことができます。

例 60: <user-service> {noop} XML 設定
<user-service>
    <user name="user"
        password="{noop}password"
        authorities="ROLE_USER" />
    <user name="admin"
        password="{noop}password"
        authorities="ROLE_USER,ROLE_ADMIN" />
</user-service>

10.10.5. JDBC 認証

Spring Security の JdbcDaoImpl は UserDetailsService を実装し、JDBC を使用して取得されるユーザー名 / パスワードベースの認証をサポートします。JdbcUserDetailsManager は JdbcDaoImpl を継承して、UserDetailsManager インターフェースを介した UserDetails の管理を提供します。UserDetails ベースの認証は、Spring Security が認証のためにユーザー名 / パスワードを受け入れますに構成されている場合に使用されます。

次のセクションで説明します。

デフォルトスキーマ

Spring Security は、JDBC ベースの認証にデフォルトのクエリを提供します。このセクションでは、デフォルトクエリで使用される対応するデフォルトスキーマを提供します。使用しているクエリとデータベースのダイアレクトに対するカスタマイズに一致するようにスキーマを調整する必要があります。

ユーザースキーマ

JdbcDaoImpl では、ユーザーのパスワード、アカウントステータス(有効または無効)、および権限のリスト(ロール)を読み込むためのテーブルが必要です。必要なデフォルトのスキーマは以下にあります。

デフォルトのスキーマは、org/springframework/security/core/userdetails/jdbc/users.ddl という名前のクラスパスリソースとしても公開されます。

例 61: デフォルトのユーザースキーマ
create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled boolean not null
);

create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

Oracle は一般的なデータベースの選択肢ですが、わずかに異なるスキーマが必要です。以下のユーザー用のデフォルト Oracle スキーマを見つけることができます。

例 62: Oracle データベースのデフォルトユーザースキーマ
CREATE TABLE USERS (
    USERNAME NVARCHAR2(128) PRIMARY KEY,
    PASSWORD NVARCHAR2(128) NOT NULL,
    ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);


CREATE TABLE AUTHORITIES (
    USERNAME NVARCHAR2(128) NOT NULL,
    AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;
スキーマのグループ化

アプリケーションがグループを活用している場合は、グループスキーマを提供する必要があります。グループのデフォルトのスキーマは以下にあります。

例 63: デフォルトのグループスキーマ
create table groups (
    id bigint generated by default as identity(start with 0) primary key,
    group_name varchar_ignorecase(50) not null
);

create table group_authorities (
    group_id bigint not null,
    authority varchar(50) not null,
    constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
    id bigint generated by default as identity(start with 0) primary key,
    username varchar(50) not null,
    group_id bigint not null,
    constraint fk_group_members_group foreign key(group_id) references groups(id)
);
DataSource のセットアップ

JdbcUserDetailsManager を構成する前に、DataSource を作成する必要があります。この例では、デフォルトのユーザースキーマで初期化される組み込み DataSource をセットアップします。

例 64: 埋め込みデータソース
Java
@Bean
DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(H2)
        .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
        .build();
}
XML
<jdbc:embedded-database>
    <jdbc:script location="classpath:org/springframework/security/core/userdetails/jdbc/users.ddl"/>
</jdbc:embedded-database>
Kotlin
@Bean
fun dataSource(): DataSource {
    return EmbeddedDatabaseBuilder()
        .setType(H2)
        .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
        .build()
}

本番環境では、外部データベースへの接続を確実にセットアップする必要があります。

JdbcUserDetailsManager Bean

このサンプルでは、Spring Boot CLI を使用して password のパスワードをエンコードし、エンコードされた {bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW のパスワードを取得します。パスワードの保存方法の詳細については、PasswordEncoder セクションを参照してください。

例 65: JdbcUserDetailsManager
Java
@Bean
UserDetailsManager users(DataSource dataSource) {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
    users.createUser(user);
    users.createUser(admin);
}
XML
<jdbc-user-service>
    <user name="user"
        password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
        authorities="ROLE_USER" />
    <user name="admin"
        password="{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW"
        authorities="ROLE_USER,ROLE_ADMIN" />
</jdbc-user-service>
Kotlin
@Bean
fun users(dataSource: DataSource): UserDetailsManager {
    val user = User.builder()
            .username("user")
            .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
            .roles("USER")
            .build();
    val admin = User.builder()
            .username("admin")
            .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
            .roles("USER", "ADMIN")
            .build();
    val users = JdbcUserDetailsManager(dataSource)
    users.createUser(user)
    users.createUser(admin)
    return users
}

10.10.6. UserDetails

UserDetails(Javadoc) UserDetailsService によって返されます。DaoAuthenticationProvider は UserDetails を検証してから、構成済みの UserDetailsService によって返された UserDetails であるプリンシパルを持つ Authentication を返します。

10.10.7. UserDetailsService

UserDetailsService(Javadoc) は、ユーザー名、パスワード、およびユーザー名とパスワードで認証するための他の属性を取得するために、DaoAuthenticationProvider によって使用されます。Spring Security は、UserDetailsServiceメモリ内および JDBC 実装を提供します。

カスタム UserDetailsService を Bean として公開することにより、カスタム認証を定義できます。例:以下は、CustomUserDetailsService が UserDetailsService を実装すると仮定して認証をカスタマイズします。

これは、AuthenticationManagerBuilder が設定されておらず、AuthenticationProviderBean が定義されていない場合にのみ使用されます。
例 66: カスタム UserDetailsService Bean
Java
@Bean
CustomUserDetailsService customUserDetailsService() {
    return new CustomUserDetailsService();
}
XML
<b:bean class="example.CustomUserDetailsService"/>
Kotlin
@Bean
fun customUserDetailsService() = CustomUserDetailsService()

10.10.8. PasswordEncoder

Spring Security のサーブレットは、PasswordEncoder と統合することにより、パスワードの安全な保管をサポートしています。Spring Security で使用される PasswordEncoder 実装をカスタマイズするには、PasswordEncoder Bean を公開します。

10.10.9. DaoAuthenticationProvider

DaoAuthenticationProvider(Javadoc) は、UserDetailsService および PasswordEncoder を利用してユーザー名とパスワードを認証する AuthenticationProvider 実装です。

DaoAuthenticationProvider が Spring Security 内でどのように機能するかを見てみましょう。この図は、ユーザー名とパスワードの読み取りの図で AuthenticationManager がどのように機能するかの詳細を説明しています。

daoauthenticationprovider
図 10: DaoAuthenticationProvider の使用箇所

 番号 1 ユーザー名とパスワードの読み取りからの認証 Filter は、ProviderManager によって実装される UsernamePasswordAuthenticationToken を AuthenticationManager に渡します。

2 番 ProviderManager は、タイプ DaoAuthenticationProviderAuthenticationProvider を使用するように構成されています。

 番号 3DaoAuthenticationProvider は UserDetailsService から UserDetails を検索します。

 番号 4 次に、DaoAuthenticationProvider は PasswordEncoder を使用して、前のステップで返された UserDetails のパスワードを検証します。

5 番 認証が成功すると、返される Authentication はタイプ UsernamePasswordAuthenticationToken であり、構成済みの UserDetailsService によって返される UserDetails であるプリンシパルを持ちます。最終的に、返された UsernamePasswordAuthenticationToken は、認証 Filter によって SecurityContextHolder に設定されます。

10.10.10. LDAP 認証

LDAP は、多くの場合、組織がユーザー情報の中央リポジトリとして、および認証サービスとして使用します。また、アプリケーションユーザーのロール情報を保存するためにも使用できます。

Spring Security の LDAP ベースの認証は、Spring Security が認証のためにユーザー名 / パスワードを受け入れますに構成されている場合に使用されます。ただし、認証にユーザー名 / パスワードを利用しても、バインド認証では LDAP サーバーがパスワードを返さないため、アプリケーションはパスワードの検証を実行できないため、UserDetailsService を使用して統合しません。

Spring Security の LDAP プロバイダーを完全に構成できるように、LDAP サーバーの構成方法にはさまざまなシナリオがあります。認証とロールの取得に別々の戦略インターフェースを使用し、さまざまな状況を処理するように構成できるデフォルトの実装を提供します。

前提条件

Spring Security で使用する前に、LDAP に精通している必要があります。次のリンクは、関連する概念の優れた導入と、フリーの LDAP サーバー OpenLDAP を使用してディレクトリを設定するためのガイドです:https://www.zytrax.com/books/ldap/ (英語) 。Java から LDAP にアクセスするために使用される JNDI API に精通していることも役立つ場合があります。LDAP プロバイダーではサードパーティの LDAP ライブラリ(Mozilla、JLDAP など)は使用していませんが、Spring LDAP が広く使用されているため、独自のカスタマイズを追加する予定がある場合は、そのプロジェクトにある程度精通していると便利です。

LDAP 認証を使用する場合、LDAP 接続プールを適切に構成することが重要です。これを行う方法を知っている未知の場合は、Java LDAP ドキュメント (英語) を参照できます。

組み込み LDAP サーバーのセットアップ

最初に行う必要があるのは、構成を指す LDAP サーバーがあることを確認することです。簡単にするために、多くの場合、組み込み LDAP サーバーを使用することをお勧めします。Spring Security は、次のいずれかの使用をサポートしています。

以下のサンプルでは、password のパスワードを持つユーザー user と admin で組み込み LDAP サーバーを初期化するために、クラスパスリソースとして users.ldif として以下を公開します。

users.ldif
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
組み込み UnboundID サーバー

UnboundID (英語) を使用する場合は、次の依存関係を指定します。

例 67: UnboundID の依存関係
Maven
<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>4.0.14</version>
    <scope>runtime</scope>
</dependency>
Gradle
depenendencies {
    runtimeOnly "com.unboundid:unboundid-ldapsdk:4.0.14"
}

その後、組み込み LDAP サーバーを構成できます。

例 68: 組み込み LDAP サーバーの構成
Java
@Bean
UnboundIdContainer ldapContainer() {
    return new UnboundIdContainer("dc=springframework,dc=org",
                "classpath:users.ldif");
}
XML
<b:bean class="org.springframework.security.ldap.server.UnboundIdContainer"
    c:defaultPartitionSuffix="dc=springframework,dc=org"
    c:ldif="classpath:users.ldif"/>
Kotlin
@Bean
fun ldapContainer(): UnboundIdContainer {
    return UnboundIdContainer("dc=springframework,dc=org","classpath:users.ldif")
}
組み込み ApacheDS サーバー

Spring Security は ApacheDS 1.x を使用しますが、これは保守されなくなりました。残念ながら、ApacheDS 2.x はマイルストーンバージョンのみをリリースしており、安定版リリースはありません。ApacheDS 2.x の安定したリリースが利用可能になったら、更新を検討します。

Apache DS (英語) を使用する場合は、次の依存関係を指定します。

例 69: ApacheDS の依存関係
Maven
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-core</artifactId>
    <version>1.5.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.apache.directory.server</groupId>
    <artifactId>apacheds-server-jndi</artifactId>
    <version>1.5.5</version>
    <scope>runtime</scope>
</dependency>
Gradle
depenendencies {
    runtimeOnly "org.apache.directory.server:apacheds-core:1.5.5"
    runtimeOnly "org.apache.directory.server:apacheds-server-jndi:1.5.5"
}

その後、組み込み LDAP サーバーを構成できます。

例 70: 組み込み LDAP サーバーの構成
Java
@Bean
ApacheDSContainer ldapContainer() {
    return new ApacheDSContainer("dc=springframework,dc=org",
                "classpath:users.ldif");
}
XML
<b:bean class="org.springframework.security.ldap.server.ApacheDSContainer"
    c:defaultPartitionSuffix="dc=springframework,dc=org"
    c:ldif="classpath:users.ldif"/>
Kotlin
@Bean
fun ldapContainer(): ApacheDSContainer {
    return ApacheDSContainer("dc=springframework,dc=org", "classpath:users.ldif")
}
LDAP ContextSource

構成を指す LDAP サーバーを作成したら、Spring Security を構成して、ユーザーの認証に使用する LDAP サーバーを指すようにする必要があります。これは、JDBC DataSource に相当する LDAP ContextSource を作成することにより行われます。

例 71: LDAP コンテキストソース
Java
ContextSource contextSource(UnboundIdContainer container) {
    return new DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org");
}
XML
<ldap-server
    url="ldap://localhost:53389/dc=springframework,dc=org" />
Kotlin
fun contextSource(container: UnboundIdContainer): ContextSource {
    return DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org")
}
認証

Spring Security の LDAP サポートは UserDetailsService を使用しません。これは、LDAP バインド認証では、クライアントがパスワードまたはパスワードのハッシュバージョンを読み取ることができないためです。これは、Spring Security がパスワードを読み取って認証する方法がないことを意味します。

このため、LDAP サポートは LdapAuthenticator インターフェースを使用して実装されます。LdapAuthenticator は、必要なユーザー属性を取得するロールも果たします。これは、属性のアクセス許可が使用されている認証の種類に依存する可能性があるためです。例:ユーザーとしてバインドする場合は、ユーザー自身の権限で読み取る必要がある場合があります。

Spring Security には 2 つの LdapAuthenticator 実装が提供されています。

バインド認証の使用

バインド認証 (英語) は、LDAP でユーザーを認証するための最も一般的なメカニズムです。バインド認証では、ユーザーの資格情報(つまり、ユーザー名 / パスワード)が LDAP サーバーに送信され、そこで認証されます。バインド認証を使用する利点は、ユーザーのシークレット(つまり、パスワード)をクライアントに公開する必要がないことです。これにより、ユーザーの漏洩を防ぐことができます。

バインド認証構成の例を以下に示します。

例 72: バインド認証
Java
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
    BindAuthenticator authenticator = new BindAuthenticator(contextSource);
    authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
    return authenticator;
}

@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
    return new LdapAuthenticationProvider(authenticator);
}
XML
<ldap-authentication-provider
    user-dn-pattern="uid={0},ou=people"/>
Kotlin
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator {
    val authenticator = BindAuthenticator(contextSource)
    authenticator.setUserDnPatterns(arrayOf("uid={0},ou=people"))
    return authenticator
}

@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
    return LdapAuthenticationProvider(authenticator)
}

この単純な例では、指定されたパターンでユーザーのログイン名を置き換え、そのユーザーとしてログインパスワードをバインドしようとすることで、ユーザーの DN を取得します。すべてのユーザーがディレクトリ内の単一ノードに保存されている場合、これは問題ありません。代わりに、ユーザーを見つけるために LDAP 検索フィルターを構成する場合は、次を使用できます。

例 73: 検索フィルターを使用したバインド認証
Java
@Bean
BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
    String searchBase = "ou=people";
    String filter = "(uid={0})";
    FilterBasedLdapUserSearch search =
        new FilterBasedLdapUserSearch(searchBase, filter, contextSource);
    BindAuthenticator authenticator = new BindAuthenticator(contextSource);
    authenticator.setUserSearch(search);
    return authenticator;
}

@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
    return new LdapAuthenticationProvider(authenticator);
}
XML
<ldap-authentication-provider
        user-search-filter="(uid={0})"
    user-search-base="ou=people"/>
Kotlin
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator {
    val searchBase = "ou=people"
    val filter = "(uid={0})"
    val search = FilterBasedLdapUserSearch(searchBase, filter, contextSource)
    val authenticator = BindAuthenticator(contextSource)
    authenticator.setUserSearch(search)
    return authenticator
}

@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
    return LdapAuthenticationProvider(authenticator)
}

上記の ContextSource  定義で使用すると、(uid={0}) をフィルターとして使用して DN ou=people,dc=springframework,dc=org で検索を実行します。この場合も、フィルター名のパラメーターはユーザーのログイン名に置き換えられるため、uid 属性がユーザー名と等しいエントリを検索します。ユーザー検索ベースが提供されない場合、検索はルートから実行されます。

パスワード認証を使用する

パスワードの比較とは、ユーザーが指定したパスワードとリポジトリに保存されているパスワードを比較することです。これは、パスワード属性の値を取得してローカルで確認するか、指定されたパスワードを比較のためにサーバーに渡して実際のパスワード値が取得されない LDAP「比較」操作を実行することで実行できます。パスワードがランダムソルトで適切にハッシュされている場合、LDAP 比較は実行できません。

例 74: 最小限のパスワード比較構成
Java
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
    return new PasswordComparisonAuthenticator(contextSource);
}

@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
    return new LdapAuthenticationProvider(authenticator);
}
XML
<ldap-authentication-provider
        user-dn-pattern="uid={0},ou=people">
    <password-compare />
</ldap-authentication-provider>
Kotlin
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator {
    return PasswordComparisonAuthenticator(contextSource)
}

@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
    return LdapAuthenticationProvider(authenticator)
}

いくつかのカスタマイズを含むより高度な構成を以下に示します。

例 75: パスワード比較構成
Java
@Bean
PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) {
    PasswordComparisonAuthenticator authenticator =
        new PasswordComparisonAuthenticator(contextSource);
    authenticator.setPasswordAttributeName("pwd"); (1)
    authenticator.setPasswordEncoder(new BCryptPasswordEncoder()); (2)
    return authenticator;
}

@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) {
    return new LdapAuthenticationProvider(authenticator);
}
XML
<ldap-authentication-provider
        user-dn-pattern="uid={0},ou=people">
    <password-compare password-attribute="pwd"> (1)
        <password-encoder ref="passwordEncoder" /> (2)
    </password-compare>
</ldap-authentication-provider>
<b:bean id="passwordEncoder"
    class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
Kotlin
@Bean
fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator {
    val authenticator = PasswordComparisonAuthenticator(contextSource)
    authenticator.setPasswordAttributeName("pwd") (1)
    authenticator.setPasswordEncoder(BCryptPasswordEncoder()) (2)
    return authenticator
}

@Bean
fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider {
    return LdapAuthenticationProvider(authenticator)
}
1 パスワード属性を pwd として指定する
2BCryptPasswordEncoder を使用する
LdapAuthoritiesPopulator

Spring Security の LdapAuthoritiesPopulator は、ユーザーに返される権限を決定するために使用されます。

例 76: 最小限のパスワード比較構成
Java
@Bean
LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) {
    String groupSearchBase = "";
    DefaultLdapAuthoritiesPopulator authorities =
        new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase);
    authorities.setGroupSearchFilter("member={0}");
    return authorities;
}

@Bean
LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authorities) {
    return new LdapAuthenticationProvider(authenticator, authorities);
}
XML
<ldap-authentication-provider
    user-dn-pattern="uid={0},ou=people"
    group-search-filter="member={0}"/>
Kotlin
@Bean
fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopulator {
    val groupSearchBase = ""
    val authorities = DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase)
    authorities.setGroupSearchFilter("member={0}")
    return authorities
}

@Bean
fun authenticationProvider(authenticator: LdapAuthenticator, authorities: LdapAuthoritiesPopulator): LdapAuthenticationProvider {
    return LdapAuthenticationProvider(authenticator, authorities)
}
Active Directory

Active Directory は独自の非標準の認証オプションをサポートしており、通常の使用パターンは標準の LdapAuthenticationProvider にはあまり合いません。通常、認証は、LDAP 識別名を使用するのではなく、ドメインユーザー名([email protected] (英語) 形式)を使用して実行されます。これを簡単にするために、Spring Security には、一般的な Active Directory セットアップ用にカスタマイズされた認証プロバイダーがあります。

ActiveDirectoryLdapAuthenticationProvider の構成は非常に簡単です。ドメイン名と、サーバーのアドレス [1] を提供する LDAP URL を提供するだけです。以下に設定例を示します。

例 77: Active Directory 構成の例
Java
@Bean
ActiveDirectoryLdapAuthenticationProvider authenticationProvider() {
    return new ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/");
}
XML
<bean id="authenticationProvider"
        class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <constructor-arg value="example.com" />
    <constructor-arg value="ldap://company.example.com/" />
</bean>
Kotlin
@Bean
fun authenticationProvider(): ActiveDirectoryLdapAuthenticationProvider {
    return ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/")
}

10.11. セッション管理

HTTP セッションに関連する機能は、フィルターが委譲する SessionManagementFilter と SessionAuthenticationStrategy インターフェースの組み合わせによって処理されます。典型的な使用箇所には、セッション固定保護攻撃防止、セッションタイムアウトの検出、および認証済みユーザーが同時に開くことができるセッション数の制限が含まれます。

10.11.1. タイムアウトの検出

Spring Security を構成して、無効なセッション ID の送信を検出し、ユーザーを適切な URL にリダイレクトできます。これは、session-management 要素によって実現されます。

<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>

このメカニズムを使用してセッションタイムアウトを検出した場合、ユーザーがログアウトしてからブラウザーを閉じずに再度ログインすると、誤ってエラーが報告される可能性があることに注意してください。これは、セッションを無効にしてもセッション Cookie がクリアされず、ユーザーがログアウトしても再送信されるためです。ログアウトハンドラーで次の構文を使用するなどして、ログアウト時に JSESSIONID Cookie を明示的に削除できる場合があります。

<http>
<logout delete-cookies="JSESSIONID" />
</http>

残念ながら、これはすべてのサーブレットコンテナーで動作することを保証できないため、環境でテストする必要があります。

プロキシの背後でアプリケーションを実行している場合は、プロキシサーバーを構成してセッション Cookie を削除することもできます。例:Apache HTTPD の mod_headers を使用すると、次のディレクティブは、ログアウトリクエストへのレスポンスで JSESSIONID Cookie を期限切れにして削除します(アプリケーションがパス /tutorial にデプロイされていると仮定します)。

<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>

10.11.2. 同時セッション制御

アプリケーションにログインするための 1 人のユーザの機能に制約を課したい場合、Spring Security は以下の簡単な追加を行うことで、これをすぐにサポートします。まず、Spring Security がセッションライフサイクルイベントについて最新の状態に保つために、以下のリスナーを web.xml ファイルに追加する必要があります。

<listener>
<listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>

次に、アプリケーションコンテキストに次の行を追加します。

<http>
...
<session-management>
    <concurrency-control max-sessions="1" />
</session-management>
</http>

これにより、ユーザーが複数回ログインするのを防ぐことができます。2 回目のログインでは、最初のログインが無効になります。多くの場合、2 回目のログインを禁止することをお勧めします。その場合

<http>
...
<session-management>
    <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>

2 回目のログインは拒否されます。「拒否」とは、フォームベースのログインが使用されている場合、ユーザーが authentication-failure-url に送信されることを意味します。2 番目の認証が「remember-me」などの別の非対話型メカニズムを介して行われる場合、「unauthorized」(401) エラーがクライアントに送信されます。代わりにエラーページを使用する場合は、session-authentication-error-url 属性を session-management 要素に追加できます。

フォームベースのログインにカスタマイズされた認証フィルターを使用している場合、同時セッション制御サポートを明示的に構成する必要があります。詳細については、セッション管理の章を参照してください。

10.11.3. セッション固定攻撃保護

セッション固定 (英語) 攻撃は、悪意のある攻撃者がサイトにアクセスしてセッションを作成し、同じセッションでログインするように別のユーザーを誘導する可能性がある潜在的なリスクです(たとえば、セッション識別子をパラメーターとして含むリンクを送信することにより))。Spring Security は、ユーザーがログインするときに新しいセッションを作成するか、セッション ID を変更することにより、これに対して自動的に保護します。この保護が必要ない場合、または他の要件と競合する場合は、<session-management> の session-fixation-protection 属性を使用して動作を制御できます。4 つのオプションがあります

  • none - 何もしないでください。元のセッションは保持されます。

  • newSession - 既存のセッションデータをコピーせずに、新しい「クリーン」セッションを作成します(Spring セキュリティ関連の属性は引き続きコピーされます)。

  • migrateSession - 新しいセッションを作成し、既存のすべてのセッション属性を新しいセッションにコピーします。これは、Servlet 3.0 以前のコンテナーのデフォルトです。

  • changeSessionId - 新しいセッションを作成しないでください。代わりに、サーブレットコンテナー(HttpServletRequest#changeSessionId())によって提供されるセッション固定保護を使用します。このオプションは、Servlet 3.1(Java EE 7)以降のコンテナーでのみ使用可能です。古いコンテナーで指定すると、例外が発生します。これは、Servlet 3.1 以降のコンテナーのデフォルトです。

セッション固定保護が発生すると、SessionFixationProtectionEvent がアプリケーションコンテキストで公開されます。changeSessionId を使用している場合は両方のイベントのためにあなたのコードを聴取した場合、この保護機能は任意 javax.servlet.http.HttpSessionIdListener S が通知される結果、その使用は注意します。追加情報については、セッション管理の章を参照してください。

10.11.4. SessionManagementFilter

SessionManagementFilter は、SecurityContextRepository のコンテンツを SecurityContextHolder の現在のコンテンツと照合して、通常、事前認証や remember-me [2] などの非対話型認証メカニズムによって現在のリクエスト中にユーザーが認証されたかどうかを判断します。リポジトリにセキュリティコンテキストが含まれている場合、フィルターは何もしません。存在せず、スレッドローカル SecurityContext に(非匿名の) Authentication オブジェクトが含まれている場合、フィルターは、スタック内の前のフィルターによって認証されたと見なします。次に、構成された SessionAuthenticationStrategy を呼び出します。

ユーザーが現在認証されていない場合、フィルターは無効なセッション ID がリクエストされたかどうかを確認し(タイムアウトなど)、構成されている InvalidSessionStrategy が設定されている場合は呼び出します。最も一般的な動作は、固定 URL にリダイレクトすることであり、これは標準実装 SimpleRedirectInvalidSessionStrategy にカプセル化されます。後者は、前述のように、ネームスペースを介して無効なセッション URL を構成するときにも使用されます。

10.11.5. SessionAuthenticationStrategy

SessionAuthenticationStrategy は SessionManagementFilter と AbstractAuthenticationProcessingFilter の両方で使用されるため、たとえば、カスタマイズされたフォームログインクラスを使用している場合は、これらの両方にそれを挿入する必要があります。この場合、名前空間とカスタム Bean を組み合わせた一般的な構成は次のようになります。

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
    <beans:property name="sessionAuthenticationStrategy" ref="sas" />
    ...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

Spring セッションスコープ Bean を含む HttpSessionBindingListener を実装するセッションに Bean を格納している場合、デフォルトの SessionFixationProtectionStrategy を使用すると問題が発生する可能性があることに注意してください。詳細については、このクラスの Javadoc を参照してください。

10.11.6. 並行性制御

Spring Security は、プリンシパルが指定された回数を超えて同じアプリケーションに対して同時に認証するのを防ぐことができます。多くの ISV はこれを利用してライセンスを強制しますが、ネットワーク管理者がこの機能を好むのは、ユーザーがログイン名を共有できないようにするためです。たとえば、ユーザー "Batman" が 2 つの異なるセッションから Web アプリケーションにログオンするのを停止できます。以前のログインを期限切れにするか、再度ログインしようとしたときにエラーを報告して、2 回目のログインを防ぐことができます。2 番目の方法を使用している場合、明示的にログアウトしていないユーザー(たとえば、ブラウザーを閉じたばかりのユーザー)は、元のセッションが期限切れになるまで再度ログインできないことに注意してください。

同時実行制御はネームスペースでサポートされているため、最も単純な構成については、以前のネームスペースの章を確認してください。ただし、場合によってはカスタマイズする必要があります。

実装では、ConcurrentSessionControlAuthenticationStrategy と呼ばれる SessionAuthenticationStrategy の特殊バージョンを使用します。

以前は、同時認証チェックは ProviderManager によって行われ、ConcurrentSessionController で注入できました。後者は、ユーザーが許可されたセッション数を超えようとしているかどうかをチェックします。ただし、このアプローチでは、HTTP セッションを事前に作成する必要があり、これは望ましくありません。Spring Security 3, では、ユーザーは最初に AuthenticationManager によって認証され、認証に成功すると、セッションが作成され、別のセッションを開くことが許可されているかどうかのチェックが行われます。

同時セッションのサポートを使用するには、以下を web.xml に追加する必要があります。

<listener>
    <listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

さらに、ConcurrentSessionFilter を FilterChainProxy に追加する必要があります。ConcurrentSessionFilter には、2 つのコンストラクター引数 sessionRegistry が必要です。これは一般に SessionRegistryImpl のインスタンスを指し、sessionInformationExpiredStrategy はセッションの有効期限が切れたときに適用する戦略を定義します。ネームスペースを使用して FilterChainProxy およびその他のデフォルト Bean を作成する構成は、次のようになります。

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
    <beans:list>
    <beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
        <beans:property name="maximumSessions" value="1" />
        <beans:property name="exceptionIfMaximumExceeded" value="true" />
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
    </beans:bean>
    <beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
        <beans:constructor-arg ref="sessionRegistry"/>
    </beans:bean>
    </beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
    class="org.springframework.security.core.session.SessionRegistryImpl" />

リスナーを web.xml に追加すると、HttpSession が開始または終了するたびに、ApplicationEvent が Spring ApplicationContext に公開されます。セッションが終了すると SessionRegistryImpl に通知できるため、これは重要です。これがないと、ユーザーは、別のセッションからログアウトしたりタイムアウトしたりしても、セッションの許容量を超えると再びログインできなくなります。

現在認証されているユーザーとそのセッションについて SessionRegistry を照会する

名前空間またはプレーン Bean を使用して同時実行制御を設定すると、アプリケーション内で直接使用できる SessionRegistry への参照が提供されるという便利な副作用があります。ユーザーが持つ可能性のあるセッションについては、インフラストラクチャをセットアップする価値があるかもしれません。maximumSession プロパティを -1 に設定して、無制限のセッションを許可できます。名前空間を使用している場合は、session-registry-alias 属性を使用して内部で作成された SessionRegistry のエイリアスを設定し、独自の Bean に注入できる参照を提供できます。

getAllPrincipals() メソッドは、現在認証されているユーザーのリストを提供します。ユーザーのセッションをリストするには、getAllSessions(Object principal, boolean includeExpiredSessions) メソッドを呼び出して、SessionInformation オブジェクトのリストを返します。SessionInformation インスタンスで expireNow() を呼び出すことにより、ユーザーのセッションを期限切れにすることもできます。ユーザーがアプリケーションに戻ると、ユーザーは続行できなくなります。たとえば、これらのメソッドは管理アプリケーションで役立ちます。詳細については、Javadoc を参照してください。

10.12. Remember-Me 認証

10.12.1. 概要

Remember-me 認証または永続ログイン認証とは、セッション間でプリンシパルの ID を記憶できる Web サイトを指します。これは通常、Cookie をブラウザに送信することで実現されます。Cookie は今後のセッションで検出され、自動ログインが行われます。Spring Security は、これらの操作を実行するために必要なフックを提供し、具体的な remember-me 実装を 2 つ備えています。1 つはハッシュを使用して Cookie ベースのトークンのセキュリティを保持し、もう 1 つはデータベースまたはその他の永続的なストレージメカニズムを使用して、生成されたトークンを保存します。

両方の実装が UserDetailsService を必要とすることに注意してください。UserDetailsService を使用しない認証プロバイダー(LDAP プロバイダーなど)を使用している場合、アプリケーションコンテキストに UserDetailsService Bean が含まれていない限り機能しません。

10.12.2. 単純なハッシュベースのトークンアプローチ

このアプローチでは、ハッシュを使用して便利な remember-me 戦略を実現します。基本的に、インタラクティブ認証が成功すると、Cookie がブラウザに送信されます。Cookie は次のように構成されます。

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

remember-me トークンは、指定された期間のみ有効であり、ユーザー名、パスワード、およびキーが変更されない限り提供されます。特に、これには、トークンの有効期限が切れるまでキャプチャーされた remember-me トークンがユーザーエージェントから使用できるという潜在的なセキュリティ上の課題があります。これは、ダイジェスト認証の場合と同じ課題です。トークンがキャプチャーされたことをプリンシパルが認識している場合、パスワードを簡単に変更し、課題のすべての remember-me トークンをすぐに無効にすることができます。より重要なセキュリティが必要な場合は、次のセクションで説明するアプローチを使用する必要があります。あるいは、remember-me サービスはまったく使用しないでください。

名前空間の構成に関する章で説明されているトピックに精通している場合は、<remember-me> 要素を追加するだけで記憶情報認証を有効にできます。

<http>
...
<remember-me key="myAppKey"/>
</http>

通常、UserDetailsService は自動的に選択されます。アプリケーションコンテキストに複数ある場合は、user-service-ref 属性で使用する必要があるものを指定する必要があります。値は UserDetailsService Bean の名前です。

10.12.3. 永久トークンアプローチ

このアプローチはいくつかの小さな修正 [3] がある記事 http://jaspan.com/improved_persistent_login_cookie_best_practice (英語) に基づいています。名前空間の構成でこのアプローチを使用するには、データソース参照を提供します。

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

データベースには、次の SQL(または同等のもの)を使用して作成された persistent_logins テーブルが含まれている必要があります。

create table persistent_logins (username varchar(64) not null,
                                series varchar(64) primary key,
                                token varchar(64) not null,
                                last_used timestamp not null)

10.12.4. Remember-Me インターフェースと実装

Remember-me は UsernamePasswordAuthenticationFilter とともに使用され、AbstractAuthenticationProcessingFilter スーパークラスのフックを介して実装されます。BasicAuthenticationFilter 内でも使用されます。フックは、適切なタイミングで具体的な RememberMeServices を呼び出します。インターフェースは次のようになります。

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication successfulAuthentication);

メソッドの機能に関する詳細については、Javadoc を参照してください。ただし、この段階では、AbstractAuthenticationProcessingFilter は loginFail() および loginSuccess() メソッドのみを呼び出すことに注意してください。autoLogin() メソッドは、SecurityContextHolder に Authentication が含まれていない場合に、RememberMeAuthenticationFilter によって呼び出されます。このインターフェースは、基礎となる remember-me 実装に認証関連イベントの十分な通知を提供し、候補 Web リクエストに Cookie が含まれていて記憶したい場合は常に実装に委譲します。この設計により、任意の数の remember-me 実装戦略を使用できます。上記で、Spring Security が 2 つの実装を提供することを確認しました。これらを順番に見ていきます。

TokenBasedRememberMeServices

この実装は、単純なハッシュベースのトークンアプローチで説明されているより単純なアプローチをサポートしています。TokenBasedRememberMeServices は RememberMeAuthenticationProvider によって処理される RememberMeAuthenticationToken を生成します。key は、この認証プロバイダーと TokenBasedRememberMeServices の間で共有されます。さらに、TokenBasedRememberMeServices には、署名の比較のためにユーザー名とパスワードを取得し、正しい GrantedAuthority を含む RememberMeAuthenticationToken を生成できる UserDetailsService が必要です。ユーザーがリクエストした場合に Cookie を無効にするアプリケーションによって、何らかのログアウトコマンドが提供される必要があります。TokenBasedRememberMeServices は Spring Security の LogoutHandler インターフェースも実装しているため、LogoutFilter と併用して Cookie を自動的にクリアすることができます。

remember-me サービスを有効にするためにアプリケーションコンテキストで必要な Bean は次のとおりです。

<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>

RememberMeServices 実装を UsernamePasswordAuthenticationFilter.setRememberMeServices() プロパティに追加し、RememberMeAuthenticationProvider を AuthenticationManager.setProviders() リストに追加し、RememberMeAuthenticationFilter を FilterChainProxy に追加することを忘れないでください(通常は UsernamePasswordAuthenticationFilter の直後)。

PersistentTokenBasedRememberMeServices

このクラスは TokenBasedRememberMeServices と同じ方法で使用できますが、トークンを保存するには PersistentTokenRepository でさらに構成する必要があります。2 つの標準実装があります。

  • テストのみを目的とした InMemoryTokenRepositoryImpl

  • トークンをデータベースに保存する JdbcTokenRepositoryImpl

データベーススキーマは、上記の永久トークンアプローチで説明されています。

10.13. OpenID サポート

名前空間は、通常のフォームベースのログインの代わりに、またはそれに加えて、簡単な変更で OpenID (英語) ログインをサポートします。

<http>
<intercept-url pattern="/**" access="ROLE_USER" />
<openid-login />
</http>

次に、OpenID プロバイダー(myopenid.com など)に自分自身を登録し、ユーザー情報をインメモリ <user-service> に追加する必要があります。

<user name="https://jimi.hendrix.myopenid.com/" authorities="ROLE_USER" />

認証のために myopenid.com サイトを使用してログインできるはずです。openid-login 要素の user-service-ref 属性を設定することにより、OpenID を使用する特定の UserDetailsService Bean を選択することもできます。このユーザーデータのセットはユーザーの権限を読み込むためにのみ使用されるため、上記のユーザー構成からパスワード属性を省略していることに注意してください。ランダムなパスワードが内部で生成されるため、このユーザーデータを構成の他の場所で認証ソースとして誤って使用することを防ぎます。

10.13.1. 属性交換

OpenID 属性交換 (英語) のサポート。例として、次の構成は、アプリケーションで使用するために、OpenID プロバイダーからメールとフルネームを取得しようとします。

<openid-login>
<attribute-exchange>
    <openid-attribute name="email" type="https://axschema.org/contact/email" required="true"/>
    <openid-attribute name="name" type="https://axschema.org/namePerson"/>
</attribute-exchange>
</openid-login>

各 OpenID 属性の「タイプ」は、特定のスキーマ(この場合は https://axschema.org/ (英語) )によって決定される URI です。認証を成功させるために属性を取得する必要がある場合、required 属性を設定できます。サポートされる正確なスキーマと属性は、OpenID プロバイダーによって異なります。属性値は認証プロセスの一部として返され、次のコードを使用して後でアクセスできます。

OpenIDAuthenticationToken token =
    (OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List<OpenIDAttribute> attributes = token.getAttributes();

SecurityContextHolder から OpenIDAuthenticationToken を取得できます。OpenIDAttribute には、属性タイプと取得された値(または複数値属性の場合は値)が含まれます。各 identifier-matcher 属性を使用して、複数の attribute-exchange 要素を提供できます。これには、ユーザーが指定した OpenID 識別子と照合される正規表現が含まれます。構成例については、コードベースの OpenID サンプルアプリケーションを参照してください。Google、Yahoo、および MyOpenID プロバイダーに異なる属性リストが提供されています。

10.14. 匿名認証

10.14.1. 概要

一般に、許可するものと明示的に禁止するものを明示的に指定する「デフォルトで拒否」を採用することは、優れたセキュリティ慣行と考えられています。特に Web アプリケーションの場合、認証されていないユーザーがアクセスできるものを定義することも同様の状況です。多くのサイトでは、いくつかの URL 以外(ホームページやログインページなど)についてユーザーを認証する必要があります。この場合、すべての保護されたリソースに対してではなく、これらの特定の URL に対してアクセス構成属性を定義するのが最も簡単です。別の言い方をすると、ROLE_SOMETHING がデフォルトで必要であり、アプリケーションのログイン、ログアウト、ホームページなど、このルールの特定の例外のみを許可すると言うのはいいことです。これらのページをフィルターチェーンから完全に省略して、アクセス制御チェックをバイパスすることもできますが、他の理由、特に認証されたユーザーに対してページの動作が異なる場合、これは望ましくない場合があります。

これが匿名認証の意味です。「匿名で認証された」ユーザーと認証されていないユーザーの間に実際の概念上の違いはないことに注意してください。Spring Security の匿名認証は、アクセス制御属性を構成するより便利な方法を提供するだけです。たとえば、getCallerPrincipal などのサーブレット API 呼び出しの呼び出しは、実際には SecurityContextHolder に匿名認証オブジェクトが存在する場合でも null を返します。

監査インターセプターが SecurityContextHolder を照会して、特定の操作を担当したプリンシパルを識別する場合など、匿名認証が役立つ状況が他にもあります。SecurityContextHolder が常に Authentication オブジェクトを含み、決して null を含まないことがわかっている場合、クラスをより堅牢に作成できます。

10.14.2. 構成

匿名認証のサポートは、HTTP 構成 Spring Security 3.0 を使用すると自動的に提供され、<anonymous> 要素を使用してカスタマイズ(または無効化)できます。従来の Bean 構成を使用していない限り、ここで説明する Bean を構成する必要はありません。

匿名認証機能を一緒に提供する 3 つのクラス。AnonymousAuthenticationToken は Authentication の実装であり、匿名プリンシパルに適用される GrantedAuthority を格納します。AnonymousAuthenticationToken が受け入れられるように、ProviderManager にチェーンされた対応する AnonymousAuthenticationProvider があります。最後に、AnonymousAuthenticationFilter があります。これは、通常の認証メカニズムの後にチェーンされ、既存の Authentication が保持されていない場合、AnonymousAuthenticationToken を SecurityContextHolder に自動的に追加します。フィルターと認証プロバイダーの定義は次のように表示されます。

<bean id="anonymousAuthFilter"
    class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

<bean id="anonymousAuthenticationProvider"
    class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

key はフィルターと認証プロバイダーの間で共有されるため、前者によって作成されたトークンは後者 [4] によって受け入れられます。userAttribute は usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority] の形式で表現されます。これは、InMemoryDaoImpl の userMap プロパティの等号の後に使用されるものと同じ構文です。

前に説明したように、匿名認証の利点は、すべての URI パターンにセキュリティを適用できることです。例:

<bean id="filterSecurityInterceptor"
    class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
    <security:filter-security-metadata-source>
    <security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
    <security:intercept-url pattern='/**' access='ROLE_USER'/>
    </security:filter-security-metadata-source>" +
</property>
</bean>

10.14.3. AuthenticationTrustResolver

匿名認証の議論を締めくくるのは、AuthenticationTrustResolver インターフェースとそれに対応する AuthenticationTrustResolverImpl 実装です。このインターフェースは isAnonymous(Authentication) メソッドを提供します。これにより、関心のあるクラスはこの特別なタイプの認証ステータスを考慮することができます。ExceptionTranslationFilter は、AccessDeniedException の処理にこのインターフェースを使用します。AccessDeniedException がスローされ、認証が匿名タイプの場合、403(禁止)レスポンスをスローする代わりに、フィルターは代わりに AuthenticationEntryPoint を開始し、プリンシパルが正しく認証できるようにします。これは必要な区別です。そうでなければ、プリンシパルは常に「認証済み」とみなされ、フォーム、基本、ダイジェスト、またはその他の通常の認証メカニズムを介してログインする機会が与えられません。

多くの場合、上記のインターセプター構成の ROLE_ANONYMOUS 属性が IS_AUTHENTICATED_ANONYMOUSLY に置き換えられます。これは、アクセス制御を定義するときに事実上同じものです。これは、認可の章で見る AuthenticatedVoter の使用例です。AuthenticationTrustResolver を使用してこの特定の構成属性を処理し、匿名ユーザーにアクセスを認可します。AuthenticatedVoter アプローチは、匿名ユーザー、remember-me ユーザー、および完全に認証されたユーザーを区別できるため、より強力です。この機能が必要ない場合は、Spring Security の標準 RoleVoter で処理される ROLE_ANONYMOUS を使用できます。

10.15. 事前認証シナリオ

Spring Security を認可に使用したい場合もありますが、ユーザーはアプリケーションにアクセスする前に外部システムによってすでに確実に認証されています。これらの状況を「事前認証済み」シナリオと呼びます。例には、X.509、Siteminder、およびアプリケーションが実行されている Java EE コンテナーによる認証が含まれます。事前認証を使用する場合、Spring Security は

  • リクエストを行っているユーザーを特定します。

  • ユーザーの権限を取得します。

詳細は、外部認証メカニズムによって異なります。ユーザーは、X.509 の場合は証明書情報によって、Siteminder の場合は HTTP リクエストヘッダーによって識別されます。コンテナー認証に依存している場合、ユーザーは受信 HTTP リクエストで getUserPrincipal() メソッドを呼び出すことにより識別されます。場合によっては、外部メカニズムがユーザーにロール / 権限情報を提供することがありますが、別の場合は、UserDetailsService などの別のソースから権限を取得する必要があります。

10.15.1. 事前認証フレームワーククラス

ほとんどの事前認証メカニズムは同じパターンに従うため、Spring Security には事前認証された認証プロバイダーを実装するための内部フレームワークを提供するクラスのセットがあります。これにより、重複がなくなり、新しい実装を構造化された方法で追加できるようになり、すべてをゼロから記述する必要がなくなります。X.509 認証のようなものを使用する場合、これらのクラスについて知る必要はありません。既に使用しやすく、使いやすい名前空間設定オプションが既にあるためです。明示的な Bean 構成を使用する必要がある場合、または独自の実装の作成を計画している場合は、提供された実装がどのように機能するかを理解しておくと役立ちます。org.springframework.security.web.authentication.preauth にクラスがあります。ここで概要を説明するだけなので、必要に応じて Javadoc とソースを参照してください。

AbstractPreAuthenticatedProcessingFilter

このクラスは、セキュリティコンテキストの現在の内容をチェックし、空の場合、HTTP リクエストからユーザー情報を抽出して AuthenticationManager に送信しようとします。この情報を取得するために、サブクラスは次のメソッドをオーバーライドします。

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

これらを呼び出した後、フィルターは返されたデータを含む PreAuthenticatedAuthenticationToken を作成し、認証のために送信します。ここでの「認証」とは、ユーザーの権限をロードするためのさらなる処理を意味しますが、標準の Spring Security 認証アーキテクチャに従います。

他の Spring Security 認証フィルターと同様に、事前認証フィルターには authenticationDetailsSource プロパティがあり、デフォルトで WebAuthenticationDetails オブジェクトを作成して、Authentication オブジェクトの details プロパティにセッション ID や発信元 IP アドレスなどの追加情報を保存します。事前認証メカニズムからユーザーロール情報を取得できる場合、データもこのプロパティに格納され、詳細が GrantedAuthoritiesContainer インターフェースを実装します。これにより、認証プロバイダーは、ユーザーに外部的に割り当てられた権限を読み取ることができます。次に具体的な例を見てみましょう。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource

このクラスのインスタンスである authenticationDetailsSource でフィルターが構成されている場合、権限情報は、「マップ可能なロール」の所定のセットごとに isUserInRole(String role) メソッドを呼び出すことによって取得されます。クラスは、構成された MappableAttributesRetriever からこれらを取得します。可能な実装には、アプリケーションコンテキストでリストをハードコードし、web.xml ファイルの <security-role> 情報からロール情報を読み取ることが含まれます。事前認証サンプルアプリケーションは、後者のアプローチを使用します。

構成された Attributes2GrantedAuthoritiesMapper を使用して、ロール(または属性)が Spring Security GrantedAuthority オブジェクトにマップされる追加の段階があります。デフォルトでは、通常の ROLE_ プレフィックスが名前に追加されますが、動作を完全に制御できます。

PreAuthenticatedAuthenticationProvider

事前認証されたプロバイダーは、ユーザーのために UserDetails オブジェクトをロードする以上のことはほとんどありません。AuthenticationUserDetailsService に委譲することでこれを行います。後者は標準の UserDetailsService に似ていますが、単なるユーザー名ではなく Authentication オブジェクトを取ります。

public interface AuthenticationUserDetailsService {
    UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

このインターフェースには他の用途もありますが、事前認証を使用すると、前のセクションで見たように、Authentication オブジェクトにパッケージ化された機関にアクセスできます。PreAuthenticatedGrantedAuthoritiesUserDetailsService クラスがこれを行います。または、UserDetailsByNameServiceWrapper 実装を介して標準 UserDetailsService に委譲することもできます。

Http403ForbiddenEntryPoint

AuthenticationEntryPoint は、認証されていないユーザー(保護されたリソースにアクセスしようとした場合)の認証プロセスを開始するロールを果たしますが、事前認証された場合は適用されません。事前認証を他の認証メカニズムと組み合わせて使用しない場合にのみ、このクラスのインスタンスで ExceptionTranslationFilter を構成します。ユーザーが AbstractPreAuthenticatedProcessingFilter によって拒否され、認証が null になった場合に呼び出されます。呼び出されると、常に 403 -forbidden レスポンスコードを返します。

10.15.2. 具体的な実装

X.509 認証はそれ自身の章でカバーされています。ここでは、事前に認証された他のシナリオをサポートするクラスをいくつか見ていきます。

リクエストヘッダー認証 (Siteminder)

外部認証システムは、HTTP リクエストに特定のヘッダーを設定することにより、アプリケーションに情報を提供できます。このよく知られた例は Siteminder で、SM_USER というヘッダーでユーザー名を渡します。このメカニズムは、RequestHeaderAuthenticationFilter クラスによってサポートされており、ヘッダーからユーザー名を抽出するだけです。デフォルトでは、名前 SM_USER がヘッダー名として使用されます。詳細については、Javadoc を参照してください。

このようなシステムを使用する場合、フレームワークは認証チェックをまったく実行しないため、外部システムが適切に構成され、アプリケーションへのすべてのアクセスを保護することが非常に重要です。攻撃者がこれを検出せずに元のリクエストのヘッダーを偽造できる場合、希望するユーザー名を選択する可能性があります。

Siteminder の構成例

このフィルターを使用した典型的な構成は次のようになります。

<security:http>
<!-- Additional http configuration omitted -->
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
    <bean id="userDetailsServiceWrapper"
        class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <property name="userDetailsService" ref="userDetailsService"/>
    </bean>
</property>
</bean>

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>

ここでは、構成にセキュリティ名前空間が使用されていると想定しています。また、ユーザーのロールをロードするために、構成に UserDetailsService (「userDetailsService」と呼ばれる)を追加したことを前提としています。

Java EE コンテナー認証

クラス J2eePreAuthenticatedProcessingFilter は、HttpServletRequest の userPrincipal プロパティからユーザー名を抽出します。このフィルターの使用は、通常、上記で J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource で説明した Java EE ロールの使用と組み合わされます。

このアプローチを使用するコードベースにはサンプルアプリケーションがあります。そのため、github からコードを入手し、興味がある場合はアプリケーションコンテキストファイルを確認してください。コードは samples/xml/preauth ディレクトリにあります。

10.16. Java 認証および認可サービス(JAAS)プロバイダー

10.16.1. 概要

Spring Security は、認証リクエストを Java Authentication and Authorization Service(JAAS)に委譲できるパッケージを提供します。このパッケージについては、以下で詳しく説明します。

10.16.2. AbstractJaasAuthenticationProvider

AbstractJaasAuthenticationProvider は、提供されている JAAS AuthenticationProvider 実装の基礎です。サブクラスは、LoginContext を作成するメソッドを実装する必要があります。AbstractJaasAuthenticationProvider には、以下で説明する多くの依存関係があります。

JAAS CallbackHandler

ほとんどの JAAS LoginModule には、何らかのコールバックが必要です。これらのコールバックは通常、ユーザーからユーザー名とパスワードを取得するために使用されます。

Spring Security デプロイでは、Spring Security が(認証メカニズムを介して)このユーザー対話を担当します。認証リクエストが JAAS に委譲されるまでに、Spring Security の認証メカニズムは、JAAS LoginModule が必要とするすべての情報を含む Authentication オブジェクトをすでに完全に取り込みました。

Spring Security の JAAS パッケージは、2 つのデフォルトコールバックハンドラー JaasNameCallbackHandler と JaasPasswordCallbackHandler を提供します。これらの各コールバックハンドラーは JaasAuthenticationCallbackHandler を実装します。ほとんどの場合、これらのコールバックハンドラーは、内部の仕組みを理解しなくても簡単に使用できます。

コールバックの動作を完全に制御する必要がある場合、内部で AbstractJaasAuthenticationProvider はこれらの JaasAuthenticationCallbackHandler を InternalCallbackHandler でラップします。InternalCallbackHandler は、JAAS の通常の CallbackHandler インターフェースを実際に実装するクラスです。JAAS LoginModule が使用されるたびに、InternalCallbackHandler で構成されたアプリケーションコンテキストのリストが渡されます。LoginModule が InternalCallbackHandler に対してコールバックをリクエストする場合、コールバックはラップされている JaasAuthenticationCallbackHandler に順番に渡されます。

JAAS AuthorityGranter

JAAS はプリンシパルと連携します。「ロール」でさえ、JAAS のプリンシパルとして表されます。一方、Spring Security は Authentication オブジェクトで機能します。各 Authentication オブジェクトには、単一のプリンシパルと複数の GrantedAuthority が含まれます。これらの異なる概念間のマッピングを容易にするために、Spring Security の JAAS パッケージには AuthorityGranter インターフェースが含まれています。

AuthorityGranter は、JAAS プリンシパルをインスペクションし、プリンシパルに割り当てられた権限を表す String のセットを返す責任があります。返された権限文字列ごとに、AbstractJaasAuthenticationProvider は、権限文字列と AuthorityGranter が渡された JAAS プリンシパルを含む JaasGrantedAuthority (Spring Security の GrantedAuthority インターフェースを実装)を作成します。AbstractJaasAuthenticationProvider は、最初に JAAS LoginModule を使用してユーザーの資格情報を正常に認証し、それが返す LoginContext にアクセスすることにより、JAAS プリンシパルを取得します。LoginContext.getSubject().getPrincipals() の呼び出しが行われ、結果の各プリンシパルが AbstractJaasAuthenticationProvider.setAuthorityGranters(List) プロパティに対して定義された各 AuthorityGranter に渡されます。

すべての JAAS プリンシパルには実装固有の意味があるため、Spring Security には本番 AuthorityGranter は含まれません。ただし、単体テストには、簡単な AuthorityGranter 実装を示す TestAuthorityGranter があります。

10.16.3. DefaultJaasAuthenticationProvider

DefaultJaasAuthenticationProvider では、JAAS Configuration オブジェクトを依存関係として挿入できます。次に、注入された JAAS Configuration を使用して LoginContext を作成します。これは、DefaultJaasAuthenticationProvider が JaasAuthenticationProvider のように Configuration の特定の実装にバインドされていないことを意味します。

InMemoryConfiguration

Configuration を DefaultJaasAuthenticationProvider に挿入しやすくするために、InMemoryConfiguration という名前のデフォルトのメモリ内実装が提供されています。実装コンストラクターは、各キーがログイン構成名を表し、値が AppConfigurationEntry の Array を表す Map を受け入れます。InMemoryConfiguration は、提供された Map 内でマッピングが見つからない場合に使用される AppConfigurationEntry オブジェクトのデフォルト Array もサポートします。詳細については、InMemoryConfiguration のクラスレベルの javadoc を参照してください。

DefaultJaasAuthenticationProvider の構成例

InMemoryConfiguration の Spring 構成は標準の JAAS 構成ファイルよりも詳細になりますが、DefaultJaasAuthenticationProvider と組み合わせて使用すると、デフォルトの Configuration 実装に依存しないため、JaasAuthenticationProvider よりも柔軟です。

InMemoryConfiguration を使用した DefaultJaasAuthenticationProvider の構成例を以下に示します。Configuration のカスタム実装は、DefaultJaasAuthenticationProvider にも簡単に挿入できることに注意してください。

<bean id="jaasAuthProvider"
class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider">
<property name="configuration">
<bean class="org.springframework.security.authentication.jaas.memory.InMemoryConfiguration">
<constructor-arg>
    <map>
    <!--
    SPRINGSECURITY is the default loginContextName
    for AbstractJaasAuthenticationProvider
    -->
    <entry key="SPRINGSECURITY">
    <array>
    <bean class="javax.security.auth.login.AppConfigurationEntry">
        <constructor-arg value="sample.SampleLoginModule" />
        <constructor-arg>
        <util:constant static-field=
            "javax.security.auth.login.AppConfigurationEntry$LoginModuleControlFlag.REQUIRED"/>
        </constructor-arg>
        <constructor-arg>
        <map></map>
        </constructor-arg>
        </bean>
    </array>
    </entry>
    </map>
    </constructor-arg>
</bean>
</property>
<property name="authorityGranters">
<list>
    <!-- You will need to write your own implementation of AuthorityGranter -->
    <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>

10.16.4. JaasAuthenticationProvider

JaasAuthenticationProvider は、デフォルトの Configuration が ConfigFile (英語) のインスタンスであると想定しています。この仮定は、Configuration を更新しようとするために行われます。JaasAuthenticationProvider は、デフォルトの Configuration を使用して LoginContext を作成します。

JAAS ログイン構成ファイル /WEB-INF/login.conf があり、次の内容があるとします。

JAASTest {
    sample.SampleLoginModule required;
};

すべての Spring Security Bean と同様に、JaasAuthenticationProvider はアプリケーションコンテキストを介して設定されます。次の定義は、上記の JAAS ログイン構成ファイルに対応します。

<bean id="jaasAuthenticationProvider"
class="org.springframework.security.authentication.jaas.JaasAuthenticationProvider">
<property name="loginConfig" value="/WEB-INF/login.conf"/>
<property name="loginContextName" value="JAASTest"/>
<property name="callbackHandlers">
<list>
<bean
    class="org.springframework.security.authentication.jaas.JaasNameCallbackHandler"/>
<bean
    class="org.springframework.security.authentication.jaas.JaasPasswordCallbackHandler"/>
</list>
</property>
<property name="authorityGranters">
    <list>
    <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
    </list>
</property>
</bean>

10.16.5. サブジェクトとして実行

構成されている場合、JaasApiIntegrationFilter は JaasAuthenticationToken で Subject として実行しようとします。これは、以下を使用して Subject にアクセスできることを意味します。

Subject subject = Subject.getSubject(AccessController.getContext());

この統合は、jaas-api-provision 属性を使用して簡単に構成できます。この機能は、実装されている JAAS サブジェクトに依存するレガシーまたは外部 API と統合する場合に役立ちます。

10.17. CAS 認証

10.17.1. 概要

JA-SIG は、CAS として知られるエンタープライズ規模のシングルサインオンシステムを作成します。他のイニシアチブとは異なり、JA-SIG の中央認証サービスはオープンソースであり、広く使用され、理解しやすく、プラットフォームに依存せず、プロキシ機能をサポートしています。Spring Security は CAS を完全にサポートし、Spring Security の単一アプリケーションデプロイから企業全体の CAS サーバーで保護された複数アプリケーションデプロイへの簡単な移行パスを提供します。

CAS の詳細については、https://www.apereo.org (英語) を参照してください。CAS サーバーファイルをダウンロードするには、このサイトにアクセスする必要もあります。

10.17.2. CAS の仕組み

CAS の Web サイトには CAS のアーキテクチャを詳しく説明するドキュメントが含まれていますが、ここでは Spring Security のコンテキスト内で一般的な概要を再度示します。Spring Security 3.x は CAS 3 をサポートしています。執筆時点では、CAS サーバーのバージョンは 3.4 でした。

企業のどこかで CAS サーバーをセットアップする必要があります。CAS サーバーは単なる標準の WAR ファイルであるため、サーバーをセットアップするのは難しくありません。WAR ファイル内では、ユーザーに表示されるログインおよびその他のシングルサインオンページをカスタマイズします。

CAS 3.4 サーバーをデプロイする場合、CAS に含まれる deployerConfigContext.xml で AuthenticationHandler を指定する必要もあります。AuthenticationHandler には、指定された資格情報のセットが有効かどうかについてブール値を返す単純なメソッドがあります。AuthenticationHandler 実装は、LDAP サーバーやデータベースなど、何らかのタイプのバックエンド認証リポジトリにリンクする必要があります。CAS 自体には、これを支援する多数の AuthenticationHandler がすぐに使用できます。サーバー war ファイルをダウンロードしてデプロイすると、ユーザー名と一致するパスワードを入力したユーザーを正常に認証するようにセットアップされます。これはテストに役立ちます。

CAS サーバー自体とは別に、他の主要なプレーヤーはもちろん、企業全体にデプロイされている安全な Web アプリケーションです。これらの Web アプリケーションは「サービス」と呼ばれます。サービスには 3 つのタイプがあります。サービスチケットを認証するもの、プロキシチケットを取得できるもの、プロキシチケットを認証するもの。プロキシチケットの認証は、プロキシのリストを検証する必要があり、多くの場合、プロキシチケットを再利用できるため、異なります。

Spring Security と CAS 相互作用シーケンス

Web ブラウザ、CAS サーバー、および Spring セキュリティで保護されたサービス間の基本的な相互作用は次のとおりです。

  • Web ユーザーはサービスの公開ページを閲覧しています。CAS または Spring Security は関係しません。

  • ユーザーは最終的に、安全なページ、または使用する Bean の 1 つが安全なページをリクエストします。Spring Security の ExceptionTranslationFilter は AccessDeniedException または AuthenticationException を検出します。

  • ユーザーの Authentication オブジェクト(またはその欠如)が AuthenticationException を引き起こしたため、ExceptionTranslationFilter は設定された AuthenticationEntryPoint を呼び出します。CAS を使用する場合、これは CasAuthenticationEntryPoint クラスになります。

  • CasAuthenticationEntryPoint は、ユーザーのブラウザを CAS サーバーにリダイレクトします。また、Spring Security サービス (あなたのアプリケーション) のコールバック URL である service パラメーターも示します。例 : ブラウザのリダイレクト先の URL は https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas (英語) の可能性があります。

  • ユーザーのブラウザが CAS にリダイレクトすると、ユーザー名とパスワードの入力を求められます。ユーザーが以前にログオンしたことを示すセッション Cookie を提示した場合、再度ログインするように求められることはありません(この手順には例外があります。これについては後で説明します)。CAS は、上記の PasswordHandler (または CAS 3.0 を使用する場合は AuthenticationHandler)を使用して、ユーザー名とパスワードが有効かどうかを判断します。

  • ログインに成功すると、CAS はユーザーのブラウザを元のサービスにリダイレクトします。また、ticket パラメーターも含まれます。これは、「サービスチケット」を表す不透明な文字列です。前の例を続けると、ブラウザーがリダイレクトされる URL は https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ (英語) になる可能性があります。

  • サービス Web アプリケーションに戻ると、CasAuthenticationFilter は常に /login/cas へのリクエストをリッスンしています(これは構成可能ですが、この導入ではデフォルトを使用します)。処理フィルターは、サービスチケットを表す UsernamePasswordAuthenticationToken を構築します。プリンシパルは CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER に等しくなりますが、クレデンシャルはサービスチケットの不透明な値になります。この認証リクエストは、構成された AuthenticationManager に渡されます。

  • AuthenticationManager 実装は ProviderManager になり、ProviderManager は CasAuthenticationProvider で構成されます。CasAuthenticationProvider は、CAS 固有のプリンシパル(CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER など)および CasAuthenticationToken (後述)を含む UsernamePasswordAuthenticationToken にのみ応答します。

  • CasAuthenticationProvider は、TicketValidator 実装を使用してサービスチケットを検証します。これは通常、CAS クライアントライブラリに含まれるクラスの 1 つである Cas20ServiceTicketValidator です。アプリケーションがプロキシチケットを検証する必要がある場合、Cas20ProxyTicketValidator が使用されます。TicketValidator は、サービスチケットを検証するために CAS サーバーに HTTPS リクエストを行います。この例に含まれているプロキシコールバック URL https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor (英語) も含まれる場合があります。

  • CAS サーバーに戻ると、検証リクエストが受信されます。提示されたサービスチケットが、チケットが発行されたサービス URL と一致する場合、CAS はユーザー名を示す肯定レスポンスを XML で提供します。認証にプロキシが関与している場合(後述)、プロキシのリストも XML レスポンスに含まれます。

  • [ オプション ] CAS 検証サービスへのリクエストにプロキシコールバック URL(pgtUrl パラメーター内)が含まれていた場合、CAS は XML レスポンスに pgtIou 文字列を含めます。この pgtIou は、プロキシ許可チケット IOU を表します。CAS サーバーは、pgtUrl への独自の HTTPS 接続を作成します。これは、CAS サーバーとリクエストされたサービス URL を相互に認証するためです。HTTPS 接続を使用して、プロキシ許可チケットを元の Web アプリケーションに送信します。例:https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH (英語)

  • Cas20TicketValidator は、CAS サーバーから受信した XML を解析します。CasAuthenticationProvider に TicketResponse を返します。これには、ユーザー名(必須)、プロキシリスト(含まれている場合)、およびプロキシ許可チケット IOU(プロキシコールバックがリクエストされた場合)が含まれます。

  • 次の CasAuthenticationProvider は、構成された CasProxyDecider を呼び出します。CasProxyDecider は、TicketResponse のプロキシリストがサービスに受け入れられるかどうかを示します。Spring Security には、RejectProxyTicketsAcceptAnyCasProxy と NamedCasProxyDecider のいくつかの実装が用意されています。これらの名前は、信頼できるプロキシの List を提供できる NamedCasProxyDecider を除き、ほとんど自明です。

  • CasAuthenticationProvider は、次に Assertion に含まれるユーザーに適用される GrantedAuthority オブジェクトをロードするために AuthenticationUserDetailsService をリクエストします。

  • 問題がなければ、CasAuthenticationProvider は TicketResponse および GrantedAuthority に含まれる詳細を含む CasAuthenticationToken を作成します。

  • 次に、制御が CasAuthenticationFilter に戻り、作成された CasAuthenticationToken がセキュリティコンテキストに配置されます。

  • ユーザーのブラウザは、AuthenticationException (または構成に応じてカスタムの宛先)を引き起こした元のページにリダイレクトされます。

まだここにいるのは良いことです!これがどのように構成されているか見てみましょう

10.17.3. CAS クライアントの構成

CAS の Web アプリケーション側は、Spring Security により簡単になりました。Spring Security の使用の基本を既に知っていることを前提としているため、以下ではこれらについて再度説明しません。名前空間ベースの構成が使用されていると想定し、必要に応じて CAS Bean を追加します。各セクションは前のセクションに基づいています。完全な CAS サンプルアプリケーションは、Spring Security サンプルにあります。

サービスチケット認証

このセクションでは、Spring Security をセットアップしてサービスチケットを認証する方法について説明します。多くの場合、これはすべて Web アプリケーションに必要です。ServiceProperties Bean をアプリケーションコンテキストに追加する必要があります。これは CAS サービスを表しています:

<bean id="serviceProperties"
    class="org.springframework.security.cas.ServiceProperties">
<property name="service"
    value="https://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>

service は、CasAuthenticationFilter によって監視される URL と等しくなければなりません。sendRenew のデフォルトは false ですが、アプリケーションが特に敏感な場合は true に設定する必要があります。このパラメーターは、シングルサインオンログインが受け入れられないことを CAS ログインサービスに通知します。代わりに、ユーザーはサービスにアクセスするためにユーザー名とパスワードを再入力する必要があります。

CAS 認証プロセスを開始するには、次の Bean を構成する必要があります(ネームスペース構成を使用していると仮定)。

<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>

<bean id="casFilter"
    class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

<bean id="casEntryPoint"
    class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

CAS が動作するには、ExceptionTranslationFilter の authenticationEntryPoint プロパティが CasAuthenticationEntryPoint Bean に設定されている必要があります。これは、上の例のように entry-point-ref を使用して簡単に実行できます。CasAuthenticationEntryPoint は、企業の CAS ログインサーバーへの URL を提供する ServiceProperties Bean(前述)を参照する必要があります。これは、ユーザーのブラウザがリダイレクトされる場所です。

CasAuthenticationFilter には、UsernamePasswordAuthenticationFilter (フォームベースのログインに使用)と非常によく似たプロパティがあります。これらのプロパティを使用して、認証の成功と失敗の動作などをカスタマイズできます。

次に、CasAuthenticationProvider とその協力者を追加する必要があります。

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
    <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <constructor-arg ref="userService" />
    </bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
    <constructor-arg index="0" value="https://localhost:9443/cas" />
    </bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>

<security:user-service id="userService">
<!-- 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 -->
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" />
...
</security:user-service>

CasAuthenticationProvider は、CAS によって認証されると、UserDetailsService インスタンスを使用してユーザーの権限を読み込みます。ここでは、簡単なメモリ内セットアップを示しました。CasAuthenticationProvider は実際には認証にパスワードを使用しないが、権限を使用することに注意してください。

CAS の仕組みセクションに戻って参照すると、Bean はすべて合理的に自明です。

これで、CAS の最も基本的な構成が完了しました。間違いを犯していない場合、Web アプリケーションは CAS シングルサインオンのフレームワーク内で問題なく動作するはずです。Spring Security の他の部分は、CAS が認証を処理したという事実を心配する必要はありません。次のセクションでは、いくつかの(オプションの)より高度な構成について説明します。

シングルログアウト

CAS プロトコルはシングルログアウトをサポートし、Spring Security 構成に簡単に追加できます。以下は、シングルログアウトを処理する Spring Security 構成の更新です。

<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>

<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>

<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
    class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://localhost:9443/cas/logout"/>
<constructor-arg>
    <bean class=
        "org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>

logout 要素はユーザーをローカルアプリケーションからログアウトしますが、CAS サーバーまたはログインしている他のアプリケーションとのセッションを終了しません。requestSingleLogoutFilter フィルターにより、/spring_security_cas_logout の URL をリクエストして、構成済みの CAS サーバーのログアウト URL にアプリケーションをリダイレクトすることができます。次に、CAS サーバーは、サインインしたすべてのサービスにシングルログアウトリクエストを送信します。singleLogoutFilter は、静的 Map で HttpSession を検索して無効化することにより、シングルログアウトリクエストを処理します。

logout 要素と singleLogoutFilter の両方が必要な理由は混乱するかもしれません。SingleSignOutFilter は HttpSession を静的 Map に保存するだけで、その上で無効化を呼び出すため、最初にローカルでログアウトすることをお勧めします。上記の構成では、ログアウトのフローは次のようになります。

  • ユーザーは /logout をリクエストします。/logout はユーザーをローカルアプリケーションからログアウトし、ログアウト成功ページに送信します。

  • ログアウト成功ページ /cas-logout.jsp は、すべてのアプリケーションからログアウトするために、/logout/cas を指すリンクをクリックするようユーザーに指示する必要があります。

  • ユーザーがリンクをクリックすると、ユーザーは CAS シングルログアウト URL(https://localhost:9443/cas/logout)にリダイレクトされます。

  • CAS サーバー側では、CAS シングルログアウト URL がシングルログアウトリクエストをすべての CAS サービスに送信します。CAS サービス側では、JASIG の SingleSignOutFilter が元のセッションを無効にすることでログアウトリクエストを処理します。

次のステップは、web.xml に以下を追加することです

<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
    org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
    <param-name>encoding</param-name>
    <param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
    org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

SingleSignOutFilter を使用すると、エンコードの問題が発生する場合があります。CharacterEncodingFilter を追加して、SingleSignOutFilter を使用するときに文字エンコードが正しいことを確認することをお勧めします。繰り返しになりますが、詳細については JASIG のドキュメントを参照してください。SingleSignOutHttpSessionListener は、HttpSession の有効期限が切れると、シングルログアウトに使用されたマッピングが削除されるようにします。

CAS を使用したステートレスサービスへの認証

このセクションでは、CAS を使用してサービスに対して認証する方法について説明します。つまり、このセクションでは、CAS で認証するサービスを使用するクライアントをセットアップする方法について説明します。次のセクションでは、CAS を使用して認証するステートレスサービスをセットアップする方法について説明します。

プロキシ許可チケットを取得するための CAS の構成

ステートレスサービスを認証するには、アプリケーションはプロキシ許可チケット(PGT)を取得する必要があります。このセクションでは、thencas-st [Service Ticket Authentication] 設定時に PGT を取得するために Spring Security を設定する方法について説明します。

最初のステップは、Spring Security 構成に ProxyGrantingTicketStorage を含めることです。これは、プロキシチケットを取得するために使用できるように、CasAuthenticationFilter によって取得された PGT を保存するために使用されます。構成例を以下に示します

<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

次のステップは、CasAuthenticationProvider を更新してプロキシチケットを取得できるようにすることです。これを行うには、Cas20ServiceTicketValidator を Cas20ProxyTicketValidator に置き換えます。proxyCallbackUrl は、アプリケーションが PGT を受け取る URL に設定する必要があります。最後に、構成は PGT を使用してプロキシチケットを取得できるように、ProxyGrantingTicketStorage も参照する必要があります。以下に行う必要がある構成変更の例を見つけることができます。

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
    <constructor-arg value="https://localhost:9443/cas"/>
        <property name="proxyCallbackUrl"
        value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/>
    <property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
    </bean>
</property>
</bean>

最後のステップは、CasAuthenticationFilter を更新して PGT を受け入れ、ProxyGrantingTicketStorage に保管することです。proxyReceptorUrl が Cas20ProxyTicketValidator の proxyCallbackUrl と一致することが重要です。構成例を以下に示します。

<bean id="casFilter"
        class="org.springframework.security.cas.web.CasAuthenticationFilter">
    ...
    <property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
    <property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>
プロキシチケットを使用したステートレスサービスの呼び出し

Spring Security が PGT を取得したため、使用して、ステートレスサービスの認証に使用できるプロキシチケットを作成できます。CAS サンプルアプリケーションには、ProxyTicketSampleServlet の実例が含まれています。サンプルコードは以下のとおりです。

protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);

// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}
代理チケット認証

CasAuthenticationProvider は、ステートフルクライアントとステートレスクライアントを区別します。ステートフルクライアントは、CasAuthenticationFilter の filterProcessUrl に送信するものと見なされます。ステートレスクライアントは、filterProcessUrl 以外の URL で CasAuthenticationFilter に認証リクエストを提示するものです。

リモーティングプロトコルには HttpSession のコンテキスト内で自身を提示する方法がないため、リクエスト間のセッションにセキュリティコンテキストを保存するデフォルトのプラクティスに依存することはできません。さらに、TicketValidator によって検証された後、CAS サーバーはチケットを無効にするため、後続のリクエストで同じプロキシチケットを提示しても機能しません。

明らかなオプションの 1 つは、リモートプロトコルクライアントに CAS をまったく使用しないことです。ただし、これは CAS の多くの望ましい機能を削除します。中間として、CasAuthenticationProvider は StatelessTicketCache を使用します。これは、CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER に等しいプリンシパルを使用するステートレスクライアントにのみ使用されます。CasAuthenticationProvider は、結果の CasAuthenticationToken を StatelessTicketCache に保存し、プロキシチケットにキーを設定します。リモーティングプロトコルクライアントは同じプロキシチケットを提示することができ、CasAuthenticationProvider は検証のために CAS サーバーに接続する必要はありません(最初のリクエストを除く)。認証されると、プロキシチケットは元のターゲットサービス以外の URL に使用できます。

このセクションは、プロキシチケット認証に対応するために、前のセクションに基づいています。最初のステップは、以下に示すように、すべてのアーティファクトの認証を指定することです。

<bean id="serviceProperties"
    class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>

次のステップは、serviceProperties と CasAuthenticationFilter の authenticationDetailsSource を指定することです。serviceProperties プロパティは、filterProcessUrl に存在するものだけではなく、すべてのアーティファクトを認証しようとするように CasAuthenticationFilter に指示します。ServiceAuthenticationDetailsSource は ServiceAuthenticationDetails を作成し、HttpServletRequest に基づいて現在の URL がチケットの検証時にサービス URL として使用されるようにします。サービス URL を生成する方法は、カスタム ServiceAuthenticationDetails を返すカスタム AuthenticationDetailsSource を注入することによりカスタマイズできます。

<bean id="casFilter"
    class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
    <bean class=
    "org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
    <constructor-arg ref="serviceProperties"/>
    </bean>
</property>
</bean>

プロキシチケットを処理するには、CasAuthenticationProvider を更新する必要もあります。これを行うには、Cas20ServiceTicketValidator を Cas20ProxyTicketValidator に置き換えます。statelessTicketCache と、受け入れたいプロキシを設定する必要があります。すべてのプロキシを受け入れるために必要な更新の例を以下に示します。

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
    <constructor-arg value="https://localhost:9443/cas"/>
    <property name="acceptAnyProxy" value="true"/>
    </bean>
</property>
<property name="statelessTicketCache">
    <bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
    <property name="cache">
        <bean class="net.sf.ehcache.Cache"
            init-method="initialise" destroy-method="dispose">
        <constructor-arg value="casTickets"/>
        <constructor-arg value="50"/>
        <constructor-arg value="true"/>
        <constructor-arg value="false"/>
        <constructor-arg value="3600"/>
        <constructor-arg value="900"/>
        </bean>
    </property>
    </bean>
</property>
</bean>

10.18. X.509 認証

10.18.1. 概要

X.509 証明書認証の最も一般的な用途は、SSL を使用する場合、最も一般的にはブラウザーから HTTPS を使用する場合のサーバーの ID の検証です。ブラウザは、サーバーによって提示された証明書が、管理している信頼できる認証局のリストの 1 つによって発行された(つまり、デジタル署名されている)ことを自動的に確認します。

「相互認証」で SSL を使用することもできます。サーバーは、SSL ハンドシェイクの一部としてクライアントに有効な証明書をリクエストします。サーバーは、証明書が受け入れ可能な機関によって署名されていることを確認することにより、クライアントを認証します。有効な証明書が提供されている場合、アプリケーションのサーブレット API を介して取得できます。Spring Security X.509 モジュールは、フィルターを使用して証明書を抽出します。証明書をアプリケーションユーザーにマップし、そのユーザーの付与された権限のセットをロードして、標準 Spring Security インフラストラクチャで使用します。

Spring Security で証明書を使用する前に、証明書の使用とサーブレットコンテナーのクライアント認証の設定に精通している必要があります。ほとんどの作業は、適切な証明書とキーの作成とインストールです。例:Tomcat を使用している場合は、こちらの手順 https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html (英語) を参照してください。Spring Security で試す前に、これを機能させることが重要です

10.18.2. Web アプリケーションへの X.509 認証の追加

X.509 クライアント認証の有効化は非常に簡単です。<x509/> 要素を http セキュリティ名前空間の構成に追加するだけです。

<http>
...
    <x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>

要素には 2 つのオプション属性があります。

  • subject-principal-regex。証明書のサブジェクト名からユーザー名を抽出するために使用される正規表現。デフォルト値は上に示されています。これは、ユーザーの権限をロードするために UserDetailsService に渡されるユーザー名です。

  • user-service-ref。これは、X.509 で使用される UserDetailsService の Bean ID です。アプリケーションコンテキストで定義されているのが 1 つだけの場合は必要ありません。

subject-principal-regex には単一のグループが含まれている必要があります。たとえば、デフォルトの表現「CN =(.* ?)」は、共通名フィールドと一致します。そのため、証明書のサブジェクト名が「CN = Jimi Hendrix、OU = …」である場合、これは「Jimi Hendrix」のユーザー名を与えます。一致は大文字と小文字を区別しません。「emailAddress =(.* ?)、」は「EMAILADDRESS = [ メール保護 ] (英語) 、CN = …」と一致し、ユーザー名「[ メール保護 ] (英語) 」を与えます。クライアントが証明書を提示し、有効なユーザー名が正常に抽出された場合、セキュリティコンテキストに有効な Authentication オブジェクトがあるはずです。証明書が見つからない場合、または対応するユーザーが見つからない場合、セキュリティコンテキストは空のままになります。これは、フォームベースのログインなどの他のオプションで X.509 認証を簡単に使用できることを意味します。

10.18.3. Tomcat で SSL をセットアップする

Spring Security プロジェクトの samples/certificate ディレクトリには、いくつかの事前生成された証明書があります。独自に生成したくない場合は、これらを使用してテスト用に SSL を有効にすることができます。ファイル server.jks には、サーバー証明書、秘密鍵、および発行認証局証明書が含まれています。サンプルアプリケーションのユーザー向けのクライアント証明書ファイルもいくつかあります。これらをブラウザにインストールして、SSL クライアント認証を有効にすることができます。

SSL サポートを使用して Tomcat を実行するには、server.jks ファイルを Tomcat conf ディレクトリにドロップし、次のコネクターを server.xml ファイルに追加する

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
            clientAuth="true" sslProtocol="TLS"
            keystoreFile="${catalina.home}/conf/server.jks"
            keystoreType="JKS" keystorePass="password"
            truststoreFile="${catalina.home}/conf/server.jks"
            truststoreType="JKS" truststorePass="password"
/>

クライアントが証明書を提供しなくても SSL 接続を成功させたい場合は、clientAuth を want に設定することもできます。証明書を提示しないクライアントは、フォーム認証などの非 X.509 認証メカニズムを使用しない限り、Spring Security で保護されたオブジェクトにアクセスできません。

10.19. Run-As 認証の置き換え

10.19.1. 概要

AbstractSecurityInterceptor は、セキュアオブジェクトコールバックフェーズ中に、SecurityContext および SecurityContextHolder の Authentication オブジェクトを一時的に置き換えることができます。これは、元の Authentication オブジェクトが AuthenticationManager および AccessDecisionManager によって正常に処理された場合にのみ発生します。RunAsManager は、SecurityInterceptorCallback の間に使用される置換 Authentication オブジェクトがあれば、それを示します。

セキュアオブジェクトコールバックフェーズ中に Authentication オブジェクトを一時的に置き換えることにより、セキュアな呼び出しは、異なる認証および認可資格情報を必要とする他のオブジェクトを呼び出すことができます。また、特定の GrantedAuthority オブジェクトの内部セキュリティチェックを実行することもできます。Spring Security は SecurityContextHolder のコンテンツに基づいてリモートプロトコルを自動的に構成する多くのヘルパークラスを提供するため、これらの run-as 置換はリモート Web サービスを呼び出すときに特に役立ちます

10.19.2. 構成

RunAsManager インターフェースは、Spring Security によって提供されます。

Authentication buildRunAs(Authentication authentication, Object object,
    List<ConfigAttribute> config);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

最初のメソッドは、メソッド呼び出し中に既存の Authentication オブジェクトを置き換える Authentication オブジェクトを返します。メソッドが null を返す場合、置換を行う必要がないことを示します。2 番目の方法は、構成属性の始動検証の一部として AbstractSecurityInterceptor によって使用されます。supports(Class) メソッドは、セキュリティインターセプターの実装によって呼び出され、構成された RunAsManager がセキュリティインターセプターが提示するタイプのセキュアオブジェクトをサポートするようにします。

RunAsManager の具体的な実装の 1 つは、Spring Security で提供されます。ConfigAttribute が RUN_AS_ で始まる場合、RunAsManagerImpl クラスは置換 RunAsUserToken を返します。そのような ConfigAttribute が見つかった場合、置換 RunAsUserToken には、元の Authentication オブジェクトと同じプリンシパル、クレデンシャル、および付与された権限が、各 RUN_AS_ConfigAttribute の新しい SimpleGrantedAuthority とともに含まれます。それぞれの新しい SimpleGrantedAuthority には ROLE_ が接頭辞として付けられ、その後に RUN_ASConfigAttribute が続きます。例: RUN_AS_SERVER は、ROLE_RUN_AS_SERVER に付与された権限を含む置換 RunAsUserToken になります。

置換 RunAsUserToken は、他の Authentication オブジェクトとまったく同じです。おそらく委譲を通して適切な AuthenticationProvider への AuthenticationManager によって認証される必要があります。RunAsImplAuthenticationProvider はそのような認証を実行します。提示された RunAsUserToken を単に有効なものとして受け入れます。

悪意のあるコードが RunAsUserToken を作成せず、RunAsImplAuthenticationProvider が確実に受け入れられるように提示するために、生成されたすべてのトークンにキーのハッシュが保存されます。RunAsManagerImpl と RunAsImplAuthenticationProvider は、同じキーで Bean コンテキストに作成されます。

<bean id="runAsManager"
    class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="my_run_as_password"/>
</bean>

<bean id="runAsAuthenticationProvider"
    class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="my_run_as_password"/>
</bean>

同じキーを使用することにより、各 RunAsUserToken は、承認された RunAsManagerImpl によって作成されたことを検証できます。RunAsUserToken は、セキュリティ上の理由から作成後に不変です

10.20. ログアウトの処理

10.20.1. ログアウト Java 構成

WebSecurityConfigurerAdapter(Javadoc) を使用すると、ログアウト機能が自動的に適用されます。デフォルトでは、URL /logout にアクセスすると、次の方法でユーザーがログアウトされます。

  • HTTP セッションの無効化

  • 構成された RememberMe 認証をクリーンアップする

  • SecurityContextHolder のクリア

  • /login?logout にリダイレクト

ただし、ログイン機能の構成と同様に、ログアウト要件をさらにカスタマイズするためのさまざまなオプションもあります。

protected void configure(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout                                                (1)
            .logoutUrl("/my/logout")                                            (2)
            .logoutSuccessUrl("/my/index")                                      (3)
            .logoutSuccessHandler(logoutSuccessHandler)                         (4)
            .invalidateHttpSession(true)                                        (5)
            .addLogoutHandler(logoutHandler)                                    (6)
            .deleteCookies(cookieNamesToClear)                                  (7)
        )
        ...
}
1 ログアウトのサポートを提供します。WebSecurityConfigurerAdapter を使用する場合、これは自動的に適用されます。
2 ログアウトをトリガーする URL(デフォルトは /logout)。CSRF 保護が有効になっている場合(デフォルト)、リクエストは POST である必要があります。詳細については、JavaDoc(Javadoc) を参照してください。
3 ログアウト後にリダイレクトする URL。デフォルトは /login?logout です。詳細については、JavaDoc(Javadoc) を参照してください。
4 カスタム LogoutSuccessHandler を指定してみましょう。これが指定されている場合、logoutSuccessUrl() は無視されます。詳細については、JavaDoc(Javadoc) を参照してください。
5 ログアウト時に HttpSession を無効にするかどうかを指定します。これはデフォルトでです。カバーに SecurityContextLogoutHandler を構成します。詳細については、JavaDoc(Javadoc) を参照してください。
6LogoutHandler を追加します。SecurityContextLogoutHandler は、デフォルトで最後の LogoutHandler として追加されます。
7 ログアウトの成功時に削除する Cookie の名前を指定できます。これは、CookieClearingLogoutHandler を明示的に追加するためのショートカットです。

もちろん、XML 名前空間表記を使用してログアウトを構成することもできます。詳細については、Spring Security XML 名前空間セクションのログアウト要素のドキュメントを参照してください。

一般に、ログアウト機能をカスタマイズするために、LogoutHandler(Javadoc)  および / または LogoutSuccessHandler(Javadoc)  実装を追加できます。多くの一般的なシナリオでは、これらのハンドラーは、Fluent API を使用するときに隠れて適用されます。

10.20.2. ログアウト XML 設定

logout 要素は、特定の URL にナビゲートすることでログアウトのサポートを追加します。デフォルトのログアウト URL は /logout ですが、logout-url 属性を使用して他の URL に設定できます。他の利用可能な属性の詳細については、ネームスペースの付録を参照してください。

10.20.3. LogoutHandler

一般的に、LogoutHandler(Javadoc)  実装は、ログアウト処理に参加できるクラスを示します。これらは、必要なクリーンアップを実行するために呼び出されることが期待されています。そのため、例外をスローすべきではありません。さまざまな実装が提供されます。

詳細については、Remember-Me インターフェースと実装を参照してください。

LogoutHandler 実装を直接提供する代わりに、流れるような API は、それぞれの LogoutHandler 実装をカバーするショートカットを提供します。例 : deleteCookies() では、ログアウト成功時に削除される 1 つ以上の Cookie の名前を指定できます。これは、CookieClearingLogoutHandler の追加と比較したショートカットです。

10.20.4. LogoutSuccessHandler

LogoutSuccessHandler は、LogoutFilter によるログアウトの成功後に呼び出され、たとえば適切な宛先へのリダイレクトまたは転送。インターフェースは LogoutHandler とほぼ同じですが、例外が発生する可能性があることに注意してください。

次の実装が提供されます。

上記のように、SimpleUrlLogoutSuccessHandler を直接指定する必要はありません。代わりに、流れるような API は logoutSuccessUrl() を設定することによりショートカットを提供します。これにより、SimpleUrlLogoutSuccessHandler がカバーにセットアップされます。指定された URL は、ログアウトが発生した後にリダイレクトされます。デフォルトは /login?logout です。

HttpStatusReturningLogoutSuccessHandler は、REST API タイプのシナリオで興味深い場合があります。ログアウトが成功したときに URL にリダイレクトする代わりに、この LogoutSuccessHandler では、返されるプレーン HTTP ステータスコードを提供できます。設定されていない場合、デフォルトでステータスコード 200 が返されます。

10.20.5. その他のログアウト関連の参照

10.21. 認証イベント

成功または失敗する認証ごとに、それぞれ AuthenticationSuccessEvent または AuthenticationFailureEvent が起動されます。

これらのイベントをリッスンするには、最初に AuthenticationEventPublisher を公開する必要があります。Spring Security の DefaultAuthenticationEventPublisher はおそらくうまくいくでしょう:

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}

次に、Spring の @EventListener サポートを使用できます。

@Component
public class AuthenticationEvents {
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent success) {
        // ...
    }

    @EventListener
    public void onFailure(AuthenticationFailureEvent failures) {
        // ...
    }
}

AuthenticationSuccessHandler および AuthenticationFailureHandler に似ていますが、これらはサーブレット API から独立して使用できるという点で優れています。

10.21.1. 例外マッピングの追加

デフォルトでは、DefaultAuthenticationEventPublisher は次のイベントに対して AuthenticationFailureEvent を公開します。

例外

イベント

BadCredentialsException

AuthenticationFailureBadCredentialsEvent

UsernameNotFoundException

AuthenticationFailureBadCredentialsEvent

AccountExpiredException

AuthenticationFailureExpiredEvent

ProviderNotFoundException

AuthenticationFailureProviderNotFoundEvent

DisabledException

AuthenticationFailureDisabledEvent

LockedException

AuthenticationFailureLockedEvent

AuthenticationServiceException

AuthenticationFailureServiceExceptionEvent

CredentialsExpiredException

AuthenticationFailureCredentialsExpiredEvent

InvalidBearerTokenException

AuthenticationFailureBadCredentialsEvent

パブリッシャーは Exception と完全に一致するため、これらの例外のサブクラスもイベントを生成しません。

そのために、setAdditionalExceptionMappings メソッドを介してパブリッシャーに追加のマッピングを提供することができます。

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    Map<Class<? extends AuthenticationException>,
        Class<? extends AuthenticationFailureEvent>> mapping =
            Collections.singletonMap(FooException.class, FooEvent.class);
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
    return authenticationEventPublisher;
}

10.21.2. デフォルトイベント

また、AuthenticationException の場合に発生するキャッチオールイベントを提供できます。

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setDefaultAuthenticationFailureEvent
        (GenericAuthenticationFailureEvent.class);
    return authenticationEventPublisher;
}

11. 認可

Spring Security の高度な認証機能は、その人気の理由の 1 つです。Spring Security が提供するメカニズムやプロバイダーを使用するか、あるいはコンテナーや他の Spring 以外のセキュリティ認証機関と統合するかに関係なく、アプリケーション内で認可サービスを一貫した簡単な方法で使用することができます。

このパートでは、パート I で紹介されたさまざまな AbstractSecurityInterceptor の実装について説明します。次に、ドメインアクセス制御リストを使用して認可を微調整する方法について説明します。

11.1. 認可アーキテクチャ

11.1.1. 権限

Authentication は、すべての Authentication 実装が GrantedAuthority オブジェクトのリストをどのように保存するかについて説明します。これらは、プリンシパルに付与された権限を表します。GrantedAuthority オブジェクトは AuthenticationManager によって Authentication オブジェクトに挿入され、後で AccessDecisionManager が認可決定を行うときに読み取られます。

GrantedAuthority は、メソッドが 1 つだけのインターフェースです。

String getAuthority();

この方法により、AccessDecisionManager は GrantedAuthority の正確な String 表現を取得できます。表現を String として返すことにより、ほとんどの AccessDecisionManager が GrantedAuthority を簡単に「読み取る」ことができます。GrantedAuthority を String として正確に表すことができない場合、GrantedAuthority は「複雑」と見なされ、getAuthority() は null を返す必要があります。

「複雑な」 GrantedAuthority の例は、異なる顧客アカウント番号に適用される操作と権限のしきい値のリストを格納する実装です。この複雑な GrantedAuthority を String として表現することは非常に難しく、その結果、getAuthority() メソッドは null を返すはずです。これは、AccessDecisionManager の内容を理解するために GrantedAuthority 実装を特にサポートする必要があることを AccessDecisionManager に示します。

Spring Security には、GrantedAuthority の具体的な実装 SimpleGrantedAuthority が含まれています。これにより、ユーザー指定の String を GrantedAuthority に変換できます。セキュリティアーキテクチャに含まれるすべての AuthenticationProvider は、SimpleGrantedAuthority を使用して Authentication オブジェクトに入力します。

11.1.2. 呼び出し前の処理

Spring Security は、メソッド呼び出しや Web リクエストなどのセキュアオブジェクトへのアクセスを制御するインターセプターを提供します。呼び出しの続行を許可するかどうかの呼び出し前の決定は、AccessDecisionManager によって行われます。

AccessDecisionManager

AccessDecisionManager は AbstractSecurityInterceptor によって呼び出され、最終的なアクセス制御の決定を行います。AccessDecisionManager インターフェースには 3 つのメソッドが含まれています。

void decide(Authentication authentication, Object secureObject,
    Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

AccessDecisionManager の decide メソッドには、認可決定を行うために必要なすべての関連情報が渡されます。特に、セキュア Object を渡すと、実際のセキュアオブジェクト呼び出しに含まれる引数をインスペクションできます。例:セキュアなオブジェクトが MethodInvocation であると仮定しましょう。Customer 引数について MethodInvocation を照会し、AccessDecisionManager に何らかのセキュリティロジックを実装して、プリンシパルがその顧客での操作を認可されていることを確認するのは簡単です。アクセスが拒否された場合、実装は AccessDeniedException をスローすることが期待されています。

supports(ConfigAttribute) メソッドは、AccessDecisionManager が渡された ConfigAttribute を処理できるかどうかを判別するために、起動時に AbstractSecurityInterceptor によって呼び出されます。supports(Class) メソッドは、セキュリティインターセプターの実装によって呼び出され、構成された AccessDecisionManager がセキュリティインターセプターが提示するタイプのセキュアオブジェクトをサポートするようにします。

投票ベースの AccessDecisionManager 実装

ユーザーは独自の AccessDecisionManager を実装して認証のすべての側面を制御できますが、Spring Security には投票に基づくいくつかの AccessDecisionManager 実装が含まれています。投票決定マネージャーは、関連するクラスを示しています。

access decision voting
図 11: 投票決定マネージャー

このアプローチを使用して、一連の AccessDecisionVoter 実装が認可決定でポーリングされます。AccessDecisionManager は、投票の評価に基づいて AccessDeniedException をスローするかどうかを決定します。

AccessDecisionVoter インターフェースには 3 つのメソッドがあります。

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

具体的な実装は int を返します。可能な値は AccessDecisionVoter 静的フィールド ACCESS_ABSTAINACCESS_DENIED および ACCESS_GRANTED に反映されます。投票の実装は、認可の決定について意見がない場合、ACCESS_ABSTAIN を返します。意見がある場合は、ACCESS_DENIED または ACCESS_GRANTED を返す必要があります。

Spring Security には投票を集計する 3 つの具体的な AccessDecisionManager があります。ConsensusBased 実装は、非棄権投票のコンセンサスに基づいてアクセスを許可または拒否します。投票が等しい場合、またはすべての投票が棄権された場合の動作を制御するプロパティが提供されます。AffirmativeBased 実装は、1 つ以上の ACCESS_GRANTED 投票が受信された場合にアクセスを許可します(つまり、少なくとも 1 つの許可投票があった場合、拒否投票は無視されます)。ConsensusBased 実装のように、すべての投票者が棄権した場合の動作を制御するパラメーターがあります。UnanimousBased プロバイダーは、棄権を無視して、アクセスを許可するために満場一致の ACCESS_GRANTED 投票を期待しています。ACCESS_DENIED 投票がある場合、アクセスを拒否します。他の実装と同様に、すべての投票者が棄権した場合の動作を制御するパラメーターがあります。

投票を異なる方法で集計するカスタム AccessDecisionManager を実装することができます。例:特定の AccessDecisionVoter からの投票には追加の重み付けが適用される場合がありますが、特定の投票者からの拒否投票には拒否権が付与される場合があります。

RoleVoter

Spring Security で提供される最も一般的に使用される AccessDecisionVoter は、単純な RoleVoter です。これは、構成属性を単純なロール名として扱い、ユーザーにそのロールが割り当てられている場合にアクセス権を付与します。

ConfigAttribute が接頭辞 ROLE_ で始まる場合に投票します。ROLE_ で始まる 1 つ以上の ConfigAttributes と正確に等しい(getAuthority() メソッドを介して) String 表現を返す GrantedAuthority がある場合、アクセスを許可するために投票します。ROLE_ で始まる ConfigAttribute の正確な一致がない場合、RoleVoter はアクセスを拒否するために投票します。ConfigAttribute が ROLE_ で始まっていない場合、投票者は棄権します。

AuthenticatedVoter

暗黙的に確認したもう 1 つの投票者は AuthenticatedVoter です。これは、匿名ユーザー、完全認証ユーザー、記憶ユーザー認証ユーザーを区別するために使用できます。多くのサイトでは、remember-me 認証で特定の制限付きアクセスが許可されていますが、完全なアクセスのためにログインすることでユーザーに ID の確認を要求します。

属性 IS_AUTHENTICATED_ANONYMOUSLY を使用して匿名アクセスを許可したとき、この属性は AuthenticatedVoter によって処理されていました。詳細については、このクラスの Javadoc を参照してください。

カスタム投票者

当然、カスタム AccessDecisionVoter を実装することもでき、必要なほぼすべてのアクセス制御ロジックを配置できます。アプリケーション固有のもの(ビジネスロジック関連)か、セキュリティ管理ロジックを実装している場合があります。例:Spring の Web サイトには、投票者を使用して、アカウントが停止されたユーザーへのアクセスをリアルタイムで拒否する方法を説明したブログ記事 (英語) があります。

11.1.3. 呼び出し処理後

AccessDecisionManager は、セキュアオブジェクトの呼び出しを続行する前に AbstractSecurityInterceptor によって呼び出されますが、一部のアプリケーションでは、セキュアオブジェクトの呼び出しによって実際に返されるオブジェクトを変更する方法が必要です。これを実現するために独自の AOP 懸念事項を簡単に実装できますが、Spring Security は ACL 機能と統合するいくつかの具体的な実装を持つ便利なフックを提供します。

呼び出し実装後は、Spring Security の AfterInvocationManager とその具体的な実装を示しています。

after invocation
図 12: 呼び出し実装後

Spring Security の他の多くの部分と同様に、AfterInvocationManager には、AfterInvocationProvider のリストをポーリングする単一の具象実装 AfterInvocationProviderManager があります。各 AfterInvocationProvider は、戻りオブジェクトを変更するか、AccessDeniedException をスローできます。実際、前のプロバイダーの結果がリスト内の次のプロバイダーに渡されるため、複数のプロバイダーがオブジェクトを変更できます。

AfterInvocationManager を使用している場合は、MethodSecurityInterceptor の AccessDecisionManager が操作を許可できるようにする構成属性が引き続き必要です。一般的な Spring Security に含まれる AccessDecisionManager 実装を使用している場合、特定のセキュアなメソッド呼び出しに定義された構成属性がないと、各 AccessDecisionVoter は投票を控えます。次に、AccessDecisionManager プロパティ「allowIfAllAbstainDecisions」が false の場合、AccessDeniedException がスローされます。この潜在的な課題を回避するには、(i)「allowIfAllAbstainDecisions」を true に設定するか(これは一般的に推奨されません)、または(ii) AccessDecisionVoter がアクセスを許可するために投票する構成属性が少なくとも 1 つあることを確認します。この後者の(推奨)アプローチは、通常 ROLE_USER または ROLE_AUTHENTICATED 構成属性によって実現されます。

11.1.4. 階層的なロール

アプリケーションの特定のロールが他のロールを自動的に「含める」ことが一般的な要件です。例:「管理者」と「ユーザー」のロールの概念を持つアプリケーションでは、管理者が通常のユーザーができることをすべて行えるようにしたい場合があります。これを実現するには、すべての管理ユーザーにも「ユーザー」ロールが割り当てられていることを確認します。または、「user」ロールに「admin」ロールも含める必要があるすべてのアクセス制約を変更できます。アプリケーションにさまざまなロールがある場合、これは非常に複雑になる可能性があります。

ロール階層を使用すると、他のロール(または権限)を含めるロールを構成できます。Spring Security の RoleVoter の拡張バージョンである RoleHierarchyVoter は RoleHierarchy で構成され、そこからユーザーが割り当てられているすべての「到達可能な権限」を取得します。典型的な構成は次のようになります。

<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
    <constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
    <property name="hierarchy">
        <value>
            ROLE_ADMIN > ROLE_STAFF
            ROLE_STAFF > ROLE_USER
            ROLE_USER > ROLE_GUEST
        </value>
    </property>
</bean>

ここでは、階層 ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST に 4 つのロールがあります。ROLE_ADMIN で認証されたユーザーは、上記の RoleHierarchyVoter で構成された AccessDecisionManager に対してセキュリティ制約が評価された場合、4 つのロールすべてを持っているかのように動作します。> シンボルは、「含む」という意味と考えることができます。

ロール階層は、アプリケーションのアクセス制御構成データを簡素化したり、ユーザーに割り当てる必要のある権限の数を減らしたりする便利な手段を提供します。より複雑な要件については、アプリケーションに必要な特定のアクセス権とユーザーに割り当てられているロール間の論理マッピングを定義し、ユーザー情報をロードするときに 2 つを変換することができます。

11.2. FilterSecurityInterceptor で HttpServletRequest を認証する

このセクションは、サーブレットベースのアプリケーション内での認可の仕組みをさらに掘り下げることにより、サーブレットのアーキテクチャと実装に基づいています。

FilterSecurityInterceptor(Javadoc) は、HttpServletRequest認可を提供します。セキュリティフィルターの 1 つとして FilterChainProxy に挿入されます。

filtersecurityinterceptor
図 13: HttpServletRequest を認証する
  •  番号 1 まず、FilterSecurityInterceptor は SecurityContextHolder から認証を取得します。

  • 2 番 次に、FilterSecurityInterceptor は HttpServletRequestHttpServletResponse から FilterInvocation(Javadoc) を作成し、FilterChain は FilterSecurityInterceptor に渡されます。

  •  番号 3 次に、FilterInvocation を SecurityMetadataSource に渡して ConfigAttribute を取得します。

  •  番号 4 最後に、AuthenticationFilterInvocation および ConfigAttribute を AccessDecisionManager に渡します。

    • 5 番 認可が拒否されると、AccessDeniedException がスローされます。この場合、ExceptionTranslationFilter は AccessDeniedException を処理します。

    • 6 番 アクセスが許可されると、FilterSecurityInterceptor は FilterChain を続行し、アプリケーションが正常に処理できるようにします。

デフォルトでは、Spring Security の認可にはすべてのリクエストの認証が必要です。明示的な構成は次のようになります。

例 78: すべてのリクエストは認証される必要があります
Java
protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        );
}
XML
<http>
    <!-- ... -->
    <intercept-url pattern="/**" access="authenticated"/>
</http>
Kotlin
fun configure(http: HttpSecurity) {
    http {
        // ...
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
    }
}

Spring Security を構成して、優先順位の高いルールを追加することで、異なるルールを設定できます。

例 79: リクエストを承認する
Java
protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize                                  (1)
            .mvcMatchers("/resources/**", "/signup", "/about").permitAll()         (2)
            .mvcMatchers("/admin/**").hasRole("ADMIN")                             (3)
            .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")   (4)
            .anyRequest().denyAll()                                                (5)
        );
}
XML
<http> (1)
    <!-- ... -->
    (2)
    <intercept-url pattern="/resources/**" access="permitAll"/>
    <intercept-url pattern="/signup" access="permitAll"/>
    <intercept-url pattern="/about" access="permitAll"/>

    <intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> (3)
    <intercept-url pattern="/db/**" access="hasRole('ADMIN') and hasRole('DBA')"/> (4)
    <intercept-url pattern="/**" access="denyAll"/> (5)
</http>
Kotlin
fun configure(http: HttpSecurity) {
   http {
        authorizeRequests { (1)
            authorize("/resources/**", permitAll) (2)
            authorize("/signup", permitAll)
            authorize("/about", permitAll)

            authorize("/admin/**", hasRole("ADMIN")) (3)
            authorize("/db/**", "hasRole('ADMIN') and hasRole('DBA')") (4)
            authorize(anyRequest, denyAll) (5)
        }
    }
}
1 複数の認可ルールが指定されています。各ルールは、宣言された順序で考慮されます。
2 すべてのユーザーがアクセスできる複数の URL パターンを指定しました。具体的には、URL が「/resources/」で始まる、「/signup」に等しい、または「/about」に等しい場合、どのユーザーもリクエストにアクセスできます。
3「/admin/」で始まる URL は、ロール「ROLE_ADMIN」を持つユーザーに制限されます。hasRole メソッドを呼び出しているため、「ROLE_」プレフィックスを指定する必要はありません。
4「/db/」で始まる URL では、ユーザーに「ROLE_ADMIN」と「ROLE_DBA」の両方が必要です。hasRole 式を使用しているため、「ROLE_」プレフィックスを指定する必要はありません。
5 まだ一致していない URL はアクセスを拒否されます。これは、認可規則の更新を誤って忘れたくない場合に適した戦略です。

11.3. 式ベースのアクセス制御

Spring Security 3.0 は、以前に見られた構成属性およびアクセス決定投票者の単純な使用に加えて、Spring EL 式を認可メカニズムとして使用する機能を導入しました。式ベースのアクセス制御は同じアーキテクチャに基づいて構築されていますが、複雑なブールロジックを単一の式にカプセル化できます。

11.3.1. 概要

Spring Security は、式のサポートに Spring EL を使用します。トピックをより深く理解することに興味がある場合は、それがどのように機能するかを確認してください。式は、評価コンテキストの一部として「ルートオブジェクト」で評価されます。Spring Security は、組み込み式と現在のプリンシパルなどの値へのアクセスを提供するために、Web およびメソッドセキュリティの特定のクラスをルートオブジェクトとして使用します。

一般的な組み込み式

式ルートオブジェクトの基本クラスは SecurityExpressionRoot です。これにより、Web セキュリティとメソッドセキュリティの両方で使用できる一般的な式が提供されます。

表 1: 一般的な組み込み式
説明

hasRole(String role)

現在のプリンシパルに指定されたロールがある場合、true を返します。

例: hasRole('admin')

デフォルトでは、指定されたロールが「ROLE_」で始まらない場合は追加されます。これは、DefaultWebSecurityExpressionHandler の defaultRolePrefix を変更することによりカスタマイズできます。

hasAnyRole(String…​ roles)

現在のプリンシパルが提供されたロールのいずれかを持っている場合(文字列のコンマ区切りリストとして与えられた場合) true を返します。

例: hasAnyRole('admin', 'user')

デフォルトでは、指定されたロールが「ROLE_」で始まらない場合は追加されます。これは、DefaultWebSecurityExpressionHandler の defaultRolePrefix を変更することによりカスタマイズできます。

hasAuthority(String authority)

現在のプリンシパルが指定された権限を持っている場合、true を返します。

例: hasAuthority('read')

hasAnyAuthority(String…​ authorities)

現在のプリンシパルに提供された権限のいずれかがある場合、true を返します (文字列のコンマ区切りリストとして与えられます)

例: hasAnyAuthority('read', 'write')

principal

現在のユーザーを表すプリンシパルオブジェクトへの直接アクセスを許可する

authentication

SecurityContext から取得した現在の Authentication オブジェクトへの直接アクセスを許可する

permitAll

常に true に評価されます

denyAll

常に false に評価されます

isAnonymous()

現在のプリンシパルが匿名ユーザーの場合、true を返します

isRememberMe()

現在のプリンシパルが remember-me ユーザーである場合、true を返します

isAuthenticated()

ユーザーが匿名でない場合、true を返します

isFullyAuthenticated()

ユーザーが匿名ユーザーでも覚えのないユーザーでもない場合は、true を返します。

hasPermission(Object target, Object permission)

ユーザーが指定された許可に対して提供されたターゲットにアクセスできる場合、true を返します。例: hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

ユーザーが指定された許可に対して提供されたターゲットにアクセスできる場合、true を返します。例: hasPermission(1, 'com.example.domain.Message', 'read')

11.3.2. Web セキュリティ式

式を使用して個々の URL を保護するには、最初に <http> 要素の use-expressions 属性を true に設定する必要があります。Spring Security は、<intercept-url> 要素の access 属性に Spring EL 式が含まれることを期待します。式はブール値に評価され、アクセスを許可するかどうかを定義する必要があります。例:

<http>
    <intercept-url pattern="/admin*"
        access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
    ...
</http>

ここで、アプリケーションの「admin」領域(URL パターンで定義)は、付与された権限「admin」を持ち、IP アドレスがローカルサブネットと一致するユーザーのみが使用できるように定義しました。前のセクションでビルトイン hasRole 式を見てきました。式 hasIpAddress は、Web セキュリティに固有の追加の組み込み式です。これは、WebSecurityExpressionRoot クラスによって定義され、そのインスタンスは、Web アクセス式を評価するときに式ルートオブジェクトとして使用されます。このオブジェクトは、request という名前で HttpServletRequest オブジェクトを直接公開しているため、式でリクエストを直接呼び出すことができます。式が使用されている場合、WebExpressionVoter がネームスペースで使用される AccessDecisionManager に追加されます。そのため、ネームスペースを使用しておらず、式を使用する場合は、これらのいずれかを構成に追加する必要があります。

Web セキュリティ式で Bean を参照する

使用可能な式を継承したい場合は、公開する Spring Bean を簡単に参照できます。例:次のメソッドシグネチャーを含む webSecurity という名前の Bean があると仮定します。

public class WebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
                ...
        }
}

以下を使用してメソッドを参照できます。

<http>
    <intercept-url pattern="/user/**"
        access="@webSecurity.check(authentication,request)"/>
    ...
</http>

または Java 構成

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
        ...
    )
Web セキュリティ式のパス変数

URL 内のパス変数を参照できると便利な場合があります。例: /user/{userId} 形式の URL パスから id でユーザーを検索する RESTful アプリケーションを検討します。

パターンに配置することで、パス変数を簡単に参照できます。例:次のメソッドシグネチャーを含む webSecurity という名前の Bean がある場合:

public class WebSecurity {
        public boolean checkUserId(Authentication authentication, int id) {
                ...
        }
}

以下を使用してメソッドを参照できます。

<http>
    <intercept-url pattern="/user/{userId}/**"
        access="@webSecurity.checkUserId(authentication,#userId)"/>
    ...
</http>

または Java 構成

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
        ...
    );

どちらの構成でも、一致する URL はパス変数を渡して(そして変換して)checkUserId メソッドに渡します。例:URL が /user/123/resource の場合、渡される ID は 123 になります。

11.3.3. メソッドセキュリティ式

メソッドのセキュリティは、単純な許可または拒否ルールよりも少し複雑です。Spring Security 3.0 は、式の使用を包括的にサポートできるようにするために、いくつかの新しいアノテーションを導入しました。

@Pre および @Post アノテーション

式属性をサポートする 4 つのアノテーションがあり、呼び出し前および呼び出し後の認可チェックを認可し、送信されたコレクション引数または戻り値のフィルタリングもサポートします。それらは @PreAuthorize@PreFilter@PostAuthorize と @PostFilter です。それらの使用は、global-method-security 名前空間要素によって有効になります。

<global-method-security pre-post-annotations="enabled"/>
@PreAuthorize および @PostAuthorize を使用したアクセス制御

最も明らかに有用なアノテーションは、メソッドを実際に呼び出すことができるかどうかを決定する @PreAuthorize です。たとえば (「連絡先」サンプルアプリケーションから)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

つまり、アクセスはロール「ROLE_USER」を持つユーザーにのみ許可されます。明らかに、従来の構成と必要なロールの単純な構成属性を使用して、同じことを簡単に実現できます。しかし、どうですか:

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

ここでは、式の一部としてメソッド引数を実際に使用して、現在のユーザーに特定の連絡先の「管理者」権限があるかどうかを判断しています。以下に示すように、ビルトイン hasPermission() 式は、アプリケーションコンテキストを介して Spring Security ACL モジュールにリンクされます。式の変数として名前でメソッドの引数にアクセスできます。

Spring Security がメソッドの引数を解決できる方法はいくつかあります。Spring Security は、DefaultSecurityParameterNameDiscoverer を使用してパラメーター名を検出します。デフォルトでは、メソッド全体に対して次のオプションが試行されます。

  • Spring Security の @P アノテーションがメソッドへの単一の引数に存在する場合、値が使用されます。これは、パラメーター名に関する情報を含まない JDK 8 より前の JDK でコンパイルされたインターフェースに役立ちます。例:

    import org.springframework.security.access.method.P;
    
    ...
    
    @PreAuthorize("#c.name == authentication.name")
    public void doSomething(@P("c") Contact contact);

    バックグラウンドでは、AnnotationParameterNameDiscoverer を使用してこの使用を実装し、指定されたアノテーションの value 属性をサポートするようにカスタマイズできます。

  • Spring Data の @Param アノテーションがメソッドの少なくとも 1 つのパラメーターに存在する場合、値が使用されます。これは、パラメーター名に関する情報を含まない JDK 8 より前の JDK でコンパイルされたインターフェースに役立ちます。例:

    import org.springframework.data.repository.query.Param;
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);

    バックグラウンドでは、AnnotationParameterNameDiscoverer を使用してこの使用を実装し、指定されたアノテーションの value 属性をサポートするようにカスタマイズできます。

  • -parameters 引数を使用してソースをコンパイルするために JDK 8 が使用され、Spring 4+ が使用されている場合、標準の JDK リフレクション API を使用してパラメーター名が検出されます。これはクラスとインターフェースの両方で機能します。

  • 最後に、コードがデバッグシンボルを使用してコンパイルされた場合、パラメーター名はデバッグシンボルを使用して検出されます。インターフェースにはパラメーター名に関するデバッグ情報がないため、これは機能しません。インターフェースには、アノテーションまたは JDK 8 アプローチを使用する必要があります。

式内で任意の Spring-EL 機能を使用できるため、引数のプロパティにアクセスすることもできます。例:連絡先のユーザー名と一致するユーザー名を持つユーザーのみにアクセスを許可する特定の方法が必要な場合は、次のように記述できます。

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

ここでは、セキュリティコンテキストに格納されている Authentication である別の組み込み式 authentication にアクセスしています。式 principal を使用して、その「プリンシパル」プロパティに直接アクセスすることもできます。多くの場合、値は UserDetails インスタンスであるため、principal.username や principal.enabled などの式を使用できます。

通常、Less では、メソッドが呼び出された後にアクセス制御チェックを実行することができます。これは、@PostAuthorize アノテーションを使用して実現できます。メソッドからの戻り値にアクセスするには、式で組み込み名 returnObject を使用します。

@PreFilter および @PostFilter を使用したフィルタリング

既にご存じかもしれませんが、Spring Security はコレクションと配列のフィルタリングをサポートしており、式を使用してこれを実現できるようになりました。これは、メソッドの戻り値で最も一般的に実行されます。例:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();

@PostFilter アノテーションを使用する場合、Spring Security は返されたコレクションを反復処理し、指定された式が false である要素を削除します。filterObject という名前は、コレクション内の現在のオブジェクトを指します。@PreFilter を使用して、メソッド呼び出しの前にフィルタリングすることもできますが、これはあまり一般的な要件ではありません。構文はまったく同じですが、コレクションタイプである引数が複数ある場合は、このアノテーションの filterTarget プロパティを使用して名前で 1 つ選択する必要があります。

フィルタリングは、データ取得クエリをチューニングするための代替ではないことに注意してください。大規模なコレクションをフィルタリングし、多くのエントリを削除する場合、これは非効率的である可能性があります。

組み込み式

メソッドのセキュリティに固有の組み込み式がいくつかありますが、これは既に上記で使用されているものです。filterTarget と returnValue の値は非常に単純ですが、hasPermission() 式を使用することで、より詳細に見ることができます。

PermissionEvaluator インターフェース

hasPermission() 式は PermissionEvaluator のインスタンスに委譲されます。式システムと Spring Security の ACL システムを橋渡しすることを目的としており、抽象的な認可に基づいて、ドメインオブジェクトの認可制約を指定できます。ACL モジュールへの明示的な依存関係はないため、必要に応じて別の実装に交換できます。インターフェースには 2 つのメソッドがあります。

boolean hasPermission(Authentication authentication, Object targetDomainObject,
                            Object permission);

boolean hasPermission(Authentication authentication, Serializable targetId,
                            String targetType, Object permission);

最初の引数(Authentication オブジェクト)が指定されていないことを除いて、使用可能な式のバージョンに直接マップします。1 つ目は、アクセスが制御されているドメインオブジェクトが既にロードされている状況で使用されます。次に、現在のユーザーがそのオブジェクトに対して与えられた許可を持っている場合、式は true を返します。2 番目のバージョンは、オブジェクトがロードされていないが、その識別子がわかっている場合に使用されます。ドメインオブジェクトの抽象的な「タイプ」指定子も必要です。これにより、正しい ACL 権限をロードできます。これは伝統的にオブジェクトの Java クラスでしたが、パーミッションのロードメソッドと一貫している限り、そうである必要はありません。

hasPermission() 式を使用するには、アプリケーションコンテキストで PermissionEvaluator を明示的に構成する必要があります。これは次のようになります。

<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler ref="expressionHandler"/>
</security:global-method-security>

<bean id="expressionHandler" class=
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
    <property name="permissionEvaluator" ref="myPermissionEvaluator"/>
</bean>

myPermissionEvaluator は、PermissionEvaluator を実装する Bean です。通常、これは AclPermissionEvaluator と呼ばれる ACL モジュールからの実装になります。詳細については、「連絡先」サンプルアプリケーションの構成を参照してください。

メソッドセキュリティメタアノテーション

メソッドのセキュリティのためにメタアノテーションを使用して、コードを読みやすくすることができます。これは、コードベース全体で同じ複雑な式を繰り返している場合に特に便利です。例:次のことを考慮してください。

@PreAuthorize("#contact.name == authentication.name")

これをどこでも繰り返す代わりに、代わりに使用できるメタアノテーションを作成できます。

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

メタアノテーションは、Spring Security メソッドのセキュリティアノテーションのいずれにも使用できます。仕様への準拠を維持するため、JSR-250 アノテーションはメタアノテーションをサポートしていません。

11.4. セキュアなオブジェクトの実装

11.4.1. AOP Alliance(MethodInvocation)セキュリティインターセプター

Spring Security 2.0, を固定する前は、MethodInvocation にはかなり多くのボイラープレート構成が必要でした。現在、メソッドセキュリティの推奨アプローチは、名前空間構成を使用することです。この方法では、メソッドセキュリティインフラストラクチャ Bean が自動的に構成されるため、実装クラスについて知る必要はありません。ここでは、関係するクラスの概要を簡単に説明します。

メソッドのセキュリティは、MethodInvocation を保護する MethodSecurityInterceptor を使用して実施されます。構成アプローチによっては、インターセプターは単一の Bean に固有であるか、複数の Bean 間で共有される場合があります。インターセプターは、MethodSecurityMetadataSource インスタンスを使用して、特定のメソッド呼び出しに適用される構成属性を取得します。MapBasedMethodSecurityMetadataSource は、メソッド名(ワイルドカードを使用可能)をキーとする構成属性を格納するために使用され、<intercept-methods> または <protect-point> 要素を使用してアプリケーションコンテキストで属性が定義されるときに内部的に使用されます。他の実装を使用して、アノテーションベースの構成を処理します。

明示的な MethodSecurityInterceptor 設定

もちろん、Spring AOP のプロキシメカニズムの 1 つで使用するために、アプリケーションコンテキストで MethodSecurityInterceptor を直接構成できます。

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

11.4.2. AspectJ(JoinPoint)セキュリティインターセプター

AspectJ セキュリティインターセプターは、前のセクションで説明した AOP Alliance セキュリティインターセプターと非常によく似ています。実際、このセクションの違いについてのみ説明します。

AspectJ インターセプターの名前は AspectJSecurityInterceptor です。Spring アプリケーションコンテキストに依存してプロキシ経由でセキュリティインターセプターを織り込む AOP Alliance セキュリティインターセプターとは異なり、AspectJSecurityInterceptor は AspectJ コンパイラー経由で織り込まれています。同じアプリケーションで両方のタイプのセキュリティインターセプターを使用することは珍しくありません。AspectJSecurityInterceptor はドメインオブジェクトインスタンスのセキュリティに使用され、AOP Alliance MethodSecurityInterceptor はサービスレイヤーのセキュリティに使用されます。

まず、AspectJSecurityInterceptor が Spring アプリケーションコンテキストでどのように構成されているかを考えてみましょう。

<bean id="bankManagerSecurity" class=
    "org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
    <sec:method-security-metadata-source>
    <sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
    <sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
    </sec:method-security-metadata-source>
</property>
</bean>

ご覧のとおり、クラス名を除き、AspectJSecurityInterceptor は AOP Alliance のセキュリティインターセプターとまったく同じです。実際、SecurityMetadataSource は AOP ライブラリ固有のクラスではなく java.lang.reflect.Method で動作するため、2 つのインターセプターは同じ securityMetadataSource を共有できます。もちろん、アクセスの決定は、関連する AOP ライブラリ固有の呼び出し(つまり MethodInvocation または JoinPoint)にアクセスできるため、アクセスの決定(メソッドの引数など)を行う際に追加の条件の範囲を考慮することができます。

次に、AspectJ aspect を定義する必要があります。例:

package org.springframework.security.samples.aspectj;

import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;

public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {

    private AspectJSecurityInterceptor securityInterceptor;

    pointcut domainObjectInstanceExecution(): target(PersistableEntity)
        && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);

    Object around(): domainObjectInstanceExecution() {
        if (this.securityInterceptor == null) {
            return proceed();
        }

        AspectJCallback callback = new AspectJCallback() {
            public Object proceedWithObject() {
                return proceed();
            }
        };

        return this.securityInterceptor.invoke(thisJoinPoint, callback);
    }

    public AspectJSecurityInterceptor getSecurityInterceptor() {
        return securityInterceptor;
    }

    public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
        this.securityInterceptor = securityInterceptor;
    }

    public void afterPropertiesSet() throws Exception {
        if (this.securityInterceptor == null)
            throw new IllegalArgumentException("securityInterceptor required");
        }
    }
}

上記の例では、セキュリティインターセプターは PersistableEntity のすべてのインスタンスに適用されますが、これは示されていない抽象クラスです(他のクラスまたは pointcut 式を使用できます)。proceed(); ステートメントは around() 本体内でのみ特別な意味を持つため、好奇心の強い人には AspectJCallback が必要です。AspectJSecurityInterceptor は、ターゲットオブジェクトを継続させたいときに、この匿名 AspectJCallback クラスを呼び出します。

アスペクトをロードして AspectJSecurityInterceptor に接続するように Spring を構成する必要があります。これを実現する Bean 宣言を以下に示します。

<bean id="domainObjectInstanceSecurityAspect"
    class="security.samples.aspectj.DomainObjectInstanceSecurityAspect"
    factory-method="aspectOf">
<property name="securityInterceptor" ref="bankManagerSecurity"/>
</bean>

以上です!これで、適切と思われる手段(new Person(); など)を使用して、アプリケーション内のどこからでも Bean を作成でき、セキュリティインターセプターが適用されます。

11.5. メソッドのセキュリティ

バージョン 2.0 以降、Spring Security では、サービスレイヤーメソッドにセキュリティを追加するためのサポートが大幅に改善されています。フレームワークの元の @Secured アノテーションだけでなく、JSR-250 アノテーションセキュリティもサポートしています。3.0 から、新しい式ベースのアノテーションを利用することもできます。intercept-methods 要素を使用して Bean 宣言を修飾することにより、単一の Bean にセキュリティを適用できます。または、AspectJ スタイルのポイントカットを使用して、サービス層全体にわたって複数の Bean を保護できます。

11.5.1. EnableGlobalMethodSecurity

@Configuration インスタンスで @EnableGlobalMethodSecurity アノテーションを使用して、アノテーションベースのセキュリティを有効にできます。例:以下は、Spring Security の @Secured アノテーションを有効にします。

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

(クラスまたはインターフェース上の)メソッドにアノテーションを追加すると、それに応じてそのメソッドへのアクセスが制限されます。Spring Security のネイティブアノテーションサポートは、メソッドの属性セットを定義します。これらは、AccessDecisionManager に渡され、実際の決定が行われます。

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

JSR-250 アノテーションのサポートは、次を使用して有効にできます。

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

これらは標準ベースであり、単純なロールベースの制約を適用できますが、Spring Security のネイティブアノテーションはありません。新しい式ベースの構文を使用するには、次を使用する

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

同等の Java コードは

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

11.5.2. GlobalMethodSecurityConfiguration

@EnableGlobalMethodSecurity アノテーションで許可されるよりも複雑な操作を実行する必要がある場合があります。これらのインスタンスでは、GlobalMethodSecurityConfiguration を継承して、@EnableGlobalMethodSecurity アノテーションがサブクラスに存在することを確認できます。例:カスタム MethodSecurityExpressionHandler を提供する場合、次の構成を使用できます。

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        // ... create and return custom MethodSecurityExpressionHandler ...
        return expressionHandler;
    }
}

オーバーライドできるメソッドの詳細については、GlobalMethodSecurityConfiguration Javadoc を参照してください。

11.5.3. <global-method-security> 要素

この要素は、(要素に適切な属性を設定することにより)アプリケーションでアノテーションベースのセキュリティを有効にし、アプリケーションコンテキスト全体に適用されるセキュリティポイントカット宣言をグループ化するために使用されます。<global-method-security> 要素を 1 つだけ宣言する必要があります。次の宣言により、Spring Security の @Secured のサポートが有効になります。

<global-method-security secured-annotations="enabled" />

(クラスまたはインターフェース上の)メソッドにアノテーションを追加すると、それに応じてそのメソッドへのアクセスが制限されます。Spring Security のネイティブアノテーションサポートは、メソッドの属性セットを定義します。これらは、実際の決定を行うために AccessDecisionManager に渡されます。

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

JSR-250 アノテーションのサポートは、次を使用して有効にできます。

<global-method-security jsr250-annotations="enabled" />

これらは標準ベースであり、単純なロールベースの制約を適用できますが、Spring Security のネイティブアノテーションはありません。新しい式ベースの構文を使用するには、次を使用する

<global-method-security pre-post-annotations="enabled" />

同等の Java コードは

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

式ベースのアノテーションは、ユーザーの権限リストに対するロール名のチェックにとどまらない単純なルールを定義する必要がある場合に適しています。

アノテーション付きメソッドは、Spring Bean として定義されているインスタンスに対してのみ保護されます(メソッドセキュリティが有効になっている同じアプリケーションコンテキスト内)。Spring によって作成されていないインスタンスを保護する場合(たとえば、new オペレーターを使用)、AspectJ を使用する必要があります。

同じアプリケーションで複数のタイプのアノテーションを有効にできますが、他の方法では動作が適切に定義されないため、インターフェースまたはクラスには 1 つのタイプのみを使用する必要があります。特定のメソッドに適用される 2 つのアノテーションが見つかった場合、そのうちの 1 つだけが適用されます。

11.5.4. protect-pointcut を使用したセキュリティポイントカットの追加

protect-pointcut の使用は、単純な宣言のみで多くの Bean にセキュリティを適用できるため、特に強力です。次の例を考えてみましょう。

<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
    access="ROLE_USER"/>
</global-method-security>

これにより、クラスが com.mycompany パッケージにあり、クラス名が「Service」で終わるアプリケーションコンテキストで宣言された Bean のすべてのメソッドが保護されます。ROLE_USER ロールを持つユーザーのみがこれらのメソッドを呼び出すことができます。URL マッチングと同様に、最初のマッチング表現が使用されるため、ポイントカットのリストで最も具体的な一致が最初に来る必要があります。セキュリティアノテーションはポイントカットよりも優先されます。

11.6. ドメインオブジェクトセキュリティ (ACL)

11.6.1. 概要

複雑なアプリケーションでは、多くの場合、単純に Web リクエストまたはメソッド呼び出しレベルではなく、アクセス認可を定義する必要があります。代わりに、セキュリティの決定には、誰(Authentication)、どこ(MethodInvocation)および何(SomeDomainObject)の両方を含める必要があります。言い換えると、認可の決定では、メソッド呼び出しの実際のドメインオブジェクトインスタンスサブジェクトも考慮する必要があります。

ペットクリニック用のアプリケーションを設計しているとします。Spring ベースのアプリケーションのユーザーには、ペットクリニックのスタッフとペットクリニックの顧客の 2 つのメイングループがあります。スタッフはすべてのデータにアクセスできますが、顧客は自分の顧客レコードのみを見ることができます。もう少し面白くするために、顧客は、「子犬幼稚園」のメンターやローカル「ポニークラブ」の社長など、他のユーザーに顧客レコードの閲覧を許可できます。Spring Security を基盤として使用すると、使用できるアプローチがいくつかあります。

  • セキュリティを強化するためのビジネスメソッドを記述します。Customer ドメインオブジェクトインスタンス内のコレクションを参照して、アクセスできるユーザーを判断できます。SecurityContextHolder.getContext().getAuthentication() を使用すると、Authentication オブジェクトにアクセスできます。

  • AccessDecisionVoter を記述して、Authentication オブジェクトに格納されている GrantedAuthority[] からセキュリティを強化します。これは、AuthenticationManager が、プリンシパルがアクセスできる Customer ドメインオブジェクトインスタンスのそれぞれを表すカスタム GrantedAuthority[] を Authentication に取り込む必要があることを意味します。

  • AccessDecisionVoter を記述してセキュリティを強化し、ターゲット Customer ドメインオブジェクトを直接開きます。これは、投票者が Customer オブジェクトを取得できる DAO にアクセスする必要があることを意味します。次に、Customer オブジェクトの承認済みユーザーのコレクションにアクセスし、適切な決定を下します。

これらのアプローチはいずれも完全に合法です。ただし、最初のステップでは、認可チェックをビジネスコードに結合します。これに関する主な問題には、単体テストの難易度の向上と、Customer 認証ロジックを他の場所で再利用することがより困難になるという事実が含まれます。Authentication オブジェクトから GrantedAuthority[] を取得することもできますが、多数の Customer に拡張することはできません。ユーザーが 5,000 Customer にアクセスできる場合(この場合はほとんどありませんが、大規模なポニークラブで人気の獣医だと想像してください!)、Authentication オブジェクトの構築に必要なメモリ量と時間は望ましくありません。Customer を外部コードから直接開く最後のメソッドは、おそらく 3 つのうちの最良のメソッドです。関心事の分離を実現し、メモリまたは CPU サイクルを誤用しませんが、AccessDecisionVoter と最終的なビジネスメソッド自体の両方が Customer オブジェクトの取得を担当する DAO への呼び出しを実行するという点で、依然として非効率的です。メソッド呼び出しごとに 2 回アクセスすることは明らかに望ましくありません。さらに、リストされているすべてのアプローチで、独自のアクセス制御リスト(ACL)の永続性とビジネスロジックをゼロから作成する必要があります。

幸いなことに、別の選択肢があります。これについては以下で説明します。

11.6.2. 主なコンセプト

Spring Security の ACL サービスは spring-security-acl-xxx.jar で提供されます。Spring Security のドメインオブジェクトインスタンスのセキュリティ機能を使用するには、この JAR をクラスパスに追加する必要があります。

Spring Security のドメインオブジェクトインスタンスのセキュリティ機能は、アクセス制御リスト(ACL)の概念に基づいています。システム内のすべてのドメインオブジェクトインスタンスには独自の ACL があり、ACL はそのドメインオブジェクトを操作できる人と操作できない人の詳細を記録します。これを念頭に置いて、Spring Security は、アプリケーションに 3 つの主要な ACL 関連機能を提供します。

  • すべてのドメインオブジェクトの ACL エントリを効率的に取得する方法 (それらの ACL の変更)

  • メソッドが呼び出される前に、特定のプリンシパルがオブジェクトで機能することを許可する方法

  • メソッドが呼び出された後、特定のプリンシパルがオブジェクト(またはオブジェクトが返すもの)で動作することを許可する方法

最初の箇条書きで示されるように、Spring Security ACL モジュールの主な機能の 1 つは、ACL を取得する高性能なメソッドを提供することです。この ACL リポジトリ機能は非常に重要です。システム内のすべてのドメインオブジェクトインスタンスには複数のアクセス制御エントリがあり、各 ACL はツリー状の構造の他の ACL から継承する可能性があるためです(これは Spring Security ですぐにサポートされます。および非常に一般的に使用されています)。Spring Security の ACL 機能は、プラグ可能なキャッシュ、デッドロックを最小限に抑えるデータベース更新、ORM フレームワークからの独立性(JDBC を直接使用)、適切なカプセル化、透過的なデータベース更新とともに、ACL の高性能検索を提供するように注意深く設計されています。

データベースが ACL モジュールの操作の中心であることを前提に、実装でデフォルトで使用される 4 つのメインテーブルを見てみましょう。表は、典型的な Spring Security ACL デプロイのサイズの順に以下に示されており、最も多くの行を持つ表が最後にリストされています。

  • ACL_SID を使用すると、システム内のプリンシパルまたは機関を一意に識別できます(「SID」は「セキュリティ ID」を表します)。唯一の列は、ID、SID のテキスト表現、およびテキスト表現がプリンシパル名または GrantedAuthority のどちらを参照しているかを示すフラグです。一意のプリンシパルまたは GrantedAuthority ごとに 1 つの行があります。許可を受け取るという文脈で使用される場合、SID は一般に「受信者」と呼ばれます。

  • ACL_CLASS を使用すると、システム内のドメインオブジェクトクラスを一意に識別できます。唯一の列は、ID と Java クラス名です。ACL のアクセス許可を保存する一意のクラスごとに 1 つの行があります。

  • ACL_OBJECT_IDENTITY は、システム内の一意の各ドメインオブジェクトインスタンスの情報を格納します。列には、ID、ACL_CLASS テーブルへの外部キー、一意の識別子が含まれているため、どの ACL_CLASS インスタンスに情報を提供しているか、親、ドメインオブジェクトインスタンスの所有者を表す ACL_SID テーブルへの外部キー、ACL エントリが親 ACL から継承できるようにするかどうか。ACL 権限を保存するドメインオブジェクトインスタンスごとに 1 つの行があります。

  • 最後に、ACL_ENTRY は各受信者に割り当てられた個々のアクセス許可を保存します。列には、ACL_OBJECT_IDENTITY への外部キー、受信者(ACL_SID への外部キー)、監査するかどうか、実際に許可または拒否される許可を表す整数ビットマスクが含まれます。ドメインオブジェクトを操作する許可を受け取るすべての受信者に対して 1 つの行があります。

最後の段落で記述されていたように、ACL システムは整数ビットマスキングを使用します。心配しないでください。ACL システムを使用するためのビットシフトの細かい点を意識する必要はありませんが、オンまたはオフに切り替えることができる 32 ビットがあると言えば十分です。これらの各ビットは許可を表し、デフォルトでは許可は読み取り(ビット 0)、書き込み(ビット 1)、作成(ビット 2)、削除(ビット 3)および管理(ビット 4)です。他のアクセス許可を使用する場合は、独自の Permission インスタンスを簡単に実装できます。ACL フレームワークの残りの部分は、拡張機能の知識がなくても動作します。

システム内のドメインオブジェクトの数は、整数ビットマスキングを使用することを選択したという事実とはまったく関係がないことを理解することが重要です。許可に 32 ビットを使用できますが、数十億のドメインオブジェクトインスタンス(ACL_OBJECT_IDENTITY およびおそらく ACL_ENTRY の数十億行を意味する)を持つことができます。これは、潜在的なドメインオブジェクトごとにビットが必要であると人々が誤って信じていることがあるためです。

ACL システムの機能と、テーブル構造での表示の基本的な概要を説明したため、次に主要なインターフェースを見てみましょう。主なインターフェースは次のとおりです。

  • Acl: すべてのドメインオブジェクトには Acl オブジェクトが 1 つだけあり、Acl オブジェクトは内部で AccessControlEntry を保持し、Acl の所有者を知っています。Acl はドメインオブジェクトを直接参照せず、代わりに ObjectIdentity を参照します。Acl は ACL_OBJECT_IDENTITY テーブルに保存されます。

  • AccessControlEntryAcl は複数の AccessControlEntry を保持しますが、これらは多くの場合、フレームワークでは ACE と略されます。各 ACE は、PermissionSid および Acl の特定のタプルを参照します。ACE は、許可または非許可で、監査設定を含めることもできます。ACE は ACL_ENTRY テーブルに保存されます。

  • Permission: 許可は、特定の不変のビットマスクを表し、ビットマスキングと情報の出力に便利な機能を提供します。上記の基本的なアクセス許可(ビット 0 〜 4)は、BasePermission クラスに含まれています。

  • Sid: ACL モジュールは、プリンシパルと GrantedAuthority[] を参照する必要があります。間接レベルは、「セキュリティ ID」の略語である Sid インターフェースによって提供されます。一般的なクラスには、PrincipalSid (Authentication オブジェクト内のプリンシパルを表す)および GrantedAuthoritySid が含まれます。セキュリティ ID 情報は ACL_SID テーブルに保存されます。

  • ObjectIdentity: 各ドメインオブジェクトは、ObjectIdentity によって ACL モジュール内で内部的に表されます。デフォルトの実装は ObjectIdentityImpl と呼ばれます。

  • AclService: 指定された ObjectIdentity に適用可能な Acl を取得します。含まれる実装(JdbcAclService)では、検索操作は LookupStrategy に委譲されます。LookupStrategy は、バッチ検索 (BasicLookupStrategy を使用して ACL 情報を取得するための高度に最適化された戦略を提供し、マテリアライズドビュー、階層クエリ、および同様のパフォーマンス中心の非 ANSI SQL 機能を活用するカスタム実装をサポートします。

  • MutableAclService: 永続化のために、変更された Acl を提示できます。希望しない場合、このインターフェースを使用することは必須ではありません。

すぐに使える AclService および関連するデータベースクラスはすべて ANSI SQL を使用することに注意してください。これはすべての主要なデータベースで機能するはずです。執筆時点では、システムは Hypersonic SQL、PostgreSQL、Microsoft SQL Server、Oracle を使用して正常にテストされていました。

Spring Security には、ACL モジュールを示す 2 つのサンプルが付属しています。1 つ目は Contacts サンプルで、もう 1 つは Document Management System(DMS)サンプルです。例としてこれらを確認することをお勧めします。

11.6.3. 入門

Spring Security の ACL 機能の使用を開始するには、ACL 情報をどこかに保存する必要があります。これには、Spring を使用した DataSource のインスタンス化が必要です。次に、DataSource は JdbcMutableAclService および BasicLookupStrategy インスタンスに注入されます。後者は高性能 ACL 検索機能を提供し、前者はミューテーター機能を提供します。構成例については、Spring Security に同梱されているサンプルのいずれかを参照してください。また、最後のセクションにリストされている 4 つの ACL 固有のテーブルをデータベースに追加する必要があります(適切な SQL ステートメントについては ACL サンプルを参照してください)。

必要なスキーマを作成して JdbcMutableAclService をインスタンス化したら、次にドメインモデルが Spring Security ACL パッケージとの相互運用性をサポートしていることを確認する必要があります。ObjectIdentityImpl が使用できる多くのメソッドを提供するため、ObjectIdentityImpl で十分であることを願っています。ほとんどの人は、public Serializable getId() メソッドを含むドメインオブジェクトを持っています。戻り値の型が長い場合、または long と互換性がある場合(int など)、ObjectIdentity の課題をさらに考慮する必要はありません。ACL モジュールの多くの部分は、長い識別子に依存しています。long(または int、byte など)を使用していない場合、多くのクラスを再実装する必要がある可能性が非常に高くなります。Spring Security の ACL モジュールでは、最も一般的な識別子データ型であるすべてのデータベースシーケンスと既に互換性があり、すべての一般的な使用シナリオに対応するのに十分な長さがあるため、長い識別子をサポートするつもりはありません

次のコードは、Acl を作成する方法、または既存の Acl を変更する方法を示しています。

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

上記の例では、ID 番号 44 の「Foo」ドメインオブジェクトに関連付けられた ACL を取得しています。次に、ACE を追加して、「Samantha」というプリンシパルがオブジェクトを「管理」できるようにします。コードの断片は、insertAce メソッドを除き、比較的自明です。insertAce メソッドの最初の引数は、Acl のどの位置に新しいエントリを挿入するかを決定します。上記の例では、新しい ACE を既存の ACE の最後に配置しています。最後の引数は、ACE が許可するか拒否するかを示すブール値です。ほとんどの場合、許可されます(true)が、拒否している場合(false)は、アクセス許可が事実上ブロックされています。

Spring Security は、DAO またはリポジトリ操作の一部として ACL を自動的に作成、更新、または削除するための特別な統合を提供しません。代わりに、個々のドメインオブジェクトに対して上記のようなコードを記述する必要があります。サービスレイヤーで AOP を使用して、ACL 情報をサービスレイヤーの操作に自動的に統合することを検討する価値があります。これは過去に非常に効果的なアプローチであることがわかりました。

上記の手法を使用していくつかの ACL 情報をデータベースに保存したら、次のステップは、認可決定ロジックの一部として実際に ACL 情報を使用することです。ここには多くの選択肢があります。メソッド呼び出しの前後にそれぞれ起動する独自の AccessDecisionVoter または AfterInvocationProvider を作成できます。このようなクラスは、AclService を使用して関連する ACL を取得し、Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) を呼び出して認可を認可するか拒否するかを決定します。または、AclEntryVoterAclEntryAfterInvocationProvider、または AclEntryAfterInvocationCollectionFilteringProvider クラスを使用できます。これらのクラスはすべて、実行時に ACL 情報を評価するための宣言ベースのアプローチを提供するため、コードを記述する必要がありません。これらのクラスの使用方法については、サンプルアプリケーションを参照してください。

12. OAuth2

12.1. OAuth 2.0 ログイン

OAuth 2.0 ログイン機能は、OAuth 2.0 プロバイダー(例:GitHub)または OpenID Connect 1.0 プロバイダー(Google など)の既存のアカウントを使用してユーザーにアプリケーションにログインさせる機能をアプリケーションに提供します。OAuth 2.0 Login は、「Google でログイン」または「GitHub でログイン」というユースケースを実装しています。

OAuth 2.0 Login は、OAuth 2.0 認証フレームワーク (英語) および OpenID Connect コア 1.0 (英語) で指定されている認証コード付与を使用して実装されます。

12.1.1. Spring Boot 2.x サンプル

Spring Boot 2.x は、OAuth 2.0 ログインの完全な自動構成機能を提供します。

このセクションでは、Google認証プロバイダーとして使用して OAuth 2.0 ログインサンプル (GitHub) を構成する方法を示し、次のトピックを扱います。

初期設定

ログインに Google の OAuth 2.0 認証システムを使用するには、Google API Console でプロジェクトを設定して OAuth 2.0 認証情報を取得する必要があります。

「OAuth 2.0 のセットアップ」セクションから始まる OpenID Connect (英語) ページの指示に従ってください。

「OAuth 2.0 資格情報の取得」の手順を完了すると、クライアント ID とクライアントシークレットで構成される資格情報を持つ新しい OAuth クライアントが必要になります。

リダイレクト URI の設定

リダイレクト URI は、エンドユーザーのユーザーエージェントが Google で認証れ、同意ページで OAuth クライアント 前のステップで作成された へのアクセスを許可した後にリダイレクトされるアプリケーションのパスです。

「リダイレクト URI の設定」サブセクションで、承認されたリダイレクト URI フィールドが http://localhost:8080/login/oauth2/code/google に設定されていることを確認します。

デフォルトのリダイレクト URI テンプレートは {baseUrl}/login/oauth2/code/{registrationId} です。registrationId は、ClientRegistration の一意の識別子です。
OAuth クライアントがプロキシサーバーの背後で実行されている場合は、プロキシサーバー構成をチェックして、アプリケーションが正しく構成されていることを確認することをお勧めします。また、redirect-uri でサポートされている URI テンプレート変数も参照してください。
application.yml を設定する

Google で新しい OAuth クライアントを作成したため、認証フローに OAuth クライアントを使用するようにアプリケーションを構成する必要があります。そうするには:

  1. application.yml に移動して、次の構成を設定します。

    spring:
      security:
        oauth2:
          client:
            registration:   (1)
              google:   (2)
                client-id: google-client-id
                client-secret: google-client-secret
    例 80: OAuth クライアントのプロパティ
    1spring.security.oauth2.client.registration は、OAuth クライアントプロパティの基本プロパティプレフィックスです。
    2 ベースプロパティプレフィックスの後には、google などの ClientRegistration の ID が続きます。
  2. client-id および client-secret プロパティの値を、前に作成した OAuth 2.0 資格情報に置き換えます。

アプリケーションを起動する

Spring Boot 2.x サンプルを起動し、http://localhost:8080 に移動します。次に、Google へのリンクを表示するデフォルトの自動生成されたログインページにリダイレクトされます。

Google リンクをクリックすると、認証のために Google にリダイレクトされます。

Google アカウントの認証情報で認証した後、表示される次のページは同意画面です。同意画面では、以前に作成した OAuth クライアントへのアクセスを認可または拒否するように求められます。許可するをクリックして、OAuth クライアントがメールアドレスと基本的なプロファイル情報にアクセスすることを認可します。

この時点で、OAuth クライアントは UserInfo エンドポイント (英語) からメールアドレスと基本プロファイル情報を取得し、認証済みセッションを確立します。

12.1.2. Spring Boot 2.x プロパティマッピング

次の表に、Spring Boot 2.x OAuth クライアントプロパティの ClientRegistration プロパティへのマッピングの概要を示します。

Spring Boot 2.xClientRegistration

spring.security.oauth2.client.registration.[registrationId]

registrationId

spring.security.oauth2.client.registration.[registrationId].client-id

clientId

spring.security.oauth2.client.registration.[registrationId].client-secret

clientSecret

spring.security.oauth2.client.registration.[registrationId].client-authentication-method

clientAuthenticationMethod

spring.security.oauth2.client.registration.[registrationId].authorization-grant-type

authorizationGrantType

spring.security.oauth2.client.registration.[registrationId].redirect-uri

redirectUriTemplate

spring.security.oauth2.client.registration.[registrationId].scope

scopes

spring.security.oauth2.client.registration.[registrationId].client-name

clientName

spring.security.oauth2.client.provider.[providerId].authorization-uri

providerDetails.authorizationUri

spring.security.oauth2.client.provider.[providerId].token-uri

providerDetails.tokenUri

spring.security.oauth2.client.provider.[providerId].jwk-set-uri

providerDetails.jwkSetUri

spring.security.oauth2.client.provider.[providerId].user-info-uri

providerDetails.userInfoEndpoint.uri

spring.security.oauth2.client.provider.[providerId].user-info-authentication-method

providerDetails.userInfoEndpoint.authenticationMethod

spring.security.oauth2.client.provider.[providerId].user-name-attribute

providerDetails.userInfoEndpoint.userNameAttributeName

ClientRegistration は、spring.security.oauth2.client.provider.[providerId].issuer-uri プロパティを指定することにより、OpenID Connect プロバイダーの構成エンドポイント (英語) または認可サーバーのメタデータエンドポイント (英語) の検出を使用して最初に構成できます。

12.1.3. CommonOAuth2Provider

CommonOAuth2Provider は、多数の有名なプロバイダー(Google、GitHub、Facebook、および Okta)の一連のデフォルトクライアントプロパティを事前に定義しています。

例: authorization-uritoken-uri および user-info-uri は、プロバイダーに対して頻繁に変更されません。必要な構成を減らすためにデフォルト値を提供することは理にかなっています。

前に示したように、Google クライアントを構成したときは、client-id プロパティと client-secret プロパティのみが必要です。

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret
registrationId (google)は CommonOAuth2Provider の GOOGLEenum (大文字と小文字を区別しない)に一致するため、クライアントプロパティの自動デフォルト設定はシームレスに機能します。

google-login など、別の registrationId を指定する場合は、provider プロパティを構成することにより、クライアントプロパティの自動デフォルト設定を活用できます。

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          google-login: (1)
            provider: google    (2)
            client-id: google-client-id
            client-secret: google-client-secret
1registrationId は google-login に設定されます。
2provider プロパティは google に設定され、CommonOAuth2Provider.GOOGLE.getBuilder() で設定されたクライアントプロパティの自動デフォルト設定を活用します。

12.1.4. カスタムプロバイダープロパティの構成

マルチテナンシーをサポートする OAuth 2.0 プロバイダーがいくつかあります。これにより、テナント(またはサブドメイン)ごとに異なるプロトコルエンドポイントが作成されます。

例:Okta に登録された OAuth クライアントは特定のサブドメインに割り当てられ、独自のプロトコルエンドポイントを持ちます。

これらの場合のために、Spring Boot 2.x はカスタムプロバイダープロパティを構成するための次の基本プロパティを提供します: spring.security.oauth2.client.provider.[providerId]

次のリストに例を示します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
        provider:
          okta: (1)
            authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
            token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
            user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
1 基本プロパティ(spring.security.oauth2.client.provider.okta)により、プロトコルエンドポイントの場所をカスタム構成できます。

12.1.5. Spring Boot 2.x 自動構成のオーバーライド

OAuth クライアントをサポートするための Spring Boot 2.x 自動構成クラスは OAuth2ClientAutoConfiguration です。

次のタスクを実行します。

  • 構成された OAuth クライアントプロパティから ClientRegistration で構成される ClientRegistrationRepository@Bean を登録します。

  • WebSecurityConfigurerAdapter@Configuration を提供し、httpSecurity.oauth2Login() を介した OAuth 2.0 ログインを有効にします。

特定の要件に基づいて自動構成をオーバーライドする必要がある場合、次の方法でオーバーライドできます。

ClientRegistrationRepository @Bean を登録する

次の例は、ClientRegistrationRepository@Bean を登録する方法を示しています。

@Configuration
public class OAuth2LoginConfig {

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile", "email", "address", "phone")
            .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
            .tokenUri("https://www.googleapis.com/oauth2/v4/token")
            .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
            .clientName("Google")
            .build();
    }
}
WebSecurityConfigurerAdapter を提供する

次の例は、WebSecurityConfigurerAdapter に @EnableWebSecurity を提供し、httpSecurity.oauth2Login() を介して OAuth 2.0 ログインを有効にする方法を示しています。

例 81: OAuth2 ログイン構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(withDefaults());
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2Login { }
        }
    }
}
自動構成を完全にオーバーライドする

次の例は、ClientRegistrationRepository@Bean を登録して WebSecurityConfigurerAdapter を提供することにより、自動構成を完全にオーバーライドする方法を示しています。

例 82: 自動構成のオーバーライド
Java
@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests(authorize -> authorize
                    .anyRequest().authenticated()
                )
                .oauth2Login(withDefaults());
        }
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return ClientRegistration.withRegistrationId("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
            .scope("openid", "profile", "email", "address", "phone")
            .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
            .tokenUri("https://www.googleapis.com/oauth2/v4/token")
            .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
            .userNameAttributeName(IdTokenClaimNames.SUB)
            .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
            .clientName("Google")
            .build();
    }
}
Kotlin
@Configuration
class OAuth2LoginConfig {

    @EnableWebSecurity
    class OAuth2LoginSecurityConfig: WebSecurityConfigurerAdapter() {

        override fun configure(http: HttpSecurity) {
            http {
                authorizeRequests {
                    authorize(anyRequest, authenticated)
                }
                oauth2Login { }
            }
        }
    }

    @Bean
    fun clientRegistrationRepository(): ClientRegistrationRepository {
        return InMemoryClientRegistrationRepository(googleClientRegistration())
    }

    private fun googleClientRegistration(): ClientRegistration {
        return ClientRegistration.withRegistrationId("google")
                .clientId("google-client-id")
                .clientSecret("google-client-secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
                .scope("openid", "profile", "email", "address", "phone")
                .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
                .tokenUri("https://www.googleapis.com/oauth2/v4/token")
                .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
                .userNameAttributeName(IdTokenClaimNames.SUB)
                .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
                .clientName("Google")
                .build()
    }
}

12.1.6. Spring Boot 2.x を使用しない Java 構成

Spring Boot 2.x を使用できず、CommonOAuth2Provider で事前定義されたプロバイダーの 1 つ(Google など)を構成する場合は、次の構成を適用します。

例 83: OAuth2 ログイン構成
Java
@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests(authorize -> authorize
                    .anyRequest().authenticated()
                )
                .oauth2Login(withDefaults());
        }
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
    }

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService(
            ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
    }

    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

    private ClientRegistration googleClientRegistration() {
        return CommonOAuth2Provider.GOOGLE.getBuilder("google")
            .clientId("google-client-id")
            .clientSecret("google-client-secret")
            .build();
    }
}
XML
<http auto-config="true">
    <intercept-url pattern="/**" access="authenticated"/>
    <oauth2-login authorized-client-repository-ref="authorizedClientRepository"/>
</http>

<client-registrations>
    <client-registration registration-id="google"
                         client-id="google-client-id"
                         client-secret="google-client-secret"
                         provider-id="google"/>
</client-registrations>

<b:bean id="authorizedClientService"
        class="org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService"
        autowire="constructor"/>

<b:bean id="authorizedClientRepository"
        class="org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository">
    <b:constructor-arg ref="authorizedClientService"/>
</b:bean>

12.1.7. 高度な構成

HttpSecurity.oauth2Login() は、OAuth 2.0 ログインをカスタマイズするための多くの構成オプションを提供します。主な構成オプションは、対応するプロトコルエンドポイントにグループ化されます。

例: oauth2Login().authorizationEndpoint() では認可エンドポイントを構成できますが、oauth2Login().tokenEndpoint() ではトークンエンドポイントを構成できます。

次のコードは例を示しています。

例 84: 高度な OAuth2 ログイン構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                        ...
                )
                .redirectionEndpoint(redirection -> redirection
                        ...
                )
                .tokenEndpoint(token -> token
                        ...
                )
                .userInfoEndpoint(userInfo -> userInfo
                        ...
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                authorizationEndpoint {
                    ...
                }
                redirectionEndpoint {
                    ...
                }
                tokenEndpoint {
                    ...
                }
                userInfoEndpoint {
                    ...
                }
            }
        }
    }
}

oauth2Login() DSL の主なゴールは、仕様で定義されているように、命名に厳密に整合することでした。

OAuth 2.0 認可フレームワークは、プロトコルエンドポイント (英語) を次のように定義します。

認可プロセスでは、2 つの認可サーバーエンドポイント(HTTP リソース)を使用します。

  • 認可エンドポイント : クライアントがユーザーエージェントリダイレクトを介してリソース所有者から認可を取得するために使用します。

  • トークンエンドポイント : 通常はクライアント認証で、アクセストークンの認可付与を交換するためにクライアントによって使用されます。

1 つのクライアントエンドポイントと同様に:

  • リダイレクトエンドポイント : リソース所有者のユーザーエージェントを介してクライアントに認証資格情報を含むレスポンスを返すために認証サーバーによって使用されます。

OpenID Connect Core 1.0 仕様では、UserInfo エンドポイント (英語) を次のように定義しています。

UserInfo エンドポイントは、認証されたエンドユーザーに関するクレームを返す OAuth 2.0 保護リソースです。エンドユーザーに関するリクエストされたクレームを取得するために、クライアントは OpenID Connect 認証を通じて取得されたアクセストークンを使用して UserInfo エンドポイントにリクエストを行います。通常、これらのクレームは、クレームの名前と値のペアのコレクションを含む JSON オブジェクトによって表されます。

次のコードは、oauth2Login() DSL で使用可能な完全な構成オプションを示しています。

例 85: OAuth2 ログイン構成オプション
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .clientRegistrationRepository(this.clientRegistrationRepository())
                .authorizedClientRepository(this.authorizedClientRepository())
                .authorizedClientService(this.authorizedClientService())
                .loginPage("/login")
                .authorizationEndpoint(authorization -> authorization
                    .baseUri(this.authorizationRequestBaseUri())
                    .authorizationRequestRepository(this.authorizationRequestRepository())
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                )
                .redirectionEndpoint(redirection -> redirection
                    .baseUri(this.authorizationResponseBaseUri())
                )
                .tokenEndpoint(token -> token
                    .accessTokenResponseClient(this.accessTokenResponseClient())
                )
                .userInfoEndpoint(userInfo -> userInfo
                    .userAuthoritiesMapper(this.userAuthoritiesMapper())
                    .userService(this.oauth2UserService())
                    .oidcUserService(this.oidcUserService())
                    .customUserType(GitHubOAuth2User.class, "github")
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                clientRegistrationRepository = clientRegistrationRepository()
                authorizedClientRepository = authorizedClientRepository()
                authorizedClientService = authorizedClientService()
                loginPage = "/login"
                authorizationEndpoint {
                    baseUri = authorizationRequestBaseUri()
                    authorizationRequestRepository = authorizationRequestRepository()
                    authorizationRequestResolver = authorizationRequestResolver()
                }
                redirectionEndpoint {
                    baseUri = authorizationResponseBaseUri()
                }
                tokenEndpoint {
                    accessTokenResponseClient = accessTokenResponseClient()
                }
                userInfoEndpoint {
                    userAuthoritiesMapper = userAuthoritiesMapper()
                    userService = oauth2UserService()
                    oidcUserService = oidcUserService()
                    customUserType(GitHubOAuth2User::class.java, "github")
                }
            }
        }
    }
}

oauth2Login() DSL に加えて、XML 構成もサポートされています。

次のコードは、セキュリティ名前空間で使用可能な完全な構成オプションを示しています。

例 86: OAuth2 ログイン XML 設定オプション
<http>
    <oauth2-login client-registration-repository-ref="clientRegistrationRepository"
                  authorized-client-repository-ref="authorizedClientRepository"
                  authorized-client-service-ref="authorizedClientService"
                  authorization-request-repository-ref="authorizationRequestRepository"
                  authorization-request-resolver-ref="authorizationRequestResolver"
                  access-token-response-client-ref="accessTokenResponseClient"
                  user-authorities-mapper-ref="userAuthoritiesMapper"
                  user-service-ref="oauth2UserService"
                  oidc-user-service-ref="oidcUserService"
                  login-processing-url="/login/oauth2/code/*"
                  login-page="/login"
                  authentication-success-handler-ref="authenticationSuccessHandler"
                  authentication-failure-handler-ref="authenticationFailureHandler"
                  jwt-decoder-factory-ref="jwtDecoderFactory"/>
</http>

以下のセクションでは、使用可能な各構成オプションについて詳しく説明します。

OAuth 2.0 ログインページ

デフォルトでは、OAuth 2.0 ログインページは DefaultLoginPageGeneratingFilter によって自動生成されます。デフォルトのログインページには、設定された各 OAuth クライアントとその ClientRegistration.clientName がリンクとして表示され、認可リクエスト(または OAuth 2.0 ログイン)を開始できます。

DefaultLoginPageGeneratingFilter が構成済みの OAuth クライアントのリンクを表示するには、登録された ClientRegistrationRepository が Iterable<ClientRegistration> も実装する必要があります。参考のために InMemoryClientRegistrationRepository を参照してください。

各 OAuth クライアントのリンクの宛先は、デフォルトで次のようになります。

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"

次の行に例を示します。

<a href="/oauth2/authorization/google">Google</a>

デフォルトのログインページをオーバーライドするには、oauth2Login().loginPage() および(オプションで) oauth2Login().authorizationEndpoint().baseUri() を構成します。

次のリストに例を示します。

例 87: OAuth2 ログインページの構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login/oauth2")
                ...
                .authorizationEndpoint(authorization -> authorization
                    .baseUri("/login/oauth2/authorization")
                    ...
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                loginPage = "/login/oauth2"
                authorizationEndpoint {
                    baseUri = "/login/oauth2/authorization"
                }
            }
        }
    }
}
XML
<http>
    <oauth2-login login-page="/login/oauth2"
                  ...
    />
</http>
カスタムログインページを表示できる @RequestMapping("/login/oauth2") を備えた @Controller を提供する必要があります。

前述のように、oauth2Login().authorizationEndpoint().baseUri() の構成はオプションです。ただし、カスタマイズする場合は、各 OAuth クライアントへのリンクが authorizationEndpoint().baseUri() と一致することを確認してください。

次の行に例を示します。

<a href="/login/oauth2/authorization/google">Google</a>
リダイレクトエンドポイント

リダイレクトエンドポイントは、認可サーバーがリソース所有者ユーザーエージェントを介してクライアントに認可レスポンス(認可資格情報を含む)を返すために使用されます。

OAuth 2.0 Login は認証コード付与を活用します。認証情報は認証コードです。

デフォルトの Authorization Response baseUri (リダイレクトエンドポイント)は /login/oauth2/code/* であり、OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI で定義されています。

Authorization Response baseUri をカスタマイズする場合は、次の例に示すように構成します。

例 88: リダイレクトエンドポイントの構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .redirectionEndpoint(redirection -> redirection
                    .baseUri("/login/oauth2/callback/*")
                    ...
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                redirectionEndpoint {
                    baseUri = "/login/oauth2/callback/*"
                }
            }
        }
    }
}
XML
<http>
    <oauth2-login login-processing-url="/login/oauth2/callback/*"
                  ...
    />
</http>

また、ClientRegistration.redirectUriTemplate がカスタム Authorization Response baseUri と一致することを確認する必要があります。

次のリストに例を示します。

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
    .clientId("google-client-id")
    .clientSecret("google-client-secret")
    .redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
    .build();
UserInfo エンドポイント

UserInfo エンドポイントには、次のサブセクションで説明するように、いくつかの構成オプションが含まれています。

ユーザー権限のマッピング

ユーザーが OAuth 2.0 プロバイダーで正常に認証された後、OAuth2User.getAuthorities() (または OidcUser.getAuthorities())が新しいセットの GrantedAuthority インスタンスにマッピングされ、認証の補完時に OAuth2AuthenticationToken に提供されます。

OAuth2AuthenticationToken.getAuthorities() は、hasRole('USER') や hasRole('ADMIN') などでリクエストを認可するために使用されます。

ユーザー権限をマッピングするときに選択できるオプションがいくつかあります。

GrantedAuthoritiesMapper を使用する

GrantedAuthoritiesMapper の実装を提供し、次の例に示すように構成します。

例 89: 権限付与マッパー構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .userAuthoritiesMapper(this.userAuthoritiesMapper())
                    ...
                )
            );
    }

    private GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities) -> {
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            authorities.forEach(authority -> {
                if (OidcUserAuthority.class.isInstance(authority)) {
                    OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

                    OidcIdToken idToken = oidcUserAuthority.getIdToken();
                    OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

                    // Map the claims found in idToken and/or userInfo
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                } else if (OAuth2UserAuthority.class.isInstance(authority)) {
                    OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

                    Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

                    // Map the attributes found in userAttributes
                    // to one or more GrantedAuthority's and add it to mappedAuthorities

                }
            });

            return mappedAuthorities;
        };
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                userInfoEndpoint {
                    userAuthoritiesMapper = userAuthoritiesMapper()
                }
            }
        }
    }

    private fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority> ->
        val mappedAuthorities = emptySet<GrantedAuthority>()

        authorities.forEach { authority ->
            if (authority is OidcUserAuthority) {
                val idToken = authority.idToken
                val userInfo = authority.userInfo
                // Map the claims found in idToken and/or userInfo
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            } else if (authority is OAuth2UserAuthority) {
                val userAttributes = authority.attributes
                // Map the attributes found in userAttributes
                // to one or more GrantedAuthority's and add it to mappedAuthorities
            }
        }

        mappedAuthorities
    }
}
XML
<http>
    <oauth2-login user-authorities-mapper-ref="userAuthoritiesMapper"
                  ...
    />
</http>

または、次の例に示すように、GrantedAuthoritiesMapper@Bean を登録して、構成に自動的に適用することもできます。

例 90: 権限付与マッパー Bean の構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(withDefaults());
    }

    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        ...
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login { }
        }
    }

    @Bean
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {
        ...
    }
}
OAuth2UserService を使用した委譲ベースの戦略

この戦略は GrantedAuthoritiesMapper を使用するよりも高度ですが、OAuth2UserRequest および OAuth2User (OAuth 2.0 UserService を使用する場合)または OidcUserRequest および OidcUser (OpenID Connect 1.0 UserService を使用する場合)にアクセスできるため、より柔軟です。

OAuth2UserRequest (および OidcUserRequest)は、関連する OAuth2AccessToken へのアクセスを提供します。これは、委譲者がユーザーのカスタム権限をマップする前に、保護されたリソースから権限情報を取得する必要がある場合に非常に便利です。

次の例は、OpenID Connect 1.0 UserService を使用して、委譲ベースの戦略を実装および構成する方法を示しています。

例 91: OAuth2UserService の構成
Java
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(this.oidcUserService())
                    ...
                )
            );
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        final OidcUserService delegate = new OidcUserService();

        return (userRequest) -> {
            // Delegate to the default implementation for loading a user
            OidcUser oidcUser = delegate.loadUser(userRequest);

            OAuth2AccessToken accessToken = userRequest.getAccessToken();
            Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

            // TODO
            // 1) Fetch the authority information from the protected resource using accessToken
            // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

            // 3) Create a copy of oidcUser but use the mappedAuthorities instead
            oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

            return oidcUser;
        };
    }
}
Kotlin
@EnableWebSecurity
class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Login {
                userInfoEndpoint {
                    oidcUserService = oidcUserService()
                }
            }
        }
    }

    @Bean
    fun oidcUserService(): OAuth2UserService<OidcUserRequest, OidcUser> {
        val delegate = OidcUserService()

        return OAuth2UserService { userRequest ->
            // Delegate to the default implementation for loading a user
            var oidcUser = delegate.loadUser(userRequest)

            val accessToken = userRequest.accessToken
            val mappedAuthorities = HashSet<GrantedAuthority>()

            // TODO
            // 1) Fetch the authority information from the protected resource using accessToken
            // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
            // 3) Create a copy of oidcUser but use the mappedAuthorities instead
            oidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo)

            oidcUser
        }
    }
}
XML
<http>
    <oauth2-login oidc-user-service-ref="oidcUserService"
                  ...
    />
</http>
カスタム OAuth2User の構成

CustomUserTypesOAuth2UserService は、カスタム OAuth2User タイプのサポートを提供する OAuth2UserService の実装です。

デフォルトの実装(DefaultOAuth2User)がニーズに合わない場合は、OAuth2User の独自の実装を定義できます。

次のコードは、GitHub のカスタム OAuth2User タイプを登録する方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .customUserType(GitHubOAuth2User.class, "github")
                    ...
                )
            );
    }
}

次のコードは、GitHub のカスタム OAuth2User タイプの例を示しています。

public class GitHubOAuth2User implements OAuth2User {
    private List<GrantedAuthority> authorities =
        AuthorityUtils.createAuthorityList("ROLE_USER");
    private Map<String, Object> attributes;
    private String id;
    private String name;
    private String login;
    private String email;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public Map<String, Object> getAttributes() {
        if (this.attributes == null) {
            this.attributes = new HashMap<>();
            this.attributes.put("id", this.getId());
            this.attributes.put("name", this.getName());
            this.attributes.put("login", this.getLogin());
            this.attributes.put("email", this.getEmail());
        }
        return attributes;
    }

    public String getId() {
        return this.id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLogin() {
        return this.login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getEmail() {
        return this.email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
idnamelogin および email は、GitHub の UserInfo レスポンスで返される属性です。UserInfo エンドポイントから返される詳細情報については、「認証されたユーザーを取得する」 (英語) の API ドキュメントを参照してください。
OAuth 2.0 UserService

DefaultOAuth2UserService は、標準の OAuth 2.0 プロバイダーをサポートする OAuth2UserService の実装です。

OAuth2UserService は、エンドユーザー(リソース所有者)のユーザー属性を UserInfo エンドポイントから取得し(認可フロー中にクライアントに付与されたアクセストークンを使用して)、AuthenticatedPrincipal を OAuth2User の形式で返します。

DefaultOAuth2UserService は、UserInfo エンドポイントでユーザー属性をリクエストするときに RestOperations を使用します。

UserInfo リクエストの前処理をカスタマイズする必要がある場合は、DefaultOAuth2UserService.setRequestEntityConverter() にカスタム Converter<OAuth2UserRequest, RequestEntity<?>> を提供できます。デフォルトの実装 OAuth2UserRequestEntityConverter は、デフォルトで Authorization ヘッダーに OAuth2AccessToken を設定する UserInfo リクエストの RequestEntity 表現を構築します。

一方、UserInfo レスポンスのリアクティブ処理をカスタマイズする必要がある場合は、DefaultOAuth2UserService.setRestOperations() にカスタム構成の RestOperations を提供する必要があります。デフォルトの RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラー(400 間違ったリクエスト)を処理できる ResponseErrorHandler です。OAuth 2.0 Error パラメーターを OAuth2Error に変換するために OAuth2ErrorHttpMessageConverter を使用します。

DefaultOAuth2UserService をカスタマイズする場合でも、OAuth2UserService の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(this.oauth2UserService())
                    ...
                )
            );
    }

    private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
        ...
    }
}
OpenID Connect 1.0 UserService

OidcUserService は、OpenID Connect 1.0 プロバイダーをサポートする OAuth2UserService の実装です。

OidcUserService は、UserInfo エンドポイントでユーザー属性をリクエストするときに DefaultOAuth2UserService を活用します。

UserInfo リクエストの前処理および / または UserInfo レスポンスのリアクティブ処理をカスタマイズする必要がある場合、OidcUserService.setOauth2UserService() にカスタム構成の DefaultOAuth2UserService を提供する必要があります。

OidcUserService をカスタマイズするか、OpenID Connect 1.0 プロバイダーの OAuth2UserService の独自の実装を提供するかどうかにかかわらず、次の例に示すように構成する必要があります。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Login(oauth2 -> oauth2
                .userInfoEndpoint(userInfo -> userInfo
                    .oidcUserService(this.oidcUserService())
                    ...
                )
            );
    }

    private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
        ...
    }
}
ID トークン署名検証

OpenID Connect 1.0 認証は ID トークン (英語) を導入します。ID トークン (英語) は、クライアントが使用する場合の認可サーバーによるエンドユーザーの認証に関するクレームを含むセキュリティトークンです。

ID トークンは JSON Web トークン (英語) (JWT)として表され、JSON Web 署名 (英語) (JWS)を使用して署名する必要があります。

OidcIdTokenDecoderFactory は、OidcIdToken 署名検証に使用される JwtDecoder を提供します。デフォルトのアルゴリズムは RS256 ですが、クライアントの登録時に割り当てられる場合は異なる場合があります。これらの場合、特定のクライアントに割り当てられた予想される JWS アルゴリズムを返すようにリゾルバーを構成できます。

JWS アルゴリズムリゾルバーは、ClientRegistration を受け入れ、クライアントに期待される JwsAlgorithm を返す Function です。SignatureAlgorithm.RS256 または MacAlgorithm.HS256

次のコードは、すべての ClientRegistration について OidcIdTokenDecoderFactory@Bean をデフォルトの MacAlgorithm.HS256 に構成する方法を示しています。

@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
    OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
    idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
    return idTokenDecoderFactory;
}
HS256HS384 または HS512 などの MAC ベースのアルゴリズムの場合、client-id に対応する client-secret が署名検証の対称キーとして使用されます。
OpenID Connect 1.0 認証用に複数の ClientRegistration が構成されている場合、JWS アルゴリズムリゾルバーは提供された ClientRegistration を評価して、返すアルゴリズムを決定します。
OpenID Connect 1.0 ログアウト

OpenID Connect セッション管理 1.0 では、クライアントを使用してプロバイダーのエンドユーザーをログアウトできます。利用可能な戦略の 1 つは RP からのログアウト (英語) です。

OpenID プロバイダーがセッション管理とディスカバリ (英語) の両方をサポートしている場合、クライアントは OpenID プロバイダーのディスカバリメタデータ (英語) から end_session_endpointURL を取得できます。これは、次の例のように、ClientRegistration を issuer-uri で構成することで実現できます。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            ...
        provider:
          okta:
            issuer-uri: https://dev-1234.oktapreview.com

…および RP 開始ログアウトを実装する OidcClientInitiatedLogoutSuccessHandler は、次のように構成できます。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(withDefaults())
            .logout(logout -> logout
                .logoutSuccessHandler(oidcLogoutSuccessHandler())
            );
    }

    private LogoutSuccessHandler oidcLogoutSuccessHandler() {
        OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
                new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);

        // Sets the location that the End-User's User Agent will be redirected to
        // after the logout has been performed at the Provider
        oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");

        return oidcLogoutSuccessHandler;
    }
}

NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder.
If used, the application's base URL, like `https://app.example.org`, will replace it at request time.

12.2. OAuth 2.0 クライアント

OAuth 2.0 クライアント機能は、OAuth 2.0 認証フレームワーク (英語) で定義されているクライアントロールのサポートを提供します。

大まかに言うと、利用可能なコア機能は次のとおりです。

HTTP クライアントのサポート

HttpSecurity.oauth2Client() DSL は、OAuth 2.0 クライアントが使用するコアコンポーネントをカスタマイズするための多くの設定オプションを提供します。さらに、HttpSecurity.oauth2Client().authorizationCodeGrant() では、認可コードの付与をカスタマイズできます。

次のコードは、HttpSecurity.oauth2Client() DSL によって提供される完全な構成オプションを示しています。

例 92: OAuth2 クライアント設定オプション
Java
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2 -> oauth2
                .clientRegistrationRepository(this.clientRegistrationRepository())
                .authorizedClientRepository(this.authorizedClientRepository())
                .authorizedClientService(this.authorizedClientService())
                .authorizationCodeGrant(codeGrant -> codeGrant
                    .authorizationRequestRepository(this.authorizationRequestRepository())
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                    .accessTokenResponseClient(this.accessTokenResponseClient())
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Client {
                clientRegistrationRepository = clientRegistrationRepository()
                authorizedClientRepository = authorizedClientRepository()
                authorizedClientService = authorizedClientService()
                authorizationCodeGrant {
                    authorizationRequestRepository = authorizationRequestRepository()
                    authorizationRequestResolver = authorizationRequestResolver()
                    accessTokenResponseClient = accessTokenResponseClient()
                }
            }
        }
    }
}

HttpSecurity.oauth2Client() DSL に加えて、XML 構成もサポートされています。

次のコードは、セキュリティ名前空間で使用可能な完全な構成オプションを示しています。

例 93: OAuth2 クライアント XML 設定オプション
<http>
    <oauth2-client client-registration-repository-ref="clientRegistrationRepository"
                   authorized-client-repository-ref="authorizedClientRepository"
                   authorized-client-service-ref="authorizedClientService">
        <authorization-code-grant
                authorization-request-repository-ref="authorizationRequestRepository"
                authorization-request-resolver-ref="authorizationRequestResolver"
                access-token-response-client-ref="accessTokenResponseClient"/>
    </oauth2-client>
</http>

OAuth2AuthorizedClientManager は、1 つ以上の OAuth2AuthorizedClientProvider と協力して、OAuth 2.0 クライアントの認可(または再認可)を管理します。

次のコードは、OAuth2AuthorizedClientManager@Bean を登録し、authorization_coderefresh_tokenclient_credentials および password 認可付与タイプのサポートを提供する OAuth2AuthorizedClientProvider コンポジットに関連付ける方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .authorizationCode()
                    .refreshToken()
                    .clientCredentials()
                    .password()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

以下のセクションでは、OAuth 2.0 クライアントで使用されるコアコンポーネントと利用可能な設定オプションについて詳しく説明します。

12.2.1. コアインターフェース / クラス

ClientRegistration

ClientRegistration は、OAuth 2.0 または OpenID Connect 1.0 プロバイダーに登録されたクライアントの表現です。

クライアント登録には、クライアント ID、クライアントシークレット、認可付与タイプ、リダイレクト URI、スコープ、認可 URI、トークン URI、その他の詳細などの情報が保持されます。

ClientRegistration とそのプロパティは次のように定義されています。

public final class ClientRegistration {
    private String registrationId;  (1)
    private String clientId;    (2)
    private String clientSecret;    (3)
    private ClientAuthenticationMethod clientAuthenticationMethod;  (4)
    private AuthorizationGrantType authorizationGrantType;  (5)
    private String redirectUriTemplate; (6)
    private Set<String> scopes; (7)
    private ProviderDetails providerDetails;
    private String clientName;  (8)

    public class ProviderDetails {
        private String authorizationUri;    (9)
        private String tokenUri;    (10)
        private UserInfoEndpoint userInfoEndpoint;
        private String jwkSetUri;   (11)
        private Map<String, Object> configurationMetadata;  (12)

        public class UserInfoEndpoint {
            private String uri; (13)
            private AuthenticationMethod authenticationMethod;  (14)
            private String userNameAttributeName;   (15)

        }
    }
}
1registrationIdClientRegistration を一意に識別する ID。
2clientId: クライアント識別子。
3clientSecret: クライアントの秘密。
4clientAuthenticationMethod: プロバイダーでクライアントを認証するために使用される方法。サポートされている値は、basicpost、および none (パブリッククライアント) (英語) です。
5authorizationGrantType: OAuth 2.0 認可フレームワークは、4 つの認可付与 (英語) タイプを定義します。サポートされる値は authorization_codeclient_credentials および password です。
6redirectUriTemplate: エンドユーザーがクライアントへのアクセスを認証および認可した後に、認可サーバーがエンドユーザーのユーザーエージェントをリダイレクトするクライアントの登録済みリダイレクト URI。
7scopes: 認可リクエストフロー中にクライアントがリクエストしたスコープ(openid、メール、プロファイルなど)。
8clientName: クライアントに使用される説明的な名前。この名前は、自動生成されたログインページにクライアントの名前を表示するときなど、特定のシナリオで使用される場合があります。
9authorizationUri: 認可サーバーの認可エンドポイント URI。
10tokenUri: 認可サーバーのトークンエンドポイント URI。
11jwkSetUri: 認可サーバーから JSON Web キー (JWK) (英語) セットを取得するために使用される URI。ID トークンの JSON Web 署名 (JWS) (英語) およびオプションで UserInfo レスポンスを検証するために使用される暗号化キーが含まれます。
12configurationMetadata: OpenID プロバイダーの構成情報 (英語) 。この情報は、Spring Boot 2.x プロパティ spring.security.oauth2.client.provider.[providerId].issuerUri が構成されている場合にのみ利用できます。
13(userInfoEndpoint)uri: 認証されたエンドユーザーのクレーム / 属性にアクセスするために使用される UserInfo エンドポイント URI。
14(userInfoEndpoint)authenticationMethod: アクセストークンを UserInfo エンドポイントに送信するときに使用される認証方法。サポートされる値は、headerform、および query です。
15userNameAttributeName: エンドユーザーの名前または識別子を参照する UserInfo レスポンスで返される属性の名前。

ClientRegistration は、OpenID Connect プロバイダーの構成エンドポイント (英語) または認可サーバーのメタデータエンドポイント (英語) の検出を使用して最初に構成できます。

ClientRegistrations は、次の例に見られるように、このメソッドで ClientRegistration を構成するための便利なメソッドを提供します。

ClientRegistration clientRegistration =
    ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();

別の方法として、ClientRegistrations.fromOidcIssuerLocation() を使用して、OpenID Connect プロバイダーの構成エンドポイントのみを照会できます。

ClientRegistrationRepository

ClientRegistrationRepository は、OAuth 2.0/OpenID Connect 1.0 ClientRegistration(s)のリポジトリとして機能します。

クライアント登録情報は最終的に保存され、関連する認可サーバーによって所有されます。このリポジトリは、認可サーバーに保存されているプライマリクライアント登録情報のサブセットを取得する機能を提供します。

Spring Boot 2.x 自動構成は、spring.security.oauth2.client.registration.[registrationId] の各プロパティを ClientRegistration のインスタンスにバインドし、ClientRegistrationRepository 内の各 ClientRegistration インスタンスを構成します。

ClientRegistrationRepository のデフォルトの実装は InMemoryClientRegistrationRepository です。

また、自動構成は、ClientRegistrationRepository を ApplicationContext の @Bean として登録し、アプリケーションで必要な場合に依存関係の注入に使用できるようにします。

次のリストに例を示します。

@Controller
public class OAuth2ClientController {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @GetMapping("/")
    public String index() {
        ClientRegistration oktaRegistration =
            this.clientRegistrationRepository.findByRegistrationId("okta");

        ...

        return "index";
    }
}
OAuth2AuthorizedClient

OAuth2AuthorizedClient は、認可クライアントの表現です。エンドユーザー(リソース所有者)がクライアントに保護されたリソースにアクセスする認可を与えた場合、クライアントは認可されたと見なされます。

OAuth2AuthorizedClient は、OAuth2AccessToken (およびオプションの OAuth2RefreshToken)を、認可を認可した Principal エンドユーザーである ClientRegistration (クライアント)およびリソース所有者に関連付ける目的に役立ちます。

OAuth2AuthorizedClientRepository/OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository は、Web リクエスト間で OAuth2AuthorizedClient を保持するロールを果たします。一方、OAuth2AuthorizedClientService の主なロールは、アプリケーションレベルで OAuth2AuthorizedClient を管理することです。

開発者の観点から、OAuth2AuthorizedClientRepository または OAuth2AuthorizedClientService は、クライアントに関連付けられた OAuth2AccessToken をルックアップする機能を提供し、保護されたリソースリクエストを開始するために使用できるようにします。

次のリストに例を示します。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @GetMapping("/")
    public String index(Authentication authentication) {
        OAuth2AuthorizedClient authorizedClient =
            this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
Spring Boot 2.x 自動構成は、ApplicationContext に OAuth2AuthorizedClientRepository または OAuth2AuthorizedClientService@Bean、あるいはその両方を登録します。ただし、アプリケーションは、カスタム OAuth2AuthorizedClientRepository または OAuth2AuthorizedClientService@Bean をオーバーライドして登録することを選択できます。

OAuth2AuthorizedClientService のデフォルトの実装は InMemoryOAuth2AuthorizedClientService であり、OAuth2AuthorizedClient(s)をメモリに保存します。

あるいは、データベースで OAuth2AuthorizedClient を永続化するために、JDBC 実装 JdbcOAuth2AuthorizedClientService を構成できます。

JdbcOAuth2AuthorizedClientService は、OAuth 2.0 クライアントスキーマで説明されているテーブル定義に依存します。
OAuth2AuthorizedClientManager/OAuth2AuthorizedClientProvider

OAuth2AuthorizedClientManager は、OAuth2AuthorizedClient の全体的な管理を担当します。

主な責任は次のとおりです。

  • OAuth2AuthorizedClientProvider を使用して、OAuth 2.0 クライアントを認可(または再認可)します。

  • 通常 OAuth2AuthorizedClientService または OAuth2AuthorizedClientRepository を使用して、OAuth2AuthorizedClient の永続性を委譲します。

  • OAuth 2.0 クライアントが正常に認証(または再認証)された場合、OAuth2AuthorizationSuccessHandler に委譲します。

  • OAuth 2.0 クライアントが認証(または再認証)に失敗した場合の OAuth2AuthorizationFailureHandler への委譲。

OAuth2AuthorizedClientProvider は、OAuth 2.0 クライアントを認可(または再認可)するための戦略を実装しています。実装は通常、認可付与タイプを実装します。authorization_codeclient_credentials など

OAuth2AuthorizedClientManager のデフォルトの実装は DefaultOAuth2AuthorizedClientManager です。これは、委譲ベースの複合を使用して複数の認可付与タイプをサポートする可能性のある OAuth2AuthorizedClientProvider に関連付けられています。OAuth2AuthorizedClientProviderBuilder を使用して、委譲ベースのコンポジットを構成および構築できます。

次のコードは、authorization_coderefresh_tokenclient_credentials および password 認可付与タイプのサポートを提供する OAuth2AuthorizedClientProvider コンポジットを構成および構築する方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .authorizationCode()
                    .refreshToken()
                    .clientCredentials()
                    .password()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

認可の試みが成功すると、DefaultOAuth2AuthorizedClientManager は OAuth2AuthorizationSuccessHandler に委譲し、OAuth2AuthorizationSuccessHandler は(デフォルトで) OAuth2AuthorizedClientRepository を介して OAuth2AuthorizedClient を保存します。再認証の失敗の場合、たとえばリフレッシュトークンは無効になり、以前に保存された OAuth2AuthorizedClient は RemoveAuthorizedClientOAuth2AuthorizationFailureHandler を介して OAuth2AuthorizedClientRepository から削除されます。デフォルトの動作は、setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler) および setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler) を介してカスタマイズできます。

DefaultOAuth2AuthorizedClientManager は、タイプ Function<OAuth2AuthorizeRequest, Map<String, Object>> の contextAttributesMapper にも関連付けられます。Function<OAuth2AuthorizeRequest, Map<String, Object>> は、OAuth2AuthorizeRequest から OAuth2AuthorizationContext に関連付けられる属性の Map への属性のマッピングを担当します。これは、OAuth2AuthorizedClientProvider に必要な(サポートされている)属性を指定する必要がある場合に役立ちます。PasswordOAuth2AuthorizedClientProvider では、リソース所有者の username および password が OAuth2AuthorizationContext.getAttributes() で使用可能である必要があります。

次のコードは、contextAttributesMapper の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .password()
                    .refreshToken()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
    // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

    return authorizedClientManager;
}

private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
    return authorizeRequest -> {
        Map<String, Object> contextAttributes = Collections.emptyMap();
        HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
        String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
        String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            contextAttributes = new HashMap<>();

            // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
            contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
        }
        return contextAttributes;
    };
}

DefaultOAuth2AuthorizedClientManager は、HttpServletRequest のコンテキストである以内にを使用するように設計されています。HttpServletRequest コンテキストの外側を操作する場合は、代わりに AuthorizedClientServiceOAuth2AuthorizedClientManager を使用してください。

サービスアプリケーションは、AuthorizedClientServiceOAuth2AuthorizedClientManager をいつ使用するかの一般的な使用例です。サービスアプリケーションは多くの場合、ユーザーの操作なしでバックグラウンドで実行され、通常はユーザーアカウントではなくシステムレベルのアカウントで実行されます。client_credentials 許可タイプで構成された OAuth 2.0 クライアントは、サービスアプリケーションのタイプと見なすことができます。

次のコードは、client_credentials 付与タイプのサポートを提供する AuthorizedClientServiceOAuth2AuthorizedClientManager を構成する方法の例を示しています。

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientService authorizedClientService) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

12.2.2. 認可付与サポート

認証コード
認証コード (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。
認可の取得
認証コードの付与については、認可リクエスト / レスポンス (英語) プロトコルフローを参照してください。
認可リクエストの開始

OAuth2AuthorizationRequestRedirectFilter は OAuth2AuthorizationRequestResolver を使用して OAuth2AuthorizationRequest を解決し、エンドユーザーのユーザーエージェントを認可サーバーの認可エンドポイントにリダイレクトすることで認可コード付与フローを開始します。

OAuth2AuthorizationRequestResolver の主なロールは、提供された Web リクエストから OAuth2AuthorizationRequest を解決することです。デフォルト実装 DefaultOAuth2AuthorizationRequestResolver は、registrationId を抽出し、それを使用して関連 ClientRegistration の OAuth2AuthorizationRequest を構築する(デフォルト)パス /oauth2/authorization/{registrationId} で一致します。

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/authorized/okta"
            scope: read, write
        provider:
          okta:
            authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

ベースパス /oauth2/authorization/okta を使用したリクエストは、OAuth2AuthorizationRequestRedirectFilter による認可リクエストリダイレクトを開始し、最終的に認可コード認可フローを開始します。

AuthorizationCodeOAuth2AuthorizedClientProvider は、認可コード付与のための OAuth2AuthorizedClientProvider の実装であり、OAuth2AuthorizationRequestRedirectFilter による認可リクエストリダイレクトも開始します。

OAuth 2.0 クライアントがパブリッククライアント (英語) の場合、OAuth 2.0 クライアントの登録を次のように構成します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-authentication-method: none
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/authorized/okta"
            ...

コード交換用の証明キー (英語) (PKCE)を使用したパブリッククライアントがサポートされています。クライアントが信頼されていない環境(ネイティブアプリケーションまたは Web ブラウザベースのアプリケーションなど)で実行されているため、その資格情報の機密性を維持できない場合、次の条件に該当する場合、PKCE が自動的に使用されます。

  1. client-secret は省略されます (または空)

  2. client-authentication-method は「なし」に設定されます (ClientAuthenticationMethod.NONE)

DefaultOAuth2AuthorizationRequestResolver は、UriComponentsBuilder を使用して redirect-uri の URI テンプレート変数もサポートします。

次の構成では、サポートされているすべての URI テンプレート変数を使用します。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            ...
            redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}"
            ...
{baseUrl} は {baseScheme}://{baseHost}{basePort}{basePath} に解決されます

URI テンプレート変数を使用して redirect-uri を構成することは、OAuth 2.0 クライアントがプロキシサーバーの背後で実行されている場合に特に役立ちます。これにより、redirect-uri を展開するときに X-Forwarded-* ヘッダーが使用されます。

認可リクエストのカスタマイズ

OAuth2AuthorizationRequestResolver が実現できる主な使用例の 1 つは、OAuth 2.0 認可フレームワークで定義された標準パラメーターを超える追加パラメーターで認可リクエストをカスタマイズする機能です。

例:OpenID Connect は、OAuth 2.0 認証フレームワーク (英語) で定義された標準パラメーターから拡張された、認証コード Flow (英語) の追加の OAuth 2.0 リクエストパラメーターを定義します。これらの拡張パラメーターの 1 つは prompt パラメーターです。

オプション。認可サーバーがエンドユーザーに再認証と同意を求めるかどうかを指定する、ASCII 文字列値のスペース区切りの大文字と小文字を区別したリスト。定義されている値は、なし、ログイン、同意、select_account です。

以下の例は、リクエストパラメーター prompt=consent を含めることにより、oauth2Login() の認可リクエストをカスタマイズする Consumer<OAuth2AuthorizationRequest.Builder> で DefaultOAuth2AuthorizationRequestResolver を構成する方法を示しています。

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                    .authorizationRequestResolver(
                        authorizationRequestResolver(this.clientRegistrationRepository)
                    )
                )
            );
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
                authorizationRequestCustomizer());

        return  authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
                    .additionalParameters(params -> params.put("prompt", "consent"));
    }
}

追加のリクエストパラメーターが特定のプロバイダーに対して常に同じである単純なユースケースでは、authorization-uri プロパティに直接追加できます。

例:リクエストパラメーター prompt の値がプロバイダー okta に対して常に consent である場合、単に以下のように構成します。

spring:
  security:
    oauth2:
      client:
        provider:
          okta:
            authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize?prompt=consent

上記の例は、標準パラメーターの上にカスタムパラメーターを追加する一般的な使用例を示しています。また、要件がより高度な場合は、OAuth2AuthorizationRequest.authorizationRequestUri プロパティをオーバーライドするだけで、認可リクエスト URI の作成を完全に制御できます。

OAuth2AuthorizationRequest.Builder.build() は OAuth2AuthorizationRequest.authorizationRequestUri を構築します。OAuth2AuthorizationRequest.authorizationRequestUri は、application/x-www-form-urlencoded 形式を使用するすべてのクエリパラメーターを含む認証リクエスト URI を表します。

次の例は、前の例の authorizationRequestCustomizer() のバリエーションを示し、代わりに OAuth2AuthorizationRequest.authorizationRequestUri プロパティをオーバーライドします。

private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
    return customizer -> customizer
                .authorizationRequestUri(uriBuilder -> uriBuilder
                    .queryParam("prompt", "consent").build());
}
認証リクエストの保存

AuthorizationRequestRepository は、認可リクエストが開始されてから認可レスポンスが受信されるまで(コールバック)、OAuth2AuthorizationRequest の永続化を担当します。

OAuth2AuthorizationRequest は、認可レスポンスを関連付けて検証するために使用されます。

AuthorizationRequestRepository のデフォルトの実装は HttpSessionOAuth2AuthorizationRequestRepository で、HttpSession に OAuth2AuthorizationRequest を格納します。

AuthorizationRequestRepository のカスタム実装がある場合、次の例に示すように構成できます。

例 94: AuthorizationRequestRepository の設定
Java
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2 -> oauth2
                .authorizationCodeGrant(codeGrant -> codeGrant
                    .authorizationRequestRepository(this.authorizationRequestRepository())
                    ...
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Client {
                authorizationCodeGrant {
                    authorizationRequestRepository = authorizationRequestRepository()
                }
            }
        }
    }
}
XML
<http>
    <oauth2-client>
        <authorization-code-grant authorization-request-repository-ref="authorizationRequestRepository"/>
    </oauth2-client>
</http>
アクセストークンのリクエスト
認証コードの付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

認可コード付与の OAuth2AccessTokenResponseClient のデフォルト実装は DefaultAuthorizationCodeTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンの認可コードを交換するために RestOperations を使用します。

DefaultAuthorizationCodeTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter() にカスタム Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装 OAuth2AuthorizationCodeGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の RequestEntity 表現を構築します。ただし、カスタム Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

カスタム Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な RequestEntity 表現を返す必要があります。
アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、DefaultAuthorizationCodeTokenResponseClient.setRestOperations() にカスタム構成の RestOperations を提供する必要があります。デフォルトの RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
Spring MVC FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを OAuth2AccessTokenResponse に変換するために使用されるカスタム Converter<Map<String, String>, OAuth2AccessTokenResponse> を OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを OAuth2Error に変換するために OAuth2ErrorHttpMessageConverter を使用します。

DefaultAuthorizationCodeTokenResponseClient をカスタマイズする場合でも、OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

例 95: アクセストークンレスポンスの構成
Java
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client(oauth2 -> oauth2
                .authorizationCodeGrant(codeGrant -> codeGrant
                    .accessTokenResponseClient(this.accessTokenResponseClient())
                    ...
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http {
            oauth2Client {
                authorizationCodeGrant {
                    accessTokenResponseClient = accessTokenResponseClient()
                }
            }
        }
    }
}
XML
<http>
    <oauth2-client>
        <authorization-code-grant access-token-response-client-ref="accessTokenResponseClient"/>
    </oauth2-client>
</http>
リフレッシュトークン
リフレッシュトークン (英語) の詳細については、OAuth 2.0 Authorization フレームワークを参照してください。
アクセストークンのリフレッシュ
リフレッシュトークンの付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

リフレッシュトークン付与の OAuth2AccessTokenResponseClient のデフォルト実装は DefaultRefreshTokenTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリフレッシュするときに RestOperations を使用します。

DefaultRefreshTokenTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter() にカスタム Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装 OAuth2RefreshTokenGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の RequestEntity 表現を構築します。ただし、カスタム Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

カスタム Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な RequestEntity 表現を返す必要があります。
アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、DefaultRefreshTokenTokenResponseClient.setRestOperations() にカスタム構成の RestOperations を提供する必要があります。デフォルトの RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
Spring MVC FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを OAuth2AccessTokenResponse に変換するために使用されるカスタム Converter<Map<String, String>, OAuth2AccessTokenResponse> を OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを OAuth2Error に変換するために OAuth2ErrorHttpMessageConverter を使用します。

DefaultRefreshTokenTokenResponseClient をカスタマイズする場合でも、OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient))
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().refreshToken() は RefreshTokenOAuth2AuthorizedClientProvider を構成します。これは、リフレッシュトークン許可用の OAuth2AuthorizedClientProvider の実装です。

OAuth2RefreshToken は、authorization_code および password 付与タイプのアクセストークンレスポンスでオプションで返されます。OAuth2AuthorizedClient.getRefreshToken() が使用可能であり、OAuth2AuthorizedClient.getAccessToken() の有効期限が切れている場合、RefreshTokenOAuth2AuthorizedClientProvider によって自動的にリフレッシュされます。

クライアント資格情報
クライアント資格情報 (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。
アクセストークンのリクエスト
クライアント資格情報の付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

クライアント資格情報付与の OAuth2AccessTokenResponseClient のデフォルト実装は DefaultClientCredentialsTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリクエストするときに RestOperations を使用します。

DefaultClientCredentialsTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter() にカスタム Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装 OAuth2ClientCredentialsGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の RequestEntity 表現を構築します。ただし、カスタム Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

カスタム Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な RequestEntity 表現を返す必要があります。
アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、DefaultClientCredentialsTokenResponseClient.setRestOperations() にカスタム構成の RestOperations を提供する必要があります。デフォルトの RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
Spring MVC FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを OAuth2AccessTokenResponse に変換するために使用されるカスタム Converter<Map<String, String>, OAuth2AccessTokenResponse> を OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを OAuth2Error に変換するために OAuth2ErrorHttpMessageConverter を使用します。

DefaultClientCredentialsTokenResponseClient をカスタマイズする場合でも、OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials() は、ClientCredentialsOAuth2AuthorizedClientProvider を構成します。これは、Client Credentials 付与のための OAuth2AuthorizedClientProvider の実装です。
アクセストークンの使用

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: client_credentials
            scope: read, write
        provider:
          okta:
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

…および OAuth2AuthorizedClientManager@Bean

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

OAuth2AccessToken は次のようにして入手できます。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientManager authorizedClientManager;

    @GetMapping("/")
    public String index(Authentication authentication,
                        HttpServletRequest servletRequest,
                        HttpServletResponse servletResponse) {

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
                .principal(authentication)
                .attributes(attrs -> {
                    attrs.put(HttpServletRequest.class.getName(), servletRequest);
                    attrs.put(HttpServletResponse.class.getName(), servletResponse);
                })
                .build();
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
HttpServletRequest と HttpServletResponse はどちらもオプション属性です。指定しない場合、RequestContextHolder.getRequestAttributes() を使用して ServletRequestAttributes がデフォルトになります。
リソース所有者のパスワード資格証明
リソース所有者のパスワード資格証明 (英語) 付与の詳細については、OAuth 2.0 認可フレームワークを参照してください。
アクセストークンのリクエスト
リソース所有者パスワード資格情報の付与については、アクセストークンリクエスト / レスポンス (英語) プロトコルフローを参照してください。

リソース所有者パスワード資格情報付与の OAuth2AccessTokenResponseClient のデフォルト実装は DefaultPasswordTokenResponseClient です。これは、認可サーバーのトークンエンドポイントでアクセストークンをリクエストするときに RestOperations を使用します。

DefaultPasswordTokenResponseClient は、トークンリクエストの前処理やトークンレスポンスのリアクティブ処理をカスタマイズできるため、非常に柔軟です。

アクセストークンリクエストのカスタマイズ

トークンリクエストの前処理をカスタマイズする必要がある場合は、DefaultPasswordTokenResponseClient.setRequestEntityConverter() にカスタム Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> を提供できます。デフォルトの実装 OAuth2PasswordGrantRequestEntityConverter は、標準 OAuth 2.0 アクセストークンリクエスト (英語) の RequestEntity 表現を構築します。ただし、カスタム Converter を提供すると、標準のトークンリクエストを継承し、カスタムパラメーターを追加できます。

カスタム Converter は、目的の OAuth 2.0 プロバイダーによって理解される OAuth 2.0 アクセストークンリクエストの有効な RequestEntity 表現を返す必要があります。
アクセストークンレスポンスのカスタマイズ

一方、トークンレスポンスのリアクティブ処理をカスタマイズする必要がある場合は、DefaultPasswordTokenResponseClient.setRestOperations() にカスタム構成の RestOperations を提供する必要があります。デフォルトの RestOperations は次のように構成されています。

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
Spring MVC FormHttpMessageConverter は、OAuth 2.0 アクセストークンリクエストの送信時に使用されるため、必要です。

OAuth2AccessTokenResponseHttpMessageConverter は、OAuth 2.0 アクセストークンレスポンス用の HttpMessageConverter です。OAuth 2.0 アクセストークンレスポンスパラメーターを OAuth2AccessTokenResponse に変換するために使用されるカスタム Converter<Map<String, String>, OAuth2AccessTokenResponse> を OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter() に提供できます。

OAuth2ErrorResponseErrorHandler は、OAuth 2.0 エラーを処理できる ResponseErrorHandler です。400 不正なリクエスト。OAuth 2.0 Error パラメーターを OAuth2Error に変換するために OAuth2ErrorHttpMessageConverter を使用します。

DefaultPasswordTokenResponseClient をカスタマイズする場合でも、OAuth2AccessTokenResponseClient の独自の実装を提供する場合でも、次の例に示すように構成する必要があります。

// Customize
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient = ...

OAuth2AuthorizedClientProvider authorizedClientProvider =
        OAuth2AuthorizedClientProviderBuilder.builder()
                .password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient))
                .refreshToken()
                .build();

...

authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().password() は PasswordOAuth2AuthorizedClientProvider を構成します。これは、リソース所有者パスワード資格情報付与のための OAuth2AuthorizedClientProvider の実装です。
アクセストークンの使用

OAuth 2.0 クライアント登録用の次の Spring Boot 2.x プロパティがあるとします。

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
            authorization-grant-type: password
            scope: read, write
        provider:
          okta:
            token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token

…および OAuth2AuthorizedClientManager@Bean

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .password()
                    .refreshToken()
                    .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
    // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
    authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());

    return authorizedClientManager;
}

private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
    return authorizeRequest -> {
        Map<String, Object> contextAttributes = Collections.emptyMap();
        HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
        String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
        String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            contextAttributes = new HashMap<>();

            // `PasswordOAuth2AuthorizedClientProvider` requires both attributes
            contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
            contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
        }
        return contextAttributes;
    };
}

OAuth2AccessToken は次のようにして入手できます。

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientManager authorizedClientManager;

    @GetMapping("/")
    public String index(Authentication authentication,
                        HttpServletRequest servletRequest,
                        HttpServletResponse servletResponse) {

        OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
                .principal(authentication)
                .attributes(attrs -> {
                    attrs.put(HttpServletRequest.class.getName(), servletRequest);
                    attrs.put(HttpServletResponse.class.getName(), servletResponse);
                })
                .build();
        OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}
HttpServletRequest と HttpServletResponse はどちらもオプション属性です。指定しない場合、RequestContextHolder.getRequestAttributes() を使用して ServletRequestAttributes がデフォルトになります。

12.2.3. 追加機能

認可されたクライアントの解決

@RegisteredOAuth2AuthorizedClient アノテーションは、メソッドパラメーターをタイプ OAuth2AuthorizedClient の引数値に解決する機能を提供します。これは、OAuth2AuthorizedClientManager または OAuth2AuthorizedClientService を使用して OAuth2AuthorizedClient にアクセスするのに比べて、便利な代替手段です。

@Controller
public class OAuth2ClientController {

    @GetMapping("/")
    public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "index";
    }
}

@RegisteredOAuth2AuthorizedClient アノテーションは OAuth2AuthorizedClientArgumentResolver によって処理されます。OAuth2AuthorizedClientArgumentResolverOAuth2AuthorizedClientManager を直接使用するため、その機能を継承します。

12.2.4. サーブレット環境の WebClient 統合

OAuth 2.0 クライアントサポートは、ExchangeFilterFunction を使用して WebClient と統合します。

ServletOAuth2AuthorizedClientExchangeFilterFunction は、OAuth2AuthorizedClient を使用し、関連する OAuth2AccessToken をベアラートークンとして含めることにより、保護されたリソースをリクエストするためのシンプルなメカニズムを提供します。OAuth2AuthorizedClientManager を直接使用するため、次の機能を継承します。

  • クライアントがまだ認可されていない場合、OAuth2AccessToken がリクエストされます。

    • authorization_code - フローを開始するために認可リクエストリダイレクトをトリガーする

    • client_credentials - アクセストークンはトークンエンドポイントから直接取得されます

    • password - アクセストークンはトークンエンドポイントから直接取得されます

  • OAuth2AccessToken の有効期限が切れている場合、OAuth2AuthorizedClientProvider が認可を実行できる場合、リフレッシュ(またはリフレッシュ)されます。

次のコードは、OAuth 2.0 クライアントサポートを使用して WebClient を構成する方法の例を示しています。

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
認定クライアントの提供

ServletOAuth2AuthorizedClientExchangeFilterFunction は、ClientRequest.attributes() (リクエスト属性)から OAuth2AuthorizedClient を解決することにより、(リクエストに)使用するクライアントを決定します。

次のコードは、OAuth2AuthorizedClient をリクエスト属性として設定する方法を示しています。

@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
    String resourceUri = ...

    String body = webClient
            .get()
            .uri(resourceUri)
            .attributes(oauth2AuthorizedClient(authorizedClient))   (1)
            .retrieve()
            .bodyToMono(String.class)
            .block();

    ...

    return "index";
}
1oauth2AuthorizedClient() は ServletOAuth2AuthorizedClientExchangeFilterFunction の static メソッドです。

次のコードは、ClientRegistration.getRegistrationId() をリクエスト属性として設定する方法を示しています。

@GetMapping("/")
public String index() {
    String resourceUri = ...

    String body = webClient
            .get()
            .uri(resourceUri)
            .attributes(clientRegistrationId("okta"))   (1)
            .retrieve()
            .bodyToMono(String.class)
            .block();

    ...

    return "index";
}
1clientRegistrationId() は ServletOAuth2AuthorizedClientExchangeFilterFunction の static メソッドです。
承認済みクライアントのデフォルト設定

OAuth2AuthorizedClient または ClientRegistration.getRegistrationId() のいずれもリクエスト属性として提供されない場合、ServletOAuth2AuthorizedClientExchangeFilterFunction はその構成に応じて、使用するデフォルトクライアントを決定できます。

setDefaultOAuth2AuthorizedClient(true) が構成され、ユーザーが HttpSecurity.oauth2Login() を使用して認証した場合、現在の OAuth2AuthenticationToken に関連付けられた OAuth2AccessToken が使用されます。

次のコードは、特定の構成を示しています。

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultOAuth2AuthorizedClient(true);
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意が必要です。

あるいは、setDefaultClientRegistrationId("okta") が有効な ClientRegistration で構成されている場合、OAuth2AuthorizedClient に関連付けられた OAuth2AccessToken が使用されます。

次のコードは、特定の構成を示しています。

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("okta");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}
すべての HTTP リクエストはアクセストークンを受け取るため、この機能には注意が必要です。

12.3. OAuth 2.0 リソースサーバー

Spring Security は、2 つの形式の OAuth 2.0 ベアラートークン (英語) を使用したエンドポイントの保護をサポートしています。

これは、アプリケーションが権限管理を認可サーバー (英語) (Okta や Ping Identity など)に委譲している場合に便利です。この認可サーバーは、リソースサーバーがリクエストを認可するために調べることができます。

JWT (GitHub) Opaque トークン (GitHub) の両方の作業サンプルが Spring Security リポジトリ (GitHub) で利用可能です。

12.3.1. 依存関係

ほとんどのリソースサーバーサポートは spring-security-oauth2-resource-server に収集されます。ただし、JWT のデコードと検証のサポートは spring-security-oauth2-jose にあります。つまり、JWT でエンコードされたベアラートークンをサポートする作業リソースサーバーを使用するには両方が必要です。

12.3.2. JWT の最小構成

Spring Boot を使用する場合、アプリケーションをリソースサーバーとして構成するには、2 つの基本的な手順が必要です。最初に、必要な依存関係を含め、2 番目に認可サーバーの場所を示します。

認可サーバーの指定

Spring Boot アプリケーションで、使用する認可サーバーを指定するには、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer

ここで、https://idp.example.com/issuer (英語)  は、認可サーバーが発行する JWT トークンの iss クレームに含まれる値です。リソースサーバーは、このプロパティを使用して、さらに自己構成を行い、認可サーバーの公開キーを検出し、受信 JWT を検証します。

issuer-uri プロパティを使用するには、https://idp.example.com/issuer/.well-known/openid-configuration (英語) https://idp.example.com/.well-known/openid-configuration/issuer (英語) または https://idp.example.com/.well-known/oauth-authorization-server/issuer (英語)  のいずれかが認可サーバーでサポートされるエンドポイントであることも真でなければなりません。このエンドポイントは、プロバイダー構成 (英語) エンドポイントまたは認可サーバーのメタデータ (英語) エンドポイントと呼ばれます。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係を使用すると、Resource Server は自動的に JWT エンコードされたベアラートークンを検証するように自身を構成します。

これは、決定論的な起動プロセスを通じてこれを実現します。

  1. プロバイダー構成または認可サーバーメタデータエンドポイントをヒットし、jwks_url プロパティのレスポンスを処理する

  2. 有効な公開鍵について jwks_url を照会するための検証戦略を構成する

  3. https://idp.example.com (英語) に対して各 JWT iss クレームを検証する検証戦略を構成します。

このプロセスの結果、リソースサーバーが正常に起動するには、認可サーバーが起動してリクエストを受信する必要があります。

リソースサーバーがクエリを実行したときに認可サーバーがダウンした場合(適切なタイムアウトが与えられた場合)、起動は失敗します。
ランタイムの期待

アプリケーションが起動すると、Resource Server は Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

整形式の JWT が与えられると、リソースサーバーは次のことを行います。

  1. 起動時に jwks_url エンドポイントから取得され、JWT と照合される公開鍵に対して署名を検証する

  2. JWT の exp および nbf タイムスタンプと、JWT の iss クレームを検証します。

  3. 各スコープを接頭辞 SCOPE_ を持つ機関にマップします。

認可サーバーが新しい鍵を使用できるようになると、Spring Security は JWT の検証に使用される鍵を自動的にローテーションします。

デフォルトでは、結果の Authentication#getPrincipal は Spring Security Jwt オブジェクトであり、Authentication#getName は JWT の sub プロパティ(存在する場合)にマップします。

ここから、次へのジャンプを検討してください。

12.3.3. 認可サーバー JWK セット Uri を直接指定する

認可サーバーが構成エンドポイントをサポートしていない場合、またはリソースサーバーが認可サーバーから独立して起動できる必要がある場合は、jwk-set-uri も提供できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
JWK Set uri は標準化されていませんが、通常は認可サーバーのドキュメントに記載されています

リソースサーバーは起動時に認可サーバーに ping を実行しません。issuer-uri を引き続き指定して、Resource Server が受信 JWT で iss クレームを検証するようにします。

このプロパティは、DSL で直接指定することもできます。

12.3.4. ブート自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する WebSecurityConfigurerAdapter です。spring-security-oauth2-jose を含めると、この WebSecurityConfigurerAdapter は次のようになります。

例 96: デフォルトの JWT 設定
Java
protected void configure(HttpSecurity http) {
    http
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
Kotlin
fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
}

アプリケーションが WebSecurityConfigurerAdapter Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

例 97: カスタム JWT 設定
Java
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(myConverter())
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("SCOPE_message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
    }
}

上記では、/messages/ で始まる URL の message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例:Spring Boot が 2 番目に作成する @Bean は、String トークンを Jwt の検証済みインスタンスにデコードする JwtDecoder です。

例 98: JWT デコーダー
@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
JwtDecoders#fromIssuerLocation(Javadoc)  を呼び出すと、JWK セット Uri を取得するためにプロバイダー構成または認可サーバーメタデータエンドポイントが呼び出されます。

アプリケーションが JwtDecoder Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は jwkSetUri() を使用してオーバーライドするか、decoder() を使用して置き換えることができます。

または、Spring Boot をまったく使用していない場合は、これらのコンポーネント(フィルターチェーンと JwtDecoder)の両方を XML で指定できます。

フィルターチェーンは次のように指定されます。

例 99: デフォルトの JWT 設定
XML
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="jwtDecoder"/>
    </oauth2-resource-server>
</http>

JwtDecoder は次のようになります。

例 100: JWT デコーダー
XML
<bean id="jwtDecoder"
        class="org.springframework.security.oauth2.jwt.JwtDecoders"
        factory-method="fromIssuerLocation">
    <constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>
jwkSetUri() を使用する

認可サーバーの JWK Set Uri は、構成プロパティとして構成することも、DSL で提供することもできます。

例 101: JWK Set Uri Configuration
Java
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
                }
            }
        }
    }
}
XML
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

jwkSetUri() の使用は、構成プロパティよりも優先されます。

decoder() を使用する

jwkSetUri() よりも強力なのは decoder() です。これは、JwtDecoder のブート自動構成を完全に置き換えます。

例 102: JWT デコーダー構成
Java
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(myCustomDecoder())
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = myCustomDecoder()
                }
            }
        }
    }
}
XML
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="myCustomDecoder"/>
    </oauth2-resource-server>
</http>

これは、validationmapping、または request timeouts のようなより詳細な構成が必要な場合に便利です。

JwtDecoder@Bean を公開する

または、JwtDecoder@Bean を公開すると、decoder() と同じ効果があります。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}

12.3.5. 信頼できるアルゴリズムの構成

デフォルトでは、NimbusJwtDecoder、リソースサーバーは、RS256 を使用したトークンのみを信頼および検証します。

これは、Spring BootNimbusJwtDecoder ビルダー、または JWK セットレスポンスからカスタマイズできます。

Spring Boot 経由

アルゴリズムを設定する最も簡単な方法は、プロパティとしてです:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithm: RS512
          jwk-set-uri: https://idp.example.org/.well-known/jwks.json
ビルダーを使用する

ただし、より強力にするには、NimbusJwtDecoder に同梱されているビルダーを使用できます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).build();
}

jwsAlgorithm を複数回呼び出すと、NimbusJwtDecoder は次のように複数のアルゴリズムを信頼するように構成されます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
}

または、jwsAlgorithms を呼び出すことができます。

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(EC512);
            }).build();
}
JWK Set レスポンスから

Spring Security の JWT サポートは Nimbus に基づいているため、その優れた機能もすべて使用できます。

例:Nimbus には、JWK Set URI レスポンスに基づいてアルゴリズムのセットを選択する JWSKeySelector 実装があります。これを使用して、NimbusJwtDecoder を次のように生成できます。

@Bean
public JwtDecoder jwtDecoder() {
    // makes a request to the JWK Set endpoint
    JWSKeySelector<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor<>();
    jwtProcessor.setJWSKeySelector(jwsKeySelector);

    return new NimbusJwtDecoder(jwtProcessor);
}

12.3.6. 単一の非対称キーを信頼する

JWK Set エンドポイントでリソースサーバーをバッキングするよりも簡単なのは、RSA 公開キーをハードコードすることです。公開鍵は、Spring Boot またはビルダーを使用するを介して提供できます。

Spring Boot 経由

Spring Boot を介したキーの指定は非常に簡単です。キーの場所は次のように指定できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-key.pub

または、より洗練されたルックアップを可能にするために、RsaKeyConversionServicePostProcessor を後処理できます。

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
    return beanFactory ->
        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
                .setResourceLoader(new CustomResourceLoader());
}

キーの場所を指定します。

key.location: hfds://my-key.pub

そして、値をオートワイヤーします。

@Value("${key.location}")
RSAPublicKey key;
ビルダーを使用する

RSAPublicKey を直接接続するには、次のように適切な NimbusJwtDecoder ビルダーを使用するだけです。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
}

12.3.7. 単一の対称キーを信頼する

単一の対称キーの使用も簡単です。次のように、SecretKey をロードして適切な NimbusJwtDecoder ビルダーを使用するだけです。

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withSecretKey(this.key).build();
}

12.3.8. 認可の構成

OAuth 2.0 認可サーバーから発行される JWT は通常、scope または scp 属性のいずれかを持ち、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、エンドポイントまたはメソッドを JWT から派生したスコープで保護するには、対応する式に次のプレフィックスを含める必要があります。

例 103: 認可設定
Java
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    }
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/contacts/**", hasAuthority("SCOPE_contacts"))
                authorize("/messages/**", hasAuthority("SCOPE_messages"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt { }
            }
        }
    }
}
XML
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
権限の手動抽出

ただし、このデフォルトでは不十分な状況がいくつかあります。例:一部の認可サーバーは scope 属性を使用せず、独自のカスタム属性を持っています。または、リソースサーバーは、属性または属性の構成を内部化された機関に適合させる必要がある場合もあります。

この目的のために、DSL は Jwt を Authentication に変換することを担当する jwtAuthenticationConverter() を公開します。

その構成の一部として、Jwt から認可された機関の Collection に移行するための補助コンバーターを提供できます。認可サーバーが authorities と呼ばれるカスタムクレームで権限と通信するとします。その場合、JwtAuthenticationConverter がインスペクションする必要があるクレームを次のように構成できます。

例 104: 権限クレーム構成
Java
@EnableWebSecurity
public class CustomAuthoritiesClaimName extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
    }
}

JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

    JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
    return jwtAuthenticationConverter;
}
XML
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authoritiesClaimName" value="authorities"/>
</bean>

権限のプレフィックスを異なるように構成することもできます。各権限の前に SCOPE_ を付ける代わりに、次のように ROLE_ に変更できます。

例 105: 機関のプレフィックス設定
Java
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
    return jwtAuthenticationConverter;
}
XML
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authorityPrefix" value="ROLE_"/>
</bean>

または、JwtGrantedAuthoritiesConverter#setAuthorityPrefix("") を呼び出して、プレフィックスを完全に削除できます。

柔軟性を高めるため、DSL はコンバーターを Converter<Jwt, AbstractAuthenticationToken> を実装するクラスに完全に置き換えることをサポートしています。

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

12.3.9. 検証の構成

認可サーバーの発行者 URI を示す最小限の Spring Boot 構成を使用して、リソースサーバーは、iss クレームと exp および nbf タイムスタンプクレームをデフォルトで検証します。

検証をカスタマイズする必要がある状況では、Resource Server には 2 つの標準バリデーターが付属しており、カスタム OAuth2TokenValidator インスタンスも受け入れます。

タイムスタンプ検証のカスタマイズ

通常、JWT には有効期間があり、ウィンドウの開始は nbf クレームで示され、終了は exp クレームで示されます。

ただし、すべてのサーバーでクロックドリフトが発生する可能性があります。これにより、あるサーバーではトークンが期限切れになり、別のサーバーでは期限切れになります。これにより、分散システムでコラボレーションサーバーの数が増えると、実装の胸焼けが発生する可能性があります。

リソースサーバーは JwtTimestampValidator を使用してトークンの有効期間を検証し、clockSkew で構成して上記の問題を軽減できます。

@Bean
JwtDecoder jwtDecoder() {
     NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
             JwtDecoders.fromIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new IssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
デフォルトでは、Resource Server は 30 秒のクロックスキューを設定します。
カスタム検証ツールの構成

aud クレームのチェックの追加は、OAuth2TokenValidator API を使用すると簡単です。

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}

または、より制御するために、独自の OAuth2TokenValidator を実装できます。

static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null);

    @Override
    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

// ...

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new AudienceValidator();
}

次に、リソースサーバーに追加するには、JwtDecoder インスタンスを指定するだけです。

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

12.3.10. クレームセットマッピングの構成

Spring Security は、Nimbus (英語) ライブラリを使用して、JWT の構文解析と署名の検証を行います。Spring Security は、各フィールド値の Nimbus の解釈と、それぞれを Java 型に強制する方法の対象となります。

例:Nimbus は Java 7 と互換性があるため、タイムスタンプフィールドを表すために Instant を使用しません。

また、別のライブラリを使用したり、JWT 処理に使用したりすることもできます。これにより、調整が必要な独自の強制決定を行うことができます。

または、非常に単純に、リソースサーバーはドメイン固有の理由で JWT にクレームを追加または削除したい場合があります。

これらの目的のために、Resource Server は MappedJwtClaimSetConverter を使用した JWT クレームセットのマッピングをサポートしています。

単一クレームの変換のカスタマイズ

デフォルトでは、MappedJwtClaimSetConverter はクレームを次のタイプに強制しようとします。

請求

Java タイプ

aud

Collection<String>

exp

Instant

iat

Instant

iss

String

jti

String

nbf

Instant

sub

String

MappedJwtClaimSetConverter.withDefaults を使用して、個々の申し立ての変換戦略を構成できます。

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setClaimSetConverter(converter);

    return jwtDecoder;
}

これにより、sub のデフォルトクレームコンバーターがオーバーライドされることを除き、すべてのデフォルトが保持されます。

クレームを追加する

MappedJwtClaimSetConverter は、たとえば既存のシステムに適応するために、カスタムクレームを追加するためにも使用できます。

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
申し立ての削除

また、同じ API を使用して、クレームを削除することも簡単です。

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
クレームの名前を変更する

一度に複数のクレームを参照したり、クレームの名前を変更したりするような、より洗練されたシナリオでは、Resource Server は Converter<Map<String, Object>, Map<String,Object>> を実装するクラスを受け入れます。

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}

そして、インスタンスは通常のように提供できます:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}

12.3.11. タイムアウトの構成

デフォルトでは、Resource Server は認可サーバーとの調整にそれぞれ 30 秒の接続およびソケットタイムアウトを使用します。

これはいくつかのシナリオでは短すぎるかもしれません。さらに、バックオフや発見などのより高度なパターンは考慮されません。

Resource Server が認可サーバーに接続する方法を調整するために、NimbusJwtDecoder は RestOperations のインスタンスを受け入れます。

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectionTimeout(60000)
            .setReadTimeout(60000)
            .build();

    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
    return jwtDecoder;
}

12.3.12. イントロスペクションの最小構成

通常、Opaque トークンは、認可サーバーによってホストされる OAuth 2.0 イントロスペクションエンドポイント (英語) を介して検証できます。これは、失効が必要な場合に便利です。

Spring Boot を使用する場合、イントロスペクションを使用するリソースサーバーとしてアプリケーションを構成するには、2 つの基本的な手順が必要です。まず、必要な依存関係を含め、次に、イントロスペクションエンドポイントの詳細を示します。

認可サーバーの指定

イントロスペクションエンドポイントの場所を指定するには、次のようにします。

security:
  oauth2:
    resourceserver:
      opaque-token:
        introspection-uri: https://idp.example.com/introspect
        client-id: client
        client-secret: secret

ここで、https://idp.example.com/introspect (英語)  は認証サーバーによってホストされるイントロスペクションエンドポイントであり、client-id および client-secret はそのエンドポイントをヒットするために必要な資格情報です。

リソースサーバーはこれらのプロパティを使用して、さらに自己構成し、受信 JWT を検証します。

イントロスペクションを使用する場合、認可サーバーの言葉は法律です。認可サーバーがトークンが有効であるとレスポンスした場合、有効です。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係が使用されると、リソースサーバーは自動的に不透明なベアラートークンを検証するように構成します。

この起動プロセスは、エンドポイントを検出する必要がなく、追加の検証ルールが追加されないため、JWT よりもかなり単純です。

ランタイムの期待

アプリケーションが起動すると、Resource Server は Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

Opaque トークンを指定すると、リソースサーバーは

  1. 指定された資格情報とトークンを使用して、指定されたイントロスペクションエンドポイントを照会する

  2. { 'active' : true } 属性のレスポンスをインスペクションする

  3. 各スコープをプレフィックス SCOPE_ を持つ機関にマップする

結果として得られる Authentication#getPrincipal は、デフォルトでは Spring Security OAuth2AuthenticatedPrincipal(Javadoc)  オブジェクトであり、Authentication#getName はトークンの sub プロパティ(存在する場合)にマップします。

ここから、次の場所にジャンプできます。

12.3.13. 認証後の属性の検索

トークンが認証されると、BearerTokenAuthentication のインスタンスが SecurityContext に設定されます。

つまり、構成で @EnableWebMvc を使用する場合、@Controller メソッドで使用できます。

@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
    return authentication.getTokenAttributes().get("sub") + " is the subject";
}

BearerTokenAuthentication は OAuth2AuthenticatedPrincipal を保持するため、コントローラーメソッドでも使用できることも意味します。

@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
    return principal.getAttribute("sub") + " is the subject";
}
SpEL を介した属性の検索

もちろん、これは、SpEL を介して属性にアクセスできることも意味します。

例: @PreAuthorize アノテーションを使用できるように @EnableGlobalMethodSecurity を使用する場合、次のことができます。

@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public String forFoosEyesOnly() {
    return "foo";
}

12.3.14. ブート自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する WebSecurityConfigurerAdapter です。Opaque トークンを使用する場合、この WebSecurityConfigurerAdapter は次のようになります。

例 106: Opaque トークンのデフォルト設定
Java
protected void configure(HttpSecurity http) {
    http
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
}
Kotlin
override fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            opaqueToken { }
        }
    }
}

アプリケーションが WebSecurityConfigurerAdapter Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

例 107: カスタム Opaque トークン構成
Java
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspector(myIntrospector())
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("SCOPE_message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                opaqueToken {
                    introspector = myIntrospector()
                }
            }
        }
    }
}

上記では、/messages/ で始まる URL の message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例:2 番目の @Bean Spring Boot が作成する OpaqueTokenIntrospector は、String トークンを OAuth2AuthenticatedPrincipal の検証済みインスタンスにデコードします。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

アプリケーションが OpaqueTokenIntrospector Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は、introspectionUri() および introspectionClientCredentials() を使用してオーバーライドするか、introspector() を使用して置き換えることができます。

または、Spring Boot をまったく使用していない場合は、これらのコンポーネント(フィルターチェーンと OpaqueTokenIntrospector)の両方を XML で指定できます。

フィルターチェーンは次のように指定されます。

例 108: Opaque トークンのデフォルト設定
XML
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <opaque-token introspector-ref="opaqueTokenIntrospector"/>
    </oauth2-resource-server>
</http>

OpaqueTokenIntrospector は次のようになります。

例 109: Opaque トークンイントロスペクター
XML
<bean id="opaqueTokenIntrospector"
        class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.introspection_uri}"/>
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_id}"/>
    <constructor-arg value="${spring.security.oauth2.resourceserver.opaquetoken.client_secret}"/>
</bean>
introspectionUri() を使用する

認可サーバーの Introspection Uri は、構成プロパティとして構成するか、DSL で提供できます。

例 110: イントロスペクション URI 設定
Java
@EnableWebSecurity
public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspectionUri("https://idp.example.com/introspect")
                    .introspectionClientCredentials("client", "secret")
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredIntrospectionUri : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                opaqueToken {
                    introspectionUri = "https://idp.example.com/introspect"
                    introspectionClientCredentials("client", "secret")
                }
            }
        }
    }
}
XML
<bean id="opaqueTokenIntrospector"
        class="org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector">
    <constructor-arg value="https://idp.example.com/introspect"/>
    <constructor-arg value="client"/>
    <constructor-arg value="secret"/>
</bean>

introspectionUri() の使用は、構成プロパティよりも優先されます。

introspector() を使用する

introspectionUri() よりも強力なのは introspector() です。これは、OpaqueTokenIntrospector のブート自動構成を完全に置き換えます。

例 111: イントロスペクターの設定
Java
@EnableWebSecurity
public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .opaqueToken(opaqueToken -> opaqueToken
                    .introspector(myCustomIntrospector())
                )
            );
    }
}
Kotlin
@EnableWebSecurity
class DirectlyConfiguredIntrospector : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                opaqueToken {
                    introspector = myCustomIntrospector()
                }
            }
        }
    }
}
XML
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <opaque-token introspector-ref="myCustomIntrospector"/>
    </oauth2-resource-server>
</http>

これは、権限マッピングJWT の取り消し、またはリクエストタイムアウトなどのより深い構成が必要な場合に便利です。

OpaqueTokenIntrospector@Bean を公開する

または、OpaqueTokenIntrospector@Bean を公開すると、introspector() と同じ効果があります。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

12.3.15. 認可の構成

OAuth 2.0 Introspection エンドポイントは、通常、scope 属性を返し、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、Opaque トークンから派生したスコープを持つエンドポイントまたはメソッドを保護するには、対応する式に次のプレフィックスを含める必要があります。

例 112: 認可 Opaque トークンの構成
Java
@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests(authorizeRequests -> authorizeRequests
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
    }
}
XML
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <opaque-token introspector-ref="opaqueTokenIntrospector"/>
    </oauth2-resource-server>
</http>

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
権限の手動抽出

デフォルトでは、Opaque トークンサポートは、イントロスペクションレスポンスからスコープ要求を抽出し、個々の GrantedAuthority インスタンスに解析します。

例:イントロスペクションのレスポンスが次の場合:

{
    "active" : true,
    "scope" : "message:read message:write"
}

次に、Resource Server は、message:read 用と message:write 用の 2 つの権限を持つ Authentication を生成します。

これはもちろん、属性セットを見て独自の方法で変換するカスタム OpaqueTokenIntrospector を使用してカスタマイズできます。

public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        return new DefaultOAuth2AuthenticatedPrincipal(
                principal.getName(), principal.getAttributes(), extractAuthorities(principal));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

その後、このカスタムイントロスペクターは、@Bean として公開するだけで構成できます。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new CustomAuthoritiesOpaqueTokenIntrospector();
}

12.3.16. タイムアウトの構成

デフォルトでは、Resource Server は認可サーバーとの調整にそれぞれ 30 秒の接続およびソケットタイムアウトを使用します。

これはいくつかのシナリオでは短すぎるかもしれません。さらに、バックオフや発見などのより高度なパターンは考慮されません。

Resource Server が認可サーバーに接続する方法を調整するために、NimbusOpaqueTokenIntrospector は RestOperations のインスタンスを受け入れます。

@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .basicAuthentication(clientId, clientSecret)
            .setConnectionTimeout(60000)
            .setReadTimeout(60000)
            .build();

    return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}

12.3.17. JWT でのイントロスペクションの使用

よくある質問は、イントロスペクションが JWT と互換性があるかどうかです。Spring Security の Opaque トークンサポートは、トークンの形式を気にしないように設計されています。提供されたイントロスペクションエンドポイントにトークンを喜んで渡します。

JWT が取り消された場合に備えて、リクエストごとに認可サーバーで確認する必要がある要件があるとしましょう。

トークンに JWT 形式を使用している場合でも、検証方法はイントロスペクションです。つまり、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://idp.example.org/introspection
          client-id: client
          client-secret: secret

この場合、結果の Authentication は BearerTokenAuthentication になります。対応する OAuth2AuthenticatedPrincipal の属性は、イントロスペクションエンドポイントによって返されたものです。

しかし、奇妙なことに、イントロスペクションエンドポイントは、トークンがアクティブであるかどうかのみを返します。それで?

この場合、エンドポイントにヒットするカスタム OpaqueTokenIntrospector を作成できますが、返されたプリンシパルを更新して、属性として JWT クレームを取得します。

public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor());

    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
        try {
            Jwt jwt = this.jwtDecoder.decode(token);
            return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES);
        } catch (JwtException e) {
            throw new OAuth2IntrospectionException(e);
        }
    }

    private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor<SecurityContext> {
        JWTClaimsSet process(SignedJWT jwt, SecurityContext context)
                throws JOSEException {
            return jwt.getJWTClaimSet();
        }
    }
}

その後、このカスタムイントロスペクターは、@Bean として公開するだけで構成できます。

@Bean
public OpaqueTokenIntrospector introspector() {
    return new JwtOpaqueTokenIntropsector();
}

12.3.18. /userinfo エンドポイントの呼び出し

一般的に、リソースサーバーは、基になるユーザーを気にするのではなく、付与された権限を気にします。

ただし、認証ステートメントをユーザーに結び付けることが重要な場合があります。

適切な ClientRegistrationRepository をセットアップして、アプリケーションが spring-security-oauth2-client も使用している場合、これはカスタム OpaqueTokenIntrospector で非常に簡単です。以下のこの実装は、3 つのことを行います。

  • トークンの有効性を確認するために、イントロスペクションエンドポイントに委譲する

  • /userinfo エンドポイントに関連付けられた適切なクライアント登録を検索する

  • /userinfo エンドポイントからレスポンスを呼び出して返します

public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService();

    private final ClientRegistrationRepository repository;

    // ... constructor

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        Instant issuedAt = authorized.getAttribute(ISSUED_AT);
        Instant expiresAt = authorized.getAttribute(EXPIRES_AT);
        ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id");
        OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
        OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token);
        return this.oauth2UserService.loadUser(oauth2UserRequest);
    }
}

spring-security-oauth2-client を使用していない場合でも、非常に簡単です。WebClient の独自のインスタンスで /userinfo を呼び出すだけです。

public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
    private final OpaqueTokenIntrospector delegate =
            new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final WebClient rest = WebClient.create();

    @Override
    public OAuth2AuthenticatedPrincipal introspect(String token) {
        OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
        return makeUserInfoRequest(authorized);
    }
}

いずれにしても、OpaqueTokenIntrospector を作成したら、それを @Bean として公開してデフォルトをオーバーライドする必要があります。

@Bean
OpaqueTokenIntrospector introspector() {
    return new UserInfoOpaqueTokenIntrospector(...);
}

12.3.19. JWT と Opaque トークンの両方をサポート

場合によっては、両方の種類のトークンにアクセスする必要があります。例:1 つのテナントが JWT を発行し、他のテナントが Opaque トークンを発行する複数のテナントをサポートできます。

リクエスト時にこの決定を行う必要がある場合は、AuthenticationManagerResolver を使用して次のように実現できます。

@Bean
AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver() {
    BearerTokenResolver bearerToken = new DefaultBearerTokenResolver();
    JwtAuthenticationProvider jwt = jwt();
    OpaqueTokenAuthenticationProvider opaqueToken = opaqueToken();

    return request -> {
        if (useJwt(request)) {
            return jwt::authenticate;
        } else {
            return opaqueToken::authenticate;
        }
    }
}
useJwt(HttpServletRequest) の実装は、パスなどのカスタムリクエストマテリアルに依存する可能性があります。

そして、DSL でこの AuthenticationManagerResolver を指定します。

例 113: 認証マネージャーリゾルバー
Java
http
    .authorizeRequests(authorize -> authorize
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
        .authenticationManagerResolver(this.tokenAuthenticationManagerResolver)
    );
XML
<http>
    <oauth2-resource-server authentication-manager-resolver-ref="tokenAuthenticationManagerResolver"/>
</http>

12.3.20. マルチテナンシー

リソースサーバーは、何らかのテナント ID をキーとするベアラートークンを検証するための複数の戦略がある場合、マルチテナントと見なされます。

例:リソースサーバーは、2 つの異なる認可サーバーからベアラートークンを受け入れる場合があります。または、認可サーバーが複数の発行者を表している場合があります。

いずれの場合も、実行する必要がある 2 つの事柄と、それらの選択方法に関連するトレードオフがあります。

  1. テナントを解決する

  2. テナントを伝播する

クレームによるテナントの解決

テナントを区別する 1 つの方法は、発行者の主張によるものです。発行者の主張には署名された JWT が伴うため、これは次のように JwtIssuerAuthenticationManagerResolver で実行できます。

例 114: JWT クレームによるマルチテナントテナント
Java
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
    ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");

http
    .authorizeRequests(authorize -> authorize
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
        .authenticationManagerResolver(authenticationManagerResolver)
    );
XML
<http>
    <oauth2-resource-server authentication-manager-resolver-ref="authenticationManagerResolver"/>
</http>

<bean id="authenticationManagerResolver"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver">
    <constructor-arg>
        <list>
            <value>https://idp.example.org/issuerOne</value>
            <value>https://idp.example.org/issuerTwo</value>
        </list>
    </constructor-arg>
</bean>

発行者エンドポイントが遅延ロードされるため、これは素晴らしいことです。実際、対応する JwtAuthenticationProvider は、対応する発行者との最初のリクエストが送信されたときにのみインスタンス化されます。これにより、アプリケーションが起動し、使用可能になっている認可サーバーとは無関係に起動できるようになります。

ダイナミックテナント

もちろん、新しいテナントが追加されるたびにアプリケーションを再起動したくない場合があります。この場合、AuthenticationManager インスタンスのリポジトリを使用して JwtIssuerAuthenticationManagerResolver を構成できます。これは、次のように実行時に編集できます。

private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
    JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
            (JwtDecoders.fromIssuerLocation(issuer));
    authenticationManagers.put(issuer, authenticationProvider::authenticate);
}

// ...

JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
        new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);

http
    .authorizeRequests(authorize -> authorize
        .anyRequest().authenticated()
    )
    .oauth2ResourceServer(oauth2 -> oauth2
        .authenticationManagerResolver(authenticationManagerResolver)
    );

この場合、発行者から AuthenticationManager を取得する方法で JwtIssuerAuthenticationManagerResolver を作成します。このアプローチにより、実行時にリポジトリ(スニペットで Map として表示)に要素を追加および削除できます。

単に発行者を取得し、そこから AuthenticationManager を作成することは安全ではありません。発行者は、ホワイトリストのような信頼できるソースからコードが検証できるものでなければなりません。
クレームの解析は 1 回のみ

この戦略は単純ですが、AuthenticationManagerResolver によって JWT が 1 回解析され、その後リクエストで JwtDecoder によって再度解析されるというトレードオフがあります。

Nimbus の JWTClaimSetAwareJWSKeySelector を使用して JwtDecoder を直接設定することにより、この余分な解析を軽減できます。

@Component
public class TenantJWSKeySelector
    implements JWTClaimSetAwareJWSKeySelector<SecurityContext> {

    private final TenantRepository tenants; (1)
    private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); (2)

    public TenantJWSKeySelector(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
            throws KeySourceException {
        return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
                .selectJWSKeys(jwsHeader, securityContext);
    }

    private String toTenant(JWTClaimsSet claimSet) {
        return (String) claimSet.getClaim("iss");
    }

    private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
        return Optional.ofNullable(this.tenantRepository.findById(tenant)) (3)
                .map(t -> t.getAttrbute("jwks_uri"))
                .map(this::fromUri)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    }

    private JWSKeySelector<SecurityContext> fromUri(String uri) {
        try {
            return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); (4)
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}
1 テナント情報の仮想ソース
2 テナント識別子をキーとする「JWKKeySelector」のキャッシュ
3 テナントのルックアップは、単に JWK Set エンドポイントをオンザフライで計算するよりも安全です - ルックアップはテナントホワイトリストとして機能する
4JWK Set エンドポイントから返されるキーの種類を介して JWSKeySelector を作成します。ここでの遅延検索は、起動時にすべてのテナントを構成する必要がないことを意味する

上記のキーセレクターは、多くのキーセレクターの構成です。JWT の iss クレームに基づいて、使用するキーセレクターを選択します。

このアプローチを使用するには、トークンの署名の一部としてクレームセットを含めるように認可サーバーが構成されていることを確認してください。これがなければ、発行者が悪役によって改ざんされていないという保証はありません。

次に、JWTProcessor を作成できます。

@Bean
JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
    ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor();
    jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
    return jwtProcessor;
}

すでに見たように、テナント認識をこのレベルに下げるためのトレードオフは、より多くの構成です。もう少しあります。

次に、発行者を検証していることを確認します。ただし、発行者は JWT ごとに異なる可能性があるため、テナント対応バリデーターも必要になります。

@Component
public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
    private final TenantRepository tenants;
    private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();

    public TenantJwtIssuerValidator(TenantRepository tenants) {
        this.tenants = tenants;
    }

    @Override
    public OAuth2TokenValidatorResult validate(Jwt token) {
        return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
                .validate(token);
    }

    private String toTenant(Jwt jwt) {
        return jwt.getIssuer();
    }

    private JwtIssuerValidator fromTenant(String tenant) {
        return Optional.ofNullable(this.tenants.findById(tenant))
                .map(t -> t.getAttribute("issuer"))
                .map(JwtIssuerValidator::new)
                .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
    }
}

テナント対応プロセッサとテナント対応バリデーターができたため、JwtDecoder の作成に進むことができます:

@Bean
JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
    NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
    OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
            (JwtValidators.createDefault(), this.jwtValidator);
    decoder.setJwtValidator(validator);
    return decoder;
}

テナントの解決についてお話ししました。

JWT クレーム以外のメソッドでテナントを解決することを選択した場合は、同じメソッドでダウンストリームリソースサーバーに対処する必要があります。例:サブドメインで解決する場合、同じサブドメインを使用してダウンストリームリソースサーバーをアドレス指定する必要がある場合があります。

ただし、ベアラートークンの要求によって解決する場合は、Spring Security のベアラートークン伝播のサポートについて学習してください。

12.3.21. ベアラートークンの解決

デフォルトでは、Resource Server は Authorization ヘッダーでベアラートークンを探します。ただし、これはいくつかの方法でカスタマイズできます。

カスタムヘッダーからベアラートークンを読み取る

例:カスタムヘッダーからベアラートークンを読み取る必要がある場合があります。これを実現するために、次の例に示すように、HeaderBearerTokenResolver インスタンスを DSL に接続できます。

例 115: カスタムベアラートークンヘッダー
Java
http
    .oauth2ResourceServer(oauth2 -> oauth2
        .bearerTokenResolver(new HeaderBearerTokenResolver("x-goog-iap-jwt-assertion"))
    );
XML
<http>
    <oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>

<bean id="bearerTokenResolver"
        class="org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver">
    <constructor-arg value="x-goog-iap-jwt-assertion"/>
</bean>
フォームパラメーターからベアラートークンを読み取る

または、以下に示すように、DefaultBearerTokenResolver を構成することで実行できるフォームパラメーターからトークンを読み取ることもできます。

例 116: フォームパラメーターベアラートークン
Java
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
http
    .oauth2ResourceServer(oauth2 -> oauth2
        .bearerTokenResolver(resolver)
    );
XML
<http>
    <oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>

<bean id="bearerTokenResolver"
        class="org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver">
    <property name="allowFormEncodedBodyParameter" value="true"/>
</bean>

12.3.22. ベアラートークンの伝播

リソースサーバーがトークンを検証したため、トークンをダウンストリームサービスに渡すと便利です。これは ServletBearerExchangeFilterFunction(Javadoc) で非常に簡単で、次の例で確認できます。

@Bean
public WebClient rest() {
    return WebClient.builder()
            .filter(new ServletBearerExchangeFilterFunction())
            .build();
}

上記の WebClient を使用してリクエストを実行すると、Spring Security は現在の Authentication を検索し、AbstractOAuth2Token(Javadoc)  資格情報を抽出します。次に、Authorization ヘッダーでそのトークンを伝搬します。

例 :

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .retrieve()
        .bodyToMono(String.class)
        .block()

https://other-service.example.com/endpoint (英語) を呼び出して、ベアラートークン Authorization ヘッダーを追加します。

この動作をオーバーライドする必要がある場所では、次のようにヘッダーを自分で指定するだけです。

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .headers(headers -> headers.setBearerAuth(overridingToken))
        .retrieve()
        .bodyToMono(String.class)
        .block()

この場合、フィルターはフォールバックし、リクエストを Web フィルターチェーンの残りの部分に単純に転送します。

OAuth 2.0 クライアントフィルター機能(Javadoc) とは異なり、このフィルター関数は、トークンが期限切れになった場合、トークンを更新しようとしません。このレベルのサポートを取得するには、OAuth 2.0 クライアントフィルターを使用してください。
RestTemplate サポート

現在のところ、ServletBearerExchangeFilterFunction に相当する RestTemplate はありませんが、独自のインターセプターを使用して、リクエストのベアラートークンを非常に簡単に伝達できます。

@Bean
RestTemplate rest() {
    RestTemplate rest = new RestTemplate();
    rest.getInterceptors().add((request, body, execution) -> {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            return execution.execute(request, body);
        }

        if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
            return execution.execute(request, body);
        }

        AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
        request.getHeaders().setBearerAuth(token.getTokenValue());
        return execution.execute(request, body);
    });
    return rest;
}
OAuth 2.0 承認済みクライアントマネージャー(Javadoc) とは異なり、このフィルターインターセプターは、トークンの有効期限が切れた場合にトークンを更新しようとしません。このレベルのサポートを取得するには、OAuth 2.0 承認済みクライアントマネージャーを使用してインターセプターを作成してください。

12.3.23. ベアラートークンエラー

ベアラートークンは、いくつかの理由で無効になる場合があります。例:トークンはアクティブではない可能性があります。

このような状況では、リソースサーバーは InvalidBearerTokenException をスローします。他の例外と同様に、これにより OAuth 2.0 Bearer Token エラーレスポンスが発生します。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

さらに、AuthenticationFailureBadCredentialsEvent として公開され、次のようにアプリケーションでリッスンできます

@Component
public class FailureEvents {
    @EventListener
    public void onFailure(AuthenticationFailureEvent failure) {
        if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
            // ... handle
        }
    }
}

13. SAML2

13.1. SAML 2.0 ログイン

SAML 2.0 ログイン、saml2Login() 機能は、SAML 2.0 ID プロバイダー(Okta、ADFS など)で既存のアカウントを使用してユーザーがアプリケーションにログインできるようにする機能をアプリケーションに提供します。

SAML 2.0 ログインは、SAML 2 プロファイル (英語) で指定されている Web ブラウザー SSO プロファイルを使用して実装されます。現在、実装は単純な認証スキームに制限されています。

13.1.1. Spring Security での SAML 2 サポート

SAML 2 サービスプロバイダー、SP(依存パーティ)のサポートは 2009 年から独立したプロジェクト (GitHub) として存在していました。ZZ の実装もベースに SAML 2.0 アイデンティティプロバイダーの実装を作成した Cloud Foundry ユーザーアカウントと認証サーバー (GitHub) を含め、1.0.x ブランチはまだ使用中です。

2018 年に、サービスプロバイダーとアイデンティティプロバイダー (GitHub) とスタンドアロンライブラリの両方の更新された実装を作成する実験を行いました。Spring Security チームである私たち Spring Security は、慎重かつ長期にわたる審議の結果、その努力を中止することにしました。この取り組みにより、そのスタンドアロン 1.0.x ライブラリの代替が作成されましたが、別のライブラリの上にライブラリを構築する必要はないと感じました。

代わりに、コア Spring Security (GitHub) の一部として SAML 2 認証のフレームワークサポートを提供することを選択しました。

13.1.2. Saml 2 ログイン - 高レベルの概念

saml2Login() は、認証プロバイダー、別名アサーティングパーティーから XML アサーションを受信するサービスプロバイダー、SP、依存パーティーである認証に焦点を当てて、SAML 2 機能セット (英語) の一部をサポートすることを目的としています。

SAML 2 ログインまたは認証は、SP が IDP からアサーションと呼ばれる XML メッセージを受信して検証するという概念です。

現在、2 つのサポートされている認証フローがあります

  1. IDP 開始フロー - 例:Okta に直接ログインし、認証対象の Web アプリケーションを選択します。IDP である Okta は、Web アプリケーションである SP にアサーションを送信します。

  2. SP 開始フロー - 例:Web アプリケーション(SP)にアクセスし、アプリケーションが IDP に認証リクエストを送信してアサーションをリクエストします。IDP での認証が成功すると、IDP はアサーションを SP に送信します。

13.1.3. Saml 2 ログイン - 現在の機能セット

  1. サービスプロバイダー(SP / 証明書利用者)は entityId = {baseUrl}/saml2/service-provider-metadata/{registrationId} によって識別されます

  2. {baseUrl}/login/saml2/sso/{registrationId} で Http-POST または Http-Redirect を介して SAML レスポンスに埋め込まれたアサーションを受信する

  3. レスポンスが署名されていない限り、アサーションに署名する必要があります

  4. 暗号化されたアサーションをサポート

  5. 暗号化された NameId 要素をサポート

  6. Converter<Assertion, Collection<? extends GrantedAuthority>> を使用して、アサーション属性を機関に抽出できます

  7. GrantedAuthoritiesMapper を使用して機関のマッピングとホワイトリストを許可する

  8. java.security.cert.X509Certificate 形式の公開鍵。

  9. AuthNRequest を介した SP 開始認証

Saml 2 ログイン - まだサポートされていません
  1. アサーション条件と属性をセッション機能にマッピングする (タイムアウト、追跡など)

  2. シングルログアウト

  3. 動的なメタデータの生成

  4. スタンドアロンアサーションの受信と検証 (レスポンスオブジェクトにラップされていません)

13.1.4. Saml 2 ログイン - Java 構成の概要

saml2Login() を Spring Security フィルターチェーンに追加するには、最小限の Java 構成には、SAML 構成と HttpSecurity.saml2Login() メソッドの呼び出しを含む構成リポジトリ RelyingPartyRegistrationRepository が必要です。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
        //SAML configuration
        //Mapping this application to one or more Identity Providers
        return new InMemoryRelyingPartyRegistrationRepository(...);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults())
        ;
    }
}

Bean 宣言は便利ですが、オプションのアプローチです。メソッド呼び出しを使用してリポジトリを直接接続できます

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(...)
            )
        ;
    }
}
RelyingPartyRegistration

RelyingPartyRegistration (GitHub) オブジェクトは、このアプリケーション、SP、およびアサーティングパーティ、IDP 間のマッピングを表します。

URI パターン

URI パターンは、受信リクエストに基づいて URI を自動的に生成するために頻繁に使用されます。saml2Login の URI パターンには、次の変数を含めることができます

  • baseUrl

  • registrationId

  • baseScheme

  • baseHost

  • basePort

次に例を示します : {baseUrl}/login/saml2/sso/{registrationId}

証明書利用者
  • registrationId - (必須)この構成マッピングの一意の識別子。この識別子は URI パスで使用される可能性があるため、URI エンコードが必要ないことに注意する必要があります。

  • localEntityIdTemplate - (オプション)受信リクエストに基づいてこのアプリケーションのエンティティ ID を作成する URI パターン。デフォルトは {baseUrl}/saml2/service-provider-metadata/{registrationId} であり、小さなサンプルアプリケーションの場合は次のようになります。

http://localhost:8080/saml2/service-provider-metadata/my-test-configuration

この構成オプションはパターンである必要はなく、固定 URI 値を使用できます。

  • assertionConsumerServiceUrlTemplate - (オプション)SP が開始したフロー中に SP から IDP に AuthNRequest とともに送信されるアサーションコンシューマーサービス URI を示す URI パターン。これはパターンにすることができますが、実際の URI は SP 上の ACS エンドポイントに解決する必要があります。デフォルト値は {baseUrl}/login/saml2/sso/{registrationId} であり、Saml2WebSsoAuthenticationFilter (GitHub) エンドポイントに直接マップする

  • credentials - メッセージの署名、検証、暗号化、復号化に使用される資格情報、秘密鍵、x509 証明書のリスト。このリストには、資格情報を簡単にローテーションできるように冗長な資格情報を含めることができます。たとえば

    • [0] - X509Certificate{VERIFICATION,ENCRYPTION} - 検証と暗号化に使用される IDP の最初の公開鍵。

    • [1]-X509Certificate/{VERIFICATION,ENCRYPTION}- 検証に使用される IDP の 2 番目の検証キー。暗号化は常にリストの最初の ENCRYPTION キーを使用して行われます。

    • [2] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - SP の最初の署名および復号化資格。

    • [3]-PrivateKey/X509Certificate{SIGNING,DECRYPTION}-SP の 2 番目の復号化資格。署名は常にリストの最初の SIGNING キーを使用して行われます。

  • ProviderDetails#entityId - (必須)ID プロバイダーのエンティティ ID。常に固定 URI 値または文字列。パターンは許可されません。

  • ProviderDetails#webSsoUrl - (必須)SP が AuthNRequest メッセージを送信する IDP シングルサインオンエンドポイントの固定 URI 値。

  • ProviderDetails#signAuthNRequest - SP の秘密鍵で AuthNRequest に署名するかどうかを示すブール値。デフォルトは true

  • ProviderDetails#binding - AuthNRequest に使用するバインディングの種類を示す Saml2MessageBinding は、それが REDIRECT か POST かに関係なく、デフォルトは REDIRECT です

受信メッセージを受信すると、署名が常に必要になります。システムは最初にインデックス [0] の証明書を使用して署名の検証を試み、最初の証明書が失敗した場合にのみ 2 番目の証明書に移動します。

同様に、SP で構成された秘密キーが復号化に使用され、同じ順序で試行されます。IDP へのメッセージに署名するときに、最初の SP 資格情報(type=SIGNING)が使用されます。

証明書利用者構成の複製

アプリケーションが複数の ID プロバイダーを使用するユースケースでは、2 つの RelyingPartyRegistration オブジェクト間で一部の構成が重複していることが明らかになります。

  • localEntityIdTemplate

  • 資格情報 (すべての SP 資格情報、IDP 資格情報の変更)

  • assertionConsumerServiceUrlTemplate

構成値の複製にはいくつかの欠点がありますが、バックエンド構成リポジトリはこのデータストレージモデルを複製する必要はありません。

このセットアップには利点があります。一部の ID プロバイダーと他の ID プロバイダーでは、資格情報のローテーションがより簡単になる場合があります。このオブジェクトモデルにより、マルチ IDP のユースケースで構成が変更されたときに混乱が生じず、すべての ID プロバイダーで資格情報をローテーションできなくなります。

サービスプロバイダーのメタデータ

Spring Security SAML 2 実装は、XML 形式で SP メタデータをダウンロードするためのエンドポイントをまだ提供していません。交換される最小限のピース

  • エンティティ ID- デフォルトは {baseUrl}/saml2/service-provider-metadata/{registrationId} この同じ値を使用する他の既知の構成名

    • ビューアーの制限

  • シングルサインオン URL- デフォルトは {baseUrl}/login/saml2/sso/{registrationId} この同じ値を使用する他の既知の構成名

    • 受信者の URL

    • リンク先 URL

    • アサーションコンシューマーサービス URL

  • X509Certificate - {SIGNING、DECRYPTION} 資格情報の一部として設定する証明書は、アイデンティティプロバイダーと共有する必要があります

認証リクエスト - SP 開始 Flow

Web アプリケーションから認証を開始するには、以下にリダイレクトできます。

{baseUrl}/saml2/authenticate/{registrationId}

このエンドポイントは、RelyingPartyRegistration に応じてリダイレクトまたは POST として AuthNRequest を生成します。

AuthNRequest のカスタマイズ

AuthNRequest を調整するには、Saml2AuthenticationRequestFactory のインスタンスを公開できます。

例:リダイレクトによって SAML Assertion を送信するように IDP をリクエストするように AuthNRequest を構成する場合、次のようにします。

@Bean
public Saml2AuthenticationRequestFactory authenticationRequestFactory() {
    OpenSamlAuthenticationRequestFactory authenticationRequestFactory =
        new OpenSamlAuthenticationRequestFactory();
    authenticationRequestFactory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect");
    return authenticationRequestFactory;
}
AuthenticationRequestFactory への委譲

または、パラメーターとして AuthenticationRequestFactory に送信されるものをさらに制御する必要がある状況では、委譲を使用できます。

@Component
public class IssuerSaml2AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory {
    private OpenSamlAuthenticationRequestFactory delegate = new OpenSamlAuthenticationRequestFactory();

    @Override
    public String createAuthenticationRequest(Saml2AuthenticationRequest request) {
        return this.delegate.createAuthenticationRequest(request);
    }

    @Override
    public Saml2PostAuthenticationRequest createPostAuthenticationRequest
        (Saml2AuthenticationRequestContext context) {

        String issuer = // ... calculate issuer

        Saml2AuthenticationRequestContext customIssuer = Saml2AuthenticationRequestContext.builder()
                .assertionConsumerServiceUrl(context.getAssertionConsumerServiceUrl())
                .issuer(issuer)
                .relayState(context.getRelayState())
                .relyingPartyRegistration(context.getRelyingPartyRegistration())
                .build();

        return this.delegate.createPostAuthenticationRequest(customIssuer);
    }

    @Override
    public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest
        (Saml2AuthenticationRequestContext context) {

        throw new UnsupportedOperationException("unsupported");
    }
}

13.1.5. 認証ロジックのカスタマイズ

デフォルトでは、Spring Security は OpenSamlAuthenticationProvider を構成して、受信した SAML 2 レスポンスとアサーションを検証および解析します。このプロバイダーには 3 つの構成オプションがあります

  1. Authority Extractor- アサーションからグループ情報を抽出する

  2. 権限マッパー - 抽出されたグループ情報を内部権限にマップする

  3. レスポンス時間の検証期間 - 時刻同期の課題がある可能性がある場合は、タイムスタンプ検証の組み込みの許容値を使用する必要があります。

カスタマイズ戦略の 1 つは、ObjectPostProcessor を使用することです。これにより、実装によって作成されたオブジェクトを変更できます。別のオプションは、SAMLResponse をインターセプトするフィルターの認証マネージャーをオーバーライドすることです。

OpenSamlAuthenticationProvider ObjectPostProcessor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ObjectPostProcessor<OpenSamlAuthenticationProvider> processor = new ObjectPostProcessor<>() {
            @Override
            public <O extends OpenSamlAuthenticationProvider> O postProcess(O provider) {
                provider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
                provider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
                provider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
                return provider;
            }
        };

        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
               .addObjectPostProcessor(processor)
            )
        ;
    }
}
OpenSamlAuthenticationProvider を認証マネージャーとして構成する

同じメソッド authenticationManager を活用して、デフォルトの OpenSamlAuthenticationProvider をオーバーライドおよびカスタマイズできます。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSamlAuthenticationProvider authProvider = new OpenSamlAuthenticationProvider();
        authProvider.setResponseTimeValidationSkew(RESPONSE_TIME_VALIDATION_SKEW);
        authProvider.setAuthoritiesMapper(AUTHORITIES_MAPPER);
        authProvider.setAuthoritiesExtractor(AUTHORITIES_EXTRACTOR);
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(asList(authProvider)))
            )
        ;
    }
}
カスタム認証マネージャー

独自のカスタム AuthenticationManager 実装を使用して、セキュリティフィルターの認証マネージャーを上書きすることもできます。この認証マネージャーは、SAML 2 レスポンス XML データを含む Saml2AuthenticationToken オブジェクトを予期する必要があります。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
    }
}

13.1.6. Spring Boot 2.x サンプル

現在、Spring Security SAML ログインの自動構成 (GitHub) で Spring Boot チームと協力しています。それまでの間、Yaml 設定をサポートする Spring Boot サンプルを提供しました。

サンプルを実行するには、次の 3 つの手順を実行する

  1. Spring Boot アプリケーションを起動する

    • ./gradlew :spring-security-samples-boot-saml2login:bootRun

  2. ブラウザを開く

  3. これにより、ID プロバイダーに移動し、次を使用してログインします。

    • ユーザー : user

    • パスワード : password

複数のアイデンティティプロバイダーのサンプル

複数のプロバイダーを使用するのは非常に簡単ですが、注意を払わないと失敗する可能性のあるデフォルトがいくつかあります。RelyingPartyRegistration オブジェクトの SAML 構成では、デフォルトで SP エンティティ ID を {baseUrl}/saml2/service-provider-metadata/{registrationId} にしています

つまり、2 つのプロバイダー構成では、システムは次のようになります

registration-1 (Identity Provider 1) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-1

registration-2 (Identity Provider 2) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-2

以下のサンプルに示すこの構成では、外部に対して、実際に同じアプリケーション内でホストされる 2 つの仮想サービスプロバイダー ID を作成しました。

spring:
  security:
    saml2:
      login:
        relying-parties:
          - entity-id: &idp-entity-id https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
            registration-id: simplesamlphp
            web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
            signing-credentials: &service-provider-credentials
              - private-key: |
                  -----BEGIN PRIVATE KEY-----
                  MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE
                  ...................SHORTENED FOR READ ABILITY...................
                  INrtuLp4YHbgk1mi
                  -----END PRIVATE KEY-----
                certificate: |
                  -----BEGIN CERTIFICATE-----
                  MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC
                  ...................SHORTENED FOR READ ABILITY...................
                  RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B
                  -----END CERTIFICATE-----
            verification-credentials: &idp-certificates
              - |
                -----BEGIN CERTIFICATE-----
                MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD
                ...................SHORTENED FOR READ ABILITY...................
                lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk
                -----END CERTIFICATE-----
          - entity-id: *idp-entity-id
            registration-id: simplesamlphp2
            web-sso-url: *idp-sso-url
            signing-credentials: *service-provider-credentials
            verification-credentials: *idp-certificates

これが望ましくない場合は、次を使用してローカル SP エンティティ ID を手動で上書きできます。

localEntityIdTemplate = {baseUrl}/saml2/service-provider-metadata

ローカル SP エンティティ ID をこの値に変更する場合、登録 ID に基づいて登録済み ID プロバイダーごとに正しいシングルサインオン URL(アサーションコンシューマーサービス URL)を提供することが重要です。{baseUrl}/login/saml2/sso/{registrationId}

14. エクスプロイトに対する保護

14.1. サーブレット環境のクロスサイトリクエストフォージェリ(CSRF)

このセクションでは、サーブレット環境に対する Spring Security のクロスサイトリクエストフォージェリ (CSRF) サポートについて説明します。

14.1.1. Spring Security CSRF 保護の使用

Spring Security の CSRF 保護を使用する手順の概要は次のとおりです。

適切な HTTP 動詞を使用する

CSRF 攻撃から保護するための最初のステップは、Web サイトが適切な HTTP 動詞を使用するようにすることです。これについては、安全なメソッドはべき等でなければなりませんで詳しく説明しています。

CSRF 保護を構成する

次のステップは、Spring Security の CSRF 保護をアプリケーション内で構成することです。Spring Security の CSRF 保護はデフォルトで有効になっていますが、構成をカスタマイズする必要がある場合があります。以下は、いくつかの一般的なカスタマイズです。

カスタム CsrfTokenRepository

デフォルトでは、Spring Security は、HttpSessionCsrfTokenRepository を使用して、期待される CSRF トークンを HttpSession に保存します。ユーザーがカスタム CsrfTokenRepository を構成したい場合があります。例: JavaScript ベースのアプリケーションをサポートするために、CsrfToken  を Cookie に保持することが望ましい場合があります。

デフォルトでは、CookieCsrfTokenRepository は XSRF-TOKEN という名前の Cookie に書き込み、X-XSRF-TOKEN という名前のヘッダーまたは HTTP パラメーター _csrf からそれを読み取ります。これらのデフォルトは AngularJS (英語) からのものです

以下を使用して、CookieCsrfTokenRepository を XML で構成できます。

例 117: XML 設定で Cookie に CSRF トークンを保存する
<http>
    <!-- ... -->
    <csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
    class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
    p:cookieHttpOnly="false"/>

サンプルは cookieHttpOnly=false を明示的に設定します。これは、JavaScript(つまり、AngularJS)が読み取れるようにするために必要です。JavaScript で Cookie を直接読み取る機能が必要ない場合は、cookieHttpOnly=false を省略してセキュリティを向上させることをお勧めします。

以下を使用して、Java 構成で CookieCsrfTokenRepository を構成できます。

例 118: Java 構成で Cookie に CSRF トークンを保存する
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
    }
}

サンプルは cookieHttpOnly=false を明示的に設定します。これは、JavaScript(つまり、AngularJS)が読み取れるようにするために必要です。JavaScript で Cookie を直接読み取る機能が必要ない場合は、cookieHttpOnly=false を省略して(代わりに new CookieCsrfTokenRepository() を使用して)セキュリティを向上させることをお勧めします。

CSRF 保護を無効にする

CSRF 保護はデフォルトで有効になっています。ただし、アプリケーションにとって意味がある場合、CSRF 保護を無効にするのは簡単です。

以下の XML 構成は、CSRF 保護を無効にします。

例 119: CSRF XML 設定を無効にする
<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

以下の Java 構成は、CSRF 保護を無効にします。

例 120: CSRF Java 構成を無効にする
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .csrf(csrf -> csrf.disable());
    }
}
CSRF トークンを含める

シンクロナイザートークンパターンを CSRF 攻撃から保護するには、実際の CSRF トークンを HTTP リクエストに含める必要があります。これは、ブラウザによって HTTP リクエストに自動的に含まれないリクエストの一部(フォームパラメーター、HTTP ヘッダーなど)に含まれる必要があります。

Spring Security の CsrfFilter(Javadoc) は、CsrfToken(Javadoc) を _csrf という名前の HttpServletRequest 属性として公開します。これは、任意のビューテクノロジが CsrfToken にアクセスして、期待されるトークンをフォームまたはメタタグとして公開できることを意味します。幸いなことに、トークンをフォームおよび ajax リクエストにさらに簡単に含めることができる統合が以下にリストされています。

エンコードされたフォーム URL

HTML フォームを送信するには、CSRF トークンを非表示の入力としてフォームに含める必要があります。例:レンダリングされた HTML は次のようになります。

例 121: CSRF トークン HTML
<input type="hidden"
    name="_csrf"
    value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

次に、CSRF トークンを非表示入力としてフォームに含めるさまざまな方法について説明します。

自動 CSRF トークンの包含

Spring Security の CSRF サポートは、CsrfRequestDataValueProcessor(Javadoc) を介して Spring の RequestDataValueProcessor(Javadoc) との統合を提供します。これは、Spring のフォームタグライブラリThymeleaf (英語) 、または RequestDataValueProcessor と統合する他のビューテクノロジーを活用する場合、安全でない HTTP メソッド(つまり、投稿)を持つフォームには実際の CSRF トークンが自動的に含まれることを意味します。

csrfInput タグ

JSP を使用している場合は、Spring のフォームタグライブラリを使用できます。ただし、それがオプションでない場合は、csrfInput タグでトークンを簡単に含めることもできます。

CsrfToken リクエスト属性

リクエストに実際の CSRF トークンを含めるための他のオプションが機能しない場合、CsrfToken が _csrf という名前の HttpServletRequest 属性として公開されているという事実を利用できます。

JSP でこれを実行する例を以下に示します。

例 122: リクエスト属性を持つフォームの CSRF トークン
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
    method="post">
<input type="submit"
    value="Log out" />
<input type="hidden"
    name="${_csrf.parameterName}"
    value="${_csrf.token}"/>
</form>
Ajax および JSON リクエスト

JSON を使用している場合、HTTP パラメーター内で CSRF トークンを送信することはできません。代わりに、HTTP ヘッダー内でトークンを送信できます。

次のセクションでは、JavaScript ベースのアプリケーションで CSRF トークンを HTTP リクエストヘッダーとして含めるさまざまな方法について説明します。

自動包含

Spring Security は、予想される CSRF トークンを Cookie に保存するように簡単に構成できます。期待される CSRF を Cookie に保存することにより、AngularJS (英語) などの JavaScript フレームワークは、実際の CSRF トークンを HTTP リクエストヘッダーに自動的に含めます。

メタタグ

Cookie で CSRF を公開する別のパターンは、meta タグ内に CSRF トークンを含めることです。HTML は次のようになります。

例 123: CSRF メタタグ HTML
<html>
<head>
    <meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
    <meta name="_csrf_header" content="X-CSRF-TOKEN"/>
    <!-- ... -->
</head>
<!-- ... -->

メタタグに CSRF トークンが含まれると、JavaScript コードはメタタグを読み取り、CSRF トークンをヘッダーとして含めます。jQuery を使用している場合、これは次の方法で実行できます。

例 124: AJAX 送信 CSRF トークン
$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});
csrfMeta タグ

JSP を使用している場合、CSRP トークンを meta タグに書き込む簡単な方法は、csrfMeta タグを利用することです。

CsrfToken リクエスト属性

リクエストに実際の CSRF トークンを含めるための他のオプションが機能しない場合、CsrfToken が _csrf という名前の HttpServletRequest 属性として公開されているという事実を利用できます。JSP でこれを実行する例を以下に示します。

例 125: CSRF メタタグ JSP
<html>
<head>
    <meta name="_csrf" content="${_csrf.token}"/>
    <!-- default header name is X-CSRF-TOKEN -->
    <meta name="_csrf_header" content="${_csrf.headerName}"/>
    <!-- ... -->
</head>
<!-- ... -->

14.1.2. CSRF の考慮事項

CSRF 攻撃に対する保護を実装する際に考慮すべき特別な考慮事項がいくつかあります。このセクションでは、サーブレット環境に関連する考慮事項について説明します。より一般的な議論については、CSRF の考慮事項を参照してください。

ログイン

ログインリクエストの偽造を防ぐために、ログインリクエストに CSRF をリクエストすることが重要です。Spring Security のサーブレットサポートは、これをすぐに実行できます。

ログアウト

ログアウト試行の偽造を防ぐために、ログアウトリクエストに CSRF をリクエストすることが重要です。CSRF 保護が有効になっている場合(デフォルト)、Spring Security の LogoutFilter は HTTP POST のみを処理します。これにより、ログアウトに CSRF トークンが必要になり、悪意のあるユーザーがユーザーを強制的にログアウトできなくなります。

最も簡単な方法は、フォームを使用してログアウトすることです。本当にリンクが必要な場合は、JavaScript を使用して、リンクに POST を実行させることができます(つまり、非表示のフォームで)。JavaScript が無効になっているブラウザの場合、オプションで、POST を実行するログアウト確認ページにユーザーをリンクさせることができます。

本当にログアウトで HTTP GET を使用したい場合は使用できますが、これは一般的に推奨されないことを忘れないでください。例:次の Java 構成は、任意の HTTP メソッドで /logout がリクエストされた URL でログアウトを実行します。

例 126: HTTP GET でログアウトする
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            .logout(logout -> logout
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            );
    }
}
CSRF およびセッションタイムアウト

デフォルトでは、Spring Security は CSRF トークンを HttpSession に保存します。これにより、セッションの有効期限が切れる状況が発生する可能性があります。つまり、検証対象の CSRF トークンが存在しないことがあります。

セッションタイムアウトの一般的なソリューションについては既に説明しました。このセクションでは、サーブレットサポートに関連する CSRF タイムアウトの詳細について説明します。

予想される CSRF トークンのストレージを Cookie に変更するのは簡単です。詳細については、カスタム CsrfTokenRepository セクションを参照してください。

トークンの有効期限が切れた場合、カスタム AccessDeniedHandler を指定して、トークンの処理方法をカスタマイズできます。カスタム AccessDeniedHandler は、InvalidCsrfTokenException を任意の方法で処理できます。AccessDeniedHandler をカスタマイズする方法の例については、xmlJava 構成 (GitHub) の両方について提供されているリンクを参照してください。

マルチパート (ファイルアップロード)

CSRF 攻撃からマルチパートリクエスト(ファイルのアップロード)を保護すると、鶏と卵 (英語) の問題がどのように発生するかについては既に説明しました。このセクションでは、サーブレットアプリケーション内で CSRF トークンを本文URL に配置する方法を説明します。

Spring でマルチパートフォームを使用する方法の詳細については、Spring リファレンスの 1.1.11. マルチパートリゾルバーセクションおよび MultipartFilter javadoc を参照してください。

CSRF トークンを本文に配置する

CSRF トークンを本文に配置することのトレードオフについては既に説明しました。このセクションでは、本体から CSRF を読み取るように Spring Security を構成する方法について説明します。

本体から CSRF トークンを読み取るために、MultipartFilter は Spring Security フィルターの前に指定されます。Spring Security フィルターの前に MultipartFilter を指定すると、MultipartFilter を呼び出す権限がないため、だれでもサーバーに一時ファイルを配置できます。ただし、認可されたユーザーのみが、アプリケーションで処理されるファイルを送信できます。一般的に、一時ファイルのアップロードはほとんどのサーバーにほとんど影響を与えないはずなので、これが推奨されるアプローチです。

java 構成の Spring Security フィルターの前に MultipartFilter が指定されるようにするために、ユーザーは、次に示すように beforeSpringSecurityFilterChain をオーバーライドできます。

例 127: イニシャライザー MultipartFilter
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
        insertFilters(servletContext, new MultipartFilter());
    }
}

XML 構成の Spring Security フィルターの前に MultipartFilter を指定するために、ユーザーは、以下に示すように、web.xml 内の springSecurityFilterChain の前に MultipartFilter の <filter-mapping> エレメントを配置することができます。

例 128: web.xml - MultipartFilter
<filter>
    <filter-name>MultipartFilter</filter-name>
    <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>MultipartFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
URL に CSRF トークンを含める

認可されていないユーザーによる一時ファイルのアップロードを認可しない場合は、Spring Security フィルターの後に MultipartFilter を配置し、フォームのアクション属性にクエリパラメーターとして CSRF を含めることもできます。CsrfToken は HttpServletRequest  リクエスト属性として公開されているため、それを使用して CSRF トークンを含む action を作成できます。jsp を使用した例を以下に示します

例 129: 実行中の CSRF トークン
<form method="post"
    action="./upload?${_csrf.parameterName}=${_csrf.token}"
    enctype="multipart/form-data">
HiddenHttpMethodFilter

CSRF トークンを本文に配置することのトレードオフについてはすでに説明しました。

Spring のサーブレットサポートでは、HiddenHttpMethodFilter(Javadoc) を使用して HTTP メソッドをオーバーライドします。詳細については、リファレンスドキュメントの HTTP メソッド変換セクションを参照してください。

14.2. セキュリティ HTTP レスポンスヘッダー

セキュリティ HTTP レスポンスヘッダーを使用して、Web アプリケーションのセキュリティを強化できます。このセクションは、セキュリティ HTTP レスポンスヘッダーのサーブレットベースのサポートに特化しています。

14.2.1. デフォルトのセキュリティヘッダー

Spring Security は、セキュリティ HTTP レスポンスヘッダーのデフォルトセットを提供して、安全なデフォルトを提供します。これらのヘッダーはそれぞれベストプラクティスと見なされますが、すべてのクライアントがヘッダーを使用するわけではないため、追加のテストが推奨されます。

特定のヘッダーをカスタマイズできます。例:X-Frame-Options に SAMEORIGIN を指定する場合を除き、デフォルトが必要であると想定します。

これは、次の Java 構成を使用して簡単に実行できます。

例 130: Java 構成でデフォルトのセキュリティヘッダーをカスタマイズする
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                    .sameOrigin()
                )
            );
    }
}

または、Spring Security XML 構成を使用している場合は、次を使用できます。

例 131: XML 設定でデフォルトのセキュリティヘッダーをカスタマイズする
<http>
    <!-- ... -->

    <headers>
        <frame-options policy="SAMEORIGIN" />
    </headers>
</http>

デフォルトの追加を望まず、使用するものを明示的に制御したい場合は、デフォルトを無効にできます。Java および XML ベースの構成の例を以下に示します。

Spring Security の Java 構成を使用している場合、以下はキャッシュ制御のみを追加します。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                // do not use any default headers unless explicitly listed
                .defaultsDisabled()
                .cacheControl(withDefaults())
            );
    }
}

次の XML はキャッシュ制御のみを追加します。

<http>
    <!-- ... -->

    <headers defaults-disabled="true">
        <cache-control/>
    </headers>
</http>

必要に応じて、次の Java 構成ですべての HTTP セキュリティレスポンスヘッダーを無効にできます。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers.disable());
    }
}

必要に応じて、以下の XML 構成ですべての HTTP セキュリティレスポンスヘッダーを無効にできます。

<http>
    <!-- ... -->

    <headers disabled="true" />
</http>

14.2.2. キャッシュ制御

Spring Security には、デフォルトでキャッシュ制御ヘッダーが含まれています。

ただし、実際に特定のレスポンスをキャッシュする場合、アプリケーションは HttpServletResponse.setHeader(String,String) (英語) を選択的に呼び出して、Spring Security によって設定されたヘッダーをオーバーライドできます。これは、CSS、JavaScript、イメージなどを適切にキャッシュできます。

Spring Web MVC を使用する場合、これは通常、構成内で行われます。これを行う方法の詳細については、Spring リファレンスドキュメントの静的リソースの部分を参照してください。

必要に応じて、Spring Security のキャッシュ制御 HTTP レスポンスヘッダーを無効にすることもできます。

例 132: Java 構成で無効化されたキャッシュ制御
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .cacheControl(cache -> cache.disable())
            );
    }
}

同様に、<cache-control> 要素を使用して無効にすることができます。

例 133: XML で無効化されたキャッシュ制御
<http>
    <!-- ... -->

    <headers>
        <cache-control disabled="true"/>
    </headers>
</http>

14.2.3. コンテンツタイプオプション

Spring Security には、デフォルトでコンテンツタイプヘッダーが含まれています。ただし、次の方法で Java 構成で無効にできます。

例 134: Java 構成で無効にされたコンテンツタイプオプション
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
            );
    }
}

同様に、<content-type-options> 要素を使用して無効にすることができます。

例 135: XML で無効にされたコンテンツタイプオプション
<http>
    <!-- ... -->

    <headers>
        <content-type-options disabled="true"/>
    </headers>
</http>

14.2.4. HTTP 厳格なトランスポートセキュリティ (HSTS)

Spring Security は、デフォルトで厳格な輸送セキュリティヘッダーを提供します。ただし、結果を明示的にカスタマイズできます。例:以下は、HSTS に Java 構成を明示的に提供する例です。

例 136: Java 構成を使用した厳密なトランスポートセキュリティ
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .preload(true)
                    .maxAgeInSeconds(31536000)
                )
            );
    }
}

同様に、次のように <hsts> 要素を使用して、HSTS に XML 構成を明示的に提供できます。

例 137: XML 構成を使用した厳密なトランスポートセキュリティ
<http>
    <!-- ... -->

    <headers>
        <hsts
            include-subdomains="true"
            max-age-seconds="31536000"
            preload="true" />
    </headers>
</http>

14.2.5. HTTP 公開キーの固定 (HPKP)

受動性の理由から、Spring Security は HTTP 公開キーの固定のサーブレットサポートを提供しますが、推奨されません

Java 構成で HPKP ヘッダーを有効にできます。

例 138: Java 構成を使用した HTTP 公開キーの固定
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .httpPublicKeyPinning(hpkp -> hpkp
                    .includeSubDomains(true)
                    .reportUri("https://example.net/pkp-report")
                    .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=")
                )
            );
    }
}

同様に、次のように <hpkp> 要素を使用して HPKP ヘッダーを有効にできます。

例 139: XML 構成を使用した HTTP 公開キーの固定
<http>
    <!-- ... -->

    <headers>
        <hpkp
            include-subdomains="true"
            report-uri="https://example.net/pkp-report">
            <pins>
                <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin>
                <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin>
            </pins>
        </hpkp>
    </headers>
</http>

14.2.6. X-Frame-Options

デフォルトでは、Spring Security は X-Frame-Options を使用して iframe 内のレンダリングを無効にします。

以下を使用して、Java オプション内で同じオリジンを使用するようにフレームオプションをカスタマイズできます。

例 140: X-Frame-Options: Java 構成の SAMEORIGIN
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions
                    .sameOrigin()
                )
            );
    }
}

または、XML 構成内で frame-options 要素を使用できます。

例 141: X-Frame-Options: XML 構成を使用した SAMEORIGIN
<http>
    <!-- ... -->

    <headers>
        <frame-options
        policy="SAMEORIGIN" />
    </headers>
</http>

14.2.7. X-XSS-Protection

デフォルトでは、Spring Security は、<< headers-xss-protection、X-XSS-Protection header> を使用して、リフレクションされた XSS 攻撃をブロックするようブラウザーに指示します。ただし、このデフォルトを変更できます。例:次の Java 構成では、Spring Security がブラウザーにコンテンツをブロックするように指示しないように指定しています。

例 142: Java 構成を使用した X-XSS-Protection のカスタマイズ
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .xssProtection(xss -> xss
                    .block(false)
                )
            );
    }
}

同様に、次の XML 構成では、Spring Security がブラウザにコンテンツをブロックするように指示しないように指定しています。

例 143: XML 構成を使用した X-XSS-Protection のカスタマイズ
<http>
    <!-- ... -->

    <headers>
        <xss-protection block="false"/>
    </headers>
</http>

14.2.8. コンテンツセキュリティポリシー (CSP)

Spring Security はデフォルトではコンテンツセキュリティポリシーを追加しません。これは、アプリケーションのコンテキストなしでは妥当なデフォルトを知ることが不可能です。Web アプリケーションの作成者は、保護されたリソースを強制または監視するためのセキュリティポリシーを宣言する必要があります。

例:次のセキュリティポリシーが与えられた場合:

例 144: コンテンツセキュリティポリシーの例
Content-Security-Policy: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

以下に示すように、Java 構成を使用して CSP ヘッダーを有効にできます。

例 145: コンテンツセキュリティポリシーの Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
                )
            );
    }
}

以下に示すように、<content-security-policy> 要素を使用して XML 構成を使用しても同じことができます。

例 146: コンテンツセキュリティポリシーの Java 構成
<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" />
    </headers>
</http>

CSP report-only ヘッダーを有効にするには、次の Java 構成を提供します。

例 147: コンテンツセキュリティポリシーレポート Java 構成のみ
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
                    .reportOnly()
                )
            );
    }
}

同じことは、次を使用した XML 構成でも実現できます。

例 148: コンテンツセキュリティポリシーの XML 設定
<http>
    <!-- ... -->

    <headers>
        <content-security-policy
            policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"
            report-only="true" />
    </headers>
</http>

14.2.9. リファラーポリシー

Spring Security は、デフォルトではリファラーポリシーヘッダーを追加しません。以下に示すように、Java 構成を使用してリファラーポリシーヘッダーを有効にできます。

例 149: リファラーポリシー Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .headers(headers -> headers
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicy.SAME_ORIGIN)
                )
            );
    }
}

以下に示すように、XML 構成と <referrer-policy> 要素を使用して Referrer-Policy ヘッダーを有効にできます。

例 150: リファラーポリシー XML 構成
<http>
    <!-- ... -->

    <headers>
        <referrer-policy policy="same-origin" />
    </headers>
</http>

14.2.10. 機能ポリシー

Spring Security は、デフォルトでは機能ポリシーヘッダーを追加しません。次の Feature-Policy ヘッダー:

例 151: 機能ポリシーの例
Feature-Policy: geolocation 'self'

以下に示すように、Java 構成を使用して機能ポリシーヘッダーを有効にできます。

例 152: 機能ポリシー Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .featurePolicy("geolocation 'self'")
            );
    }
}

または、以下に示すように、<feature-policy> 要素を含む XML 構成を使用して、Feature-Policy ヘッダーを有効にすることができます。

例 153: 機能ポリシー XML 設定
<http>
    <!-- ... -->

    <headers>
        <feature-policy policy-directives="geolocation 'self'" />
    </headers>
</http>

14.2.11. サイトデータを消去する

Spring Security は、デフォルトではクリアサイトデータヘッダーを追加しません。次の Clear-Site-Data ヘッダー:

例 154: クリアサイトデータの例
Clear-Site-Data: "cache", "cookies"

ログアウト時に次の構成で送信できます。

例 155: Clear-Site-Data Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .logout()
                .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(CACHE, COOKIES)));
    }
}

14.2.12. カスタムヘッダー

Spring Security には、より一般的なセキュリティヘッダーをアプリケーションに追加するのに便利なメカニズムがあります。ただし、カスタムヘッダーの追加を可能にするフックも提供します。

静的ヘッダー

すぐにサポートされないカスタムセキュリティヘッダーをアプリケーションに挿入したい場合があります。例:次のカスタムセキュリティヘッダーを指定します。

X-Custom-Security-Header: header-value

以下に示すように、Java 構成を使用してヘッダーをレスポンスに追加できます。

例 156: StaticHeadersWriter Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"))
            );
    }
}

XML 名前空間を使用する場合、以下に示すように、これらのヘッダーを <header> 要素を使用してレスポンスに追加できます。

例 157: StaticHeadersWriter XML 設定
<http>
    <!-- ... -->

    <headers>
        <header name="X-Custom-Security-Header" value="header-value"/>
    </headers>
</http>
ヘッダーライター

名前空間または Java 構成が必要なヘッダーをサポートしていない場合、カスタム HeadersWriter インスタンスを作成するか、HeadersWriter のカスタム実装を提供することもできます。

XFrameOptionsHeaderWriter のカスタムインスタンスの使用例を見てみましょう。X-Frame-Options を明示的に構成する場合は、次の Java 構成で実行できます。

例 158: ヘッダーライターの Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // ...
            .headers(headers -> headers
                .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
            );
    }
}

または、XML ベースの構成に ref 属性を使用できます。

例 159: ヘッダーライターの XML 構成
<http>
    <!-- ... -->

    <headers>
        <header ref="frameOptionsWriter"/>
    </headers>
</http>
<!-- Requires the c-namespace.
See https://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace
-->
<beans:bean id="frameOptionsWriter"
    class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"
    c:frameOptionsMode="SAMEORIGIN"/>
DelegatingRequestMatcherHeaderWriter

特定のリクエストに対してのみヘッダーを書きたい場合があります。例:おそらく、ログインページをフレームから保護するだけです。そのためには、DelegatingRequestMatcherHeaderWriter を使用できます。

Java 構成で DelegatingRequestMatcherHeaderWriter を使用する例を以下に示します。

例 160: DelegatingRequestMatcherHeaderWriter Java 構成
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        RequestMatcher matcher = new AntPathRequestMatcher("/login");
        DelegatingRequestMatcherHeaderWriter headerWriter =
            new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());
        http
            // ...
            .headers(headers -> headers
                .frameOptions(frameOptions -> frameOptions.disable())
                .addHeaderWriter(headerWriter)
            );
    }
}

XML ベースの構成でも同じことが実現できます。

例 161: DelegatingRequestMatcherHeaderWriter XML 設定
<http>
    <!-- ... -->

    <headers>
        <frame-options disabled="true"/>
        <header ref="headerWriter"/>
    </headers>
</http>

<beans:bean id="headerWriter"
    class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
    <beans:constructor-arg>
        <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"
            c:pattern="/login"/>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/>
    </beans:constructor-arg>
</beans:bean>

14.3. HTTP

すべての HTTP ベースの通信は、TLS を使用して保護する必要があります

以下に、HTTPS の使用を支援するサーブレット固有の機能に関する詳細を示します。

14.3.1. HTTPS にリダイレクト

クライアントが HTTPS ではなく HTTP を使用してリクエストを行う場合、Spring Security は HTTPS にリダイレクトするように構成できます。

例:次の Java 構成は、HTTP リクエストを HTTPS にリダイレクトします。

例 162: Java 構成で HTTPS にリダイレクトする
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends
        WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) {
        http
            // ...
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()
            );
    }
}

次の XML 構成は、すべての HTTP リクエストを HTTPS にリダイレクトする

例 163: XML 設定で HTTPS にリダイレクトする
<http>
    <intercept-url pattern="/**" access="ROLE_USER" requires-channel="https"/>
...
</http>

14.3.2. 厳格な輸送セキュリティ

Spring Security は厳格な輸送セキュリティをサポートし、デフォルトで有効にします。

14.3.3. プロキシサーバー構成

14.4. HttpFirewall

Spring Security には、リクエストの処理方法を決定するために、定義したパターンが受信リクエストに対してテストされるいくつかの領域があります。これは、FilterChainProxy がリクエストを通過させるフィルターチェーンを決定する場合、および FilterSecurityInterceptor がリクエストに適用するセキュリティ制約を決定する場合に発生します。定義したパターンに対してテストする場合、メカニズムとは何か、どの URL 値が使用されるかを理解することが重要です。

サーブレット仕様では、getter メソッドを介してアクセス可能な HttpServletRequest のいくつかのプロパティを定義します。これらは contextPathservletPathpathInfo と queryString です。Spring Security はアプリケーション内のパスの保護のみに関心があるため、contextPath は無視されます。残念ながら、サーブレット仕様では、特定のリクエスト URI に対して servletPath および pathInfo の値が何を含むかを正確に定義していません。例:URL の各パスセグメントには、RFC 2396 (英語) [5] で定義されているパラメーターを含めることができます。仕様では、これらを servletPath および pathInfo の値に含める必要があるかどうかは明確に記載されておらず、動作はサーブレットコンテナーによって異なります。これらの値からパスパラメーターを除去しないコンテナーにアプリケーションがデプロイされると、攻撃者がリクエストされた URL に追加して、パターンマッチを予期せず成功または失敗させる危険性があります。[6]。受信 URL の他のバリエーションも可能です。例:パストラバーサルシーケンス(/../ など)または複数のスラッシュ(//)が含まれている可能性があり、パターンマッチが失敗する可能性もあります。サーブレットマッピングを実行する前にこれらを正規化するコンテナーもあれば、そうでないコンテナーもあります。このような課題から保護するために、FilterChainProxy は HttpFirewall 戦略を使用してリクエストをチェックおよびラップします。正規化されていないリクエストはデフォルトで自動的に拒否され、パスパラメーターと重複するスラッシュは一致する目的で削除されます。[7]FilterChainProxy を使用してセキュリティフィルターチェーンを管理することが不可欠です。servletPath および pathInfo の値はコンテナーによってデコードされるため、これらの部分は一致する目的で削除されるため、アプリケーションにはセミコロンを含む有効なパスが含まれていないことに注意してください。

前述のように、デフォルトの戦略はマッチングに Ant スタイルのパスを使用することであり、これはほとんどのユーザーにとって最良の選択である可能性が高いです。この戦略は、AntPathRequestMatcher に実装され、Spring の AntPathMatcher を使用して、queryString を無視して、連結された servletPath および pathInfo に対してパターンの大文字と小文字を区別しない一致を実行します。

何らかの理由で、より強力なマッチング戦略が必要な場合は、正規表現を使用できます。戦略の実装は RegexRequestMatcher です。詳細については、このクラスの Javadoc を参照してください。

実際には、サービス層でメソッドセキュリティを使用して、アプリケーションへのアクセスを制御し、Web アプリケーションレベルで定義されたセキュリティ制約の使用に完全に依存しないことをお勧めします。URL は変化し、アプリケーションがサポートする可能性のあるすべての URL と、リクエストの操作方法を考慮することは困難です。理解しやすいいくつかの単純な ant パスを使用するように制限する必要があります。キャッチオールワイルドカード(/ または)が最後に定義され、アクセスを拒否する場合は、常に「デフォルトで拒否」アプローチを使用してください。

サービス層で定義されたセキュリティは、はるかに堅牢であり、迂回するのが難しいため、Spring Security のメソッドセキュリティオプションを常に利用する必要があります。

HttpFirewall は、HTTP レスポンスヘッダーの改行文字を拒否することにより、HTTP レスポンスの分割 (英語) も防止します。

デフォルトでは、StrictHttpFirewall が使用されます。この実装は、悪意があると思われるリクエストを拒否します。ニーズに対して厳しすぎる場合は、拒否するリクエストのタイプをカスタマイズできます。ただし、これによりアプリケーションが攻撃にさらされる可能性があることを知っておくことが重要です。例:Spring MVC のマトリックス変数を活用する場合、次の構成を使用できます。

例 164: 行列変数を許可
Java
@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}
XML
<b:bean id="httpFirewall"
    class="org.springframework.security.web.firewall.StrictHttpFirewall"
    p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>
Kotlin
@Bean
fun httpFirewall(): StrictHttpFirewall {
    val firewall = StrictHttpFirewall()
    firewall.setAllowSemicolon(true)
    return firewall
}

StrictHttpFirewall は、クロスサイトトレース (XST) (英語) および HTTP 動詞の改ざん (英語) に対する保護が許可されている有効な HTTP メソッドの許可リストを提供します。デフォルトの有効なメソッドは、「DELETE」、「GET」、「HEAD」、「OPTIONS」、「PATCH」、「POST」、および「PUT」です。アプリケーションで有効なメソッドを変更する必要がある場合は、カスタム StrictHttpFirewall Bean を構成できます。例:以下は、HTTP の「GET」および「POST」メソッドのみを許可します。

例 165: GET&POST のみを許可
Java
@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}
XML
<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>
Kotlin
@Bean
fun httpFirewall(): StrictHttpFirewall {
    val firewall = StrictHttpFirewall()
    firewall.setAllowedHttpMethods(listOf("GET", "POST"))
    return firewall
}

new MockHttpServletRequest() を使用している場合、現在は空の文字列 "" として HTTP メソッドを作成します。これは無効な HTTP メソッドであり、Spring Security によって拒否されます。これを解決するには、new MockHttpServletRequest("GET", "") に置き換えます。これを改善することをリクエストする課題については、SPR_16851 (英語) を参照してください。

HTTP メソッドを許可する必要がある場合(推奨されません)、StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true) を使用できます。これにより、HTTP メソッドの検証が完全に無効になります。

15. 統合

15.1. サーブレット API の統合

このセクションでは、Spring Security がサーブレット API とどのように統合されるかについて説明します。servletapi-xml (GitHub) サンプルアプリケーションは、これらの各メソッドの使用方法を示しています。

15.1.1. Servlet 2.5+ 統合

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser() (英語) は、通常は現在のユーザー名である SecurityContextHolder.getContext().getAuthentication().getName() の結果を返します。これは、アプリケーションで現在のユーザー名を表示する場合に役立ちます。さらに、これが null であるかどうかのチェックを使用して、ユーザーが認証済みか匿名かを示すことができます。ユーザーが認証されているかどうかを知ることは、特定の UI 要素を表示するかどうかを判断できます(つまり、ユーザーが認証された場合にのみログアウトリンクを表示する必要があります)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal() (英語) は SecurityContextHolder.getContext().getAuthentication() の結果を返します。これは、ユーザー名とパスワードベースの認証を使用する場合、通常 UsernamePasswordAuthenticationToken のインスタンスである Authentication であることを意味します。これは、ユーザーに関する追加情報が必要な場合に役立ちます。例:ユーザーの姓名を含むカスタム UserDetails を返すカスタム UserDetailsService を作成した可能性があります。次の方法でこの情報を取得できます。

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();

通常、アプリケーション全体でこれほど多くのロジックを実行するのは悪い習慣です。代わりに、Spring Security とサーブレット API のカップリングを減らすために一元化する必要があります。

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String) (英語) は、SecurityContextHolder.getContext().getAuthentication().getAuthorities() に、isUserInRole(String) に渡されたロールを持つ GrantedAuthority が含まれているかどうかを判別します。通常、このメソッドは自動的に追加されるため、ユーザーはこのメソッドに「ROLE_」プレフィックスを渡さないでください。例:現在のユーザーが権限 "ROLE_ADMIN" を持っているかどうかを確認したい場合は、次を使用できます。

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

これは、特定の UI コンポーネントを表示する必要があるかどうかを判断できます。例:現在のユーザーが管理者である場合にのみ、管理者リンクを表示できます。

15.1.2. Servlet 3+ 統合

次のセクションでは、Spring Security が統合される Servlet 3 メソッドについて説明します。

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse) (英語) メソッドを使用して、ユーザーが認証されていることを確認できます。認証されていない場合、構成された AuthenticationEntryPoint を使用して、ユーザーに認証をリクエストします(つまり、ログインページにリダイレクトします)。

HttpServletRequest.login(String,String)

HttpServletRequest.login(String,String) (英語) メソッドを使用して、現在の AuthenticationManager でユーザーを認証できます。例:以下は、ユーザー名「user」とパスワード「password」で認証を試みます。

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}

Spring Security に失敗した認証試行を処理させたい場合は、ServletException をキャッチする必要はありません。

HttpServletRequest.logout()

HttpServletRequest.logout() (英語) メソッドを使用して、現在のユーザーをログアウトできます。

通常、これは、SecurityContextHolder がクリアされ、HttpSession が無効化され、「自分を記憶」認証がクリーンアップされることを意味します。ただし、構成された LogoutHandler 実装は、Spring Security 構成によって異なります。HttpServletRequest.logout() が呼び出された後も、レスポンスの書き込みを担当していることに注意することが重要です。通常、これにはようこそページへのリダイレクトが含まれます。

AsyncContext.start(Runnable)

資格情報を新しいスレッドに確実に伝達する AsyncContext.start(Runnable) (英語) メソッド。Spring Security の同時実行サポートを使用して、Spring Security は AsyncContext.start(Runnable)をオーバーライドして、Runnable の処理時に現在の SecurityContext が使用されるようにします。例:以下は、現在のユーザーの認証を出力します:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
    public void run() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        try {
            final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
            asyncResponse.setStatus(HttpServletResponse.SC_OK);
            asyncResponse.getWriter().write(String.valueOf(authentication));
            async.complete();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
});
非同期サーブレットのサポート

Java ベースの構成を使用している場合、準備は完了です。XML 構成を使用している場合、いくつかの更新が必要です。最初のステップは、以下に示すように、少なくとも 3.0 スキーマを使用するように web.xml を更新したことを確認することです。

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

次に、springSecurityFilterChain が非同期リクエストを処理するために設定されていることを確認する必要があります。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
    org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

以上です!これで、Spring Security は、SecurityContext が非同期リクエストでも伝搬されるようにします。

それはどのように機能するのでしょうか?本当に興味がない場合は、このセクションの残りの部分をスキップして構いません。それ以外の場合は参照してください。これのほとんどはサーブレット仕様に組み込まれていますが、Spring Security が非同期リクエストを適切に処理できるようにするために微調整が少しあります。Spring Security 3.2, より前は、HttpServletResponse がコミットされるとすぐに、SecurityContextHolder からの SecurityContext が自動的に保存されました。これにより、非同期環境で問題が発生する可能性があります。例:以下を検討してください。

httpServletRequest.startAsync();
new Thread("AsyncThread") {
    @Override
    public void run() {
        try {
            // Do work
            TimeUnit.SECONDS.sleep(1);

            // Write to and commit the httpServletResponse
            httpServletResponse.getOutputStream().flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}.start();

課題は、このスレッドが Spring Security に認識されていないため、SecurityContext がこのスレッドに伝播されないことです。これは、HttpServletResponse をコミットするときに SecurityContext がないことを意味します。Spring Security が HttpServletResponse のコミット時に SecurityContext を自動的に保存すると、ログインしているユーザーが失われます。

バージョン 3.2 以降、Spring Security は、HttpServletRequest.startAsync() が呼び出されるとすぐに HttpServletResponse のコミット時に SecurityContext を自動的に保存しないように十分にスマートになっています。

15.1.3. Servlet 3.1+ 統合

次のセクションでは、Spring Security が統合される Servlet 3.1 メソッドについて説明します。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId()(Javadoc) は、Servlet 3.1 以降でのセッション固定攻撃から保護するためのデフォルトの方法です。

15.2. Spring Data 統合

Spring Security は、Spring Data 統合を提供し、クエリ内で現在のユーザーを参照できるようにします。結果のフィルタリングはスケーリングされないため、ページ化された結果をサポートするためにクエリにユーザーを含める必要があるだけでなく、必要です。

15.2.1. Spring Data および Spring Security の構成

このサポートを使用するには、org.springframework.security:spring-security-data 依存関係を追加し、タイプ SecurityEvaluationContextExtension の Bean を提供します。Java 構成では、これは次のようになります。

@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
    return new SecurityEvaluationContextExtension();
}

XML 構成では、これは次のようになります。

<bean class="org.springframework.security.data.repository.query.SecurityEvaluationContextExtension"/>

15.2.2. @Query 内のセキュリティ表現

これで、Spring Security をクエリ内で使用できます。例:

@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
    @Query("select m from Message m where m.to.id = ?#{ principal?.id }")
    Page<Message> findInbox(Pageable pageable);
}

これにより、Authentication.getPrincipal().getId() が Message の受信者と等しいかどうかが確認されます。この例では、プリンシパルが id プロパティを持つオブジェクトにカスタマイズされていることを前提としていることに注意してください。SecurityEvaluationContextExtension Bean を公開することにより、すべての一般的なセキュリティ表現がクエリ内で利用可能になります。

15.3. 並行性サポート

ほとんどの環境では、セキュリティは Thread ごとに保存されます。これは、新しい Thread で作業が行われると、SecurityContext が失われることを意味します。Spring Security は、ユーザーがこれをはるかに簡単にするためのインフラストラクチャを提供します。Spring Security は、マルチスレッド環境で Spring Security を操作するための低レベルの抽象化を提供します。実際、これが Spring Security が AsyncContext.start(Runnable) および Spring MVC 非同期統合との統合に基づいて構築しているものです。

15.3.1. DelegatingSecurityContextRunnable

Spring Security の同時実行性サポートの中で最も基本的な構成要素の 1 つは DelegatingSecurityContextRunnable です。デリゲート用に指定された SecurityContext で SecurityContextHolder を初期化するために、デリゲート Runnable をラップします。その後、SecurityContextHolder をクリアすることを保証する Runnable デリゲートを呼び出します。DelegatingSecurityContextRunnable は次のようになります。

public void run() {
try {
    SecurityContextHolder.setContext(securityContext);
    delegate.run();
} finally {
    SecurityContextHolder.clearContext();
}
}

非常にシンプルですが、SecurityContext をあるスレッドから別のスレッドにシームレスに転送します。ほとんどの場合、SecurityContextHolder はスレッドごとに動作するため、これは重要です。例:Spring Security の <global-method-security> サポートを使用して、サービスの 1 つを保護した可能性があります。これで、現在の Thread の SecurityContext を、保護されたサービスを呼び出す Thread に簡単に転送できます。これを行う方法の例を以下に示します。

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

上記のコードは次の手順を実行します。

  • 保護されたサービスを呼び出す Runnable を作成します。Spring Security を認識していないことに注意してください

  • 使用したい SecurityContext を SecurityContextHolder から取得し、DelegatingSecurityContextRunnable を初期化する

  • DelegatingSecurityContextRunnable を使用してスレッドを作成する

  • 作成したスレッドを開始する

SecurityContextHolder から SecurityContext で DelegatingSecurityContextRunnable を作成することは非常に一般的であるため、ショートカットコンストラクターがあります。次のコードは上記のコードと同じです。

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

このコードは簡単に使えますが、Spring Security を使っているという知識が必要です。次のセクションでは、Spring Security を使っているという事実を隠すために DelegatingSecurityContextExecutor をどのように利用できるかを見ていきます。

15.3.2. DelegatingSecurityContextExecutor

前のセクションで、DelegatingSecurityContextRunnable を使用するのは簡単であることがわかりましたが、Spring Security を使用するためには Spring Security に注意する必要があるため、理想的ではありませんでした。DelegatingSecurityContextExecutor を使用して、Spring Security を使用しているという知識からコードを保護する方法を見てみましょう。

DelegatingSecurityContextExecutor の設計は、代理 Runnable の代わりに代理 Executor を受け入れることを除いて、DelegatingSecurityContextRunnable の設計と非常に似ています。以下に使用方法の例を示します。

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
    new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor =
    new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

executor.execute(originalRunnable);

コードは次の手順を実行します。

  • DelegatingSecurityContextExecutor に使用する SecurityContext を作成します。この例では、SecurityContext を手動で作成するだけです。ただし、SecurityContext をどこでどのように取得するかは重要ではありません(つまり、必要に応じて SecurityContextHolder から取得できます)。

  • 送信された Runnable の実行を担当する delegateExecutor を作成する

  • 最後に、DelegatingSecurityContextRunnable で execute メソッドに渡される Runnable をラップする DelegatingSecurityContextExecutor を作成します。次に、ラップされた Runnable を delegateExecutor に渡します。この例では、DelegatingSecurityContextExecutor に送信されるすべての Runnable に同じ SecurityContext が使用されます。これは、昇格した特権を持つユーザーが実行する必要があるバックグラウンドタスクを実行している場合に便利です。

  • この時点で、「これにより、Spring Security の知識のコードがどのように保護されますか?」独自のコードで SecurityContext と DelegatingSecurityContextExecutor を作成する代わりに、すでに初期化された DelegatingSecurityContextExecutor のインスタンスを注入できます。

@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor

public void submitRunnable() {
Runnable originalRunnable = new Runnable() {
    public void run() {
    // invoke secured service
    }
};
executor.execute(originalRunnable);
}

これで、コードは SecurityContext が Thread に伝搬されていることを認識せず、次に originalRunnable が実行され、次に SecurityContextHolder がクリアされます。この例では、同じユーザーが各スレッドの実行に使用されています。originalRunnable を処理するために executor.execute(Runnable) (つまり、現在ログインしているユーザー)を呼び出したときに SecurityContextHolder のユーザーを使用したい場合はどうなるでしょうか?これを行うには、DelegatingSecurityContextExecutor コンストラクターから SecurityContext 引数を削除します。例:

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor);

executor.execute(Runnable) が実行されると、SecurityContext が最初に SecurityContextHolder によって取得され、次に SecurityContext を使用して DelegatingSecurityContextRunnable が作成されます。これは、executor.execute(Runnable) コードの呼び出しに使用されたのと同じユーザーで Runnable を実行していることを意味します。

15.3.3. Spring Security 並行性クラス

Java コンカレント API と Spring タスク抽象化の両方との追加統合については、Javadoc を参照してください。前のコードを理解すると、それらは非常に自明です。

  • DelegatingSecurityContextCallable

  • DelegatingSecurityContextExecutor

  • DelegatingSecurityContextExecutorService

  • DelegatingSecurityContextRunnable

  • DelegatingSecurityContextScheduledExecutorService

  • DelegatingSecurityContextSchedulingTaskExecutor

  • DelegatingSecurityContextAsyncTaskExecutor

  • DelegatingSecurityContextTaskExecutor

  • DelegatingSecurityContextTaskScheduler

15.4. Jackson サポート

Spring Security は、Spring Security 関連クラスを永続化するための Jackson サポートを提供します。これにより、分散セッション(つまり、セッションレプリケーション、Spring Session など)で作業する際に Spring Security 関連クラスを直列化するパフォーマンスを改善できます。

使用するには、SecurityJackson2Modules.getModules(ClassLoader) を ObjectMapper に登録します(jackson- (GitHub) databind):

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);

以下の Spring Security モジュールは、Jackson サポートを提供します。

  • spring-security-core (CoreJackson2Module)

  • spring-security-web (WebJackson2ModuleWebServletJackson2ModuleWebServerJackson2Module)

  • spring-security-oauth2-client (OAuth2ClientJackson2Module)

  • spring-security-cas (CasJackson2Module)

15.5. ローカライゼーション

Spring Security は、エンドユーザーに表示される可能性が高い例外メッセージのローカライズをサポートしています。アプリケーションが英語を話すユーザー向けに設計されている場合、デフォルトではすべてのセキュリティメッセージは英語であるため、何もする必要はありません。他のロケールをサポートする必要がある場合、知る必要があるすべてがこのセクションに含まれています。

認証の失敗やアクセスの拒否(認可の失敗)に関連するメッセージを含め、すべての例外メッセージをローカライズできます。開発者またはシステムの開発者に焦点を当てた例外とログメッセージ(不適切な属性、インターフェース契約違反、不適切なコンストラクターの使用、起動時の検証、デバッグレベルのログ記録など)はローカライズされておらず、Spring Security のコード内で英語でハードコードされています。

spring-security-core-xx.jar に同梱されている org.springframework.security パッケージには、messages.properties ファイルと、いくつかの共通言語のローカライズバージョンが含まれています。Spring Security クラスは Spring の MessageSourceAware インターフェースを実装し、アプリケーションコンテキストの起動時にメッセージリゾルバーが依存性注入されることを想定しているため、これは ApplicationContext によって参照される必要があります。通常、アプリケーションコンテキスト内で Bean を登録して、メッセージを参照するだけです。以下に例を示します。

<bean id="messageSource"
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

messages.properties は、標準のリソースバンドルに従って名前が付けられ、Spring Security メッセージでサポートされるデフォルト言語を表します。このデフォルトファイルは英語です。

messages.properties ファイルをカスタマイズする場合、または他の言語をサポートする場合は、ファイルをコピーし、それに応じて名前を変更し、上記の Bean 定義内に登録する必要があります。このファイル内には多数のメッセージキーがないため、ローカライズは主要なイニシアチブと見なされるべきではありません。このファイルのローカライズを実行する場合は、JIRA タスクをログに記録し、適切な名前のローカライズされたバージョンの messages.properties を添付して、コミュニティと作業を共有することを検討してください。

Spring Security は、実際に適切なメッセージを検索するために、Spring のローカライゼーションサポートに依存しています。これが機能するためには、受信リクエストのロケールが Spring の org.springframework.context.i18n.LocaleContextHolder に保存されていることを確認する必要があります。Spring MVC の DispatcherServlet はアプリケーションに対してこれを自動的に行いますが、Spring Security のフィルターはこの前に呼び出されるため、LocaleContextHolder はフィルターが呼び出される前に正しい Locale を含むように設定する必要があります。これを自分でフィルターで行うことができます(web.xml で Spring Security フィルターの前に来る必要があります)か、Spring の RequestContextFilter を使用できます。Spring でのローカライズの使用の詳細については、Spring Framework のドキュメントを参照してください。

「contacts」サンプルアプリケーションは、ローカライズされたメッセージを使用するように設定されています。

15.6. Spring MVC 統合

Spring Security は、Spring MVC とのオプションの統合をいくつか提供します。このセクションでは、統合についてさらに詳しく説明します。

15.6.1. @EnableWebMvcSecurity

Spring Security 4.0, 現在、@EnableWebMvcSecurity は非推奨です。代替は @EnableWebSecurity であり、クラスパスに基づいて Spring MVC 機能の追加を決定します。

Spring Security と Spring MVC の統合を有効にするには、@EnableWebSecurity アノテーションを構成に追加します。

Spring Security は、Spring MVC の WebMvcConfigurer を使用した構成を提供します。つまり、WebMvcConfigurationSupport と直接統合するなど、より高度なオプションを使用している場合は、Spring Security 構成を手動で提供する必要があります。

15.6.2. MvcRequestMatcher

Spring Security は、MvcRequestMatcher を使用した URL で Spring MVC がどのように一致するかを深く統合します。これは、セキュリティルールがリクエストの処理に使用されるロジックと一致することを確認できます。

MvcRequestMatcher を使用するには、Spring Security 構成を DispatcherServlet と同じ ApplicationContext に配置する必要があります。Spring Security の MvcRequestMatcher は、mvcHandlerMappingIntrospector という名前の HandlerMappingIntrospector Bean が、マッチングの実行に使用される Spring MVC 構成によって登録されることを想定しているため、これが必要です。

web.xml の場合、これは設定を DispatcherServlet.xml に配置する必要があることを意味します。

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ -->
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>

<servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <!-- Load from the ContextLoaderListener -->
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
  </init-param>
</servlet>

<servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

DispatcherServlet の ApplicationContext に配置された WebSecurityConfiguration の下。

public class SecurityInitializer extends
    AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] { RootConfiguration.class,
        WebMvcConfiguration.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

HttpServletRequest とメソッドセキュリティを照合することにより、常に認可規則を提供することをお勧めします。

HttpServletRequest での照合による認可ルールの提供は、コードパスの非常に早い段階で行われ、攻撃対象領域 (英語) を減らすのに役立つため、優れています。メソッドセキュリティにより、誰かが Web 認証ルールをバイパスした場合でも、アプリケーションは引き続き保護されます。これは多層防御 (英語) として知られているものです

次のようにマップされているコントローラーを考えます。

@RequestMapping("/admin")
public String admin() {

このコントローラーメソッドへのアクセスを管理ユーザーに制限したい場合、開発者は HttpServletRequest で次と照合することで認可ルールを提供できます。

protected configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(authorize -> authorize
            .antMatchers("/admin").hasRole("ADMIN")
        );
}

または XML

<http>
    <intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

どちらの構成でも、URL /admin では、認証されたユーザーが管理ユーザーである必要があります。ただし、Spring MVC 構成によっては、URL /admin.html も admin() メソッドにマップされます。さらに、Spring MVC 構成に応じて、URL /admin/ も admin() メソッドにマップされます。

問題は、セキュリティルールが /admin のみを保護していることです。Spring MVC のすべての順列に追加のルールを追加できますが、これは非常に冗長で面倒です。

代わりに、Spring Security の MvcRequestMatcher を活用できます。次の構成は、Spring MVC を使用して URL で照合することにより、Spring MVC が照合する URL と同じ URL を保護します。

protected configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests(authorize -> authorize
            .mvcMatchers("/admin").hasRole("ADMIN")
        );
}

または XML

<http request-matcher="mvc">
    <intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

15.6.3. @AuthenticationPrincipal

Spring Security は、Spring MVC 引数の現在の Authentication.getPrincipal() を自動的に解決できる AuthenticationPrincipalArgumentResolver を提供します。@EnableWebSecurity を使用すると、Spring MVC 構成にこれが自動的に追加されます。XML ベースの構成を使用する場合は、これを自分で追加する必要があります。例:

<mvc:annotation-driven>
        <mvc:argument-resolvers>
                <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
</mvc:annotation-driven>

AuthenticationPrincipalArgumentResolver が適切に構成されたら、Spring MVC レイヤーで Spring Security から完全に切り離すことができます。

UserDetails と独自の CustomUserObject を実装する Object を返すカスタム UserDetailsService が存在する状況を考えます。現在認証されているユーザーの CustomUser は、次のコードを使用してアクセスできます。

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
    Authentication authentication =
    SecurityContextHolder.getContext().getAuthentication();
    CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();

    // .. find messages for this user and return them ...
}

Spring Security 3.2 以降、アノテーションを追加することで、引数をより直接解決できます。例:

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

    // .. find messages for this user and return them ...
}

場合によっては、何らかの方法でプリンシパルを変換する必要があります。例: CustomUser をファイナルにする必要がある場合、拡張できませんでした。この状況では、UserDetailsService は UserDetails を実装する Object を返し、CustomUser にアクセスするために getCustomUser という名前のメソッドを提供します。例:次のようになります。

public class CustomUserUserDetails extends User {
        // ...
        public CustomUser getCustomUser() {
                return customUser;
        }
}

次に、Authentication.getPrincipal() をルートオブジェクトとして使用する SpEL 式を使用して、CustomUser にアクセスできます。

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {

    // .. find messages for this user and return them ...
}

SpEL 式で Bean を参照することもできます。例:JPA を使用してユーザーを管理しており、現在のユーザーのプロパティを変更して保存する場合は、以下を使用できます。

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
        @RequestParam String firstName) {

    // change the firstName on an attached instance which will be persisted to the database
    attachedCustomUser.setFirstName(firstName);

    // ...
}

@AuthenticationPrincipal を独自のアノテーションのメタアノテーションにすることで、Spring Security への依存をさらに削除できます。以下に、@CurrentUser という名前のアノテーションでこれを行う方法を示します。

Spring Security への依存を削除するために、@CurrentUser を作成するのは消費アプリケーションであることを認識することが重要です。このステップは厳密には必要ありませんが、Spring Security への依存関係をより中央の場所に分離できます。
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}

@CurrentUser が指定されたため、それを使用して、現在認証されているユーザーの CustomUser を解決するためのシグナルを送ることができます。また、Spring Security への依存関係を単一のファイルに分離しました。

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {

    // .. find messages for this user and return them ...
}

15.6.4. Spring MVC 非同期統合

Spring Web MVC 3.2 + は、非同期リクエスト処理 (英語) に優れたサポートを提供します。追加の構成を行わなくても、Spring Security は自動的に SecurityContext を Thread にセットアップし、コントローラーから返される Callable を実行します。例:次のメソッドでは、Callable が作成されたときに使用可能であった SecurityContext で Callable が自動的に実行されます。

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

return new Callable<String>() {
    public Object call() throws Exception {
    // ...
    return "someView";
    }
};
}
SecurityContext と Callable の関連付け

技術的に言えば、Spring Security は WebAsyncManager と統合されます。Callable の処理に使用される SecurityContext は、startCallableProcessing が呼び出された時点で SecurityContextHolder に存在する SecurityContext です。

コントローラーによって返される DeferredResult との自動統合はありません。これは、DeferredResult がユーザーによって処理され、自動的に統合する方法がないためです。ただし、並行性サポートを使用して、Spring Security との透過的な統合を提供できます。

15.6.5. Spring MVC と CSRF の統合

自動トークンインクルージョン

Spring Security は、Spring MVC フォームタグ (英語) を使用するフォーム内に CSRF トークンを自動的に組み込みます。例:次の JSP:

<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:form="http://www.springframework.org/tags/form" version="2.0">
    <jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <!-- ... -->

    <c:url var="logoutUrl" value="/logout"/>
    <form:form action="${logoutUrl}"
        method="post">
    <input type="submit"
        value="Log out" />
    <input type="hidden"
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>
    </form:form>

    <!-- ... -->
</html>
</jsp:root>

次のような HTML を出力します。

<!-- ... -->

<form action="/context/logout" method="post">
<input type="submit" value="Log out"/>
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/>
</form>

<!-- ... -->
CsrfToken の解決

Spring Security は、Spring MVC 引数の現在の CsrfToken を自動的に解決できる CsrfTokenArgumentResolver を提供します。@EnableWebSecurity を使用すると、これが Spring MVC 構成に自動的に追加されます。XML ベースの構成を使用する場合は、これを自分で追加する必要があります。

CsrfTokenArgumentResolver が適切に構成されたら、CsrfToken を静的な HTML ベースのアプリケーションに公開できます。

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

CsrfToken を他のドメインから秘密にしておくことが重要です。つまり、クロスオリジン共有 (CORS) (英語) を使用している場合、CsrfToken を外部ドメインに公開しないでください。

15.7. WebSocket セキュリティ

Spring Security 4 は、Spring の WebSocket サポートを保護するためのサポートを追加しました。このセクションでは、Spring Security の WebSocket サポートの使用方法について説明します。

https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket (英語) で WebSocket セキュリティの完全な実用サンプルを見つけることができます。
JSR-356 の直接サポート

Spring Security は、JSR-356 の直接的なサポートを提供しません。なぜなら、そうするとほとんど価値がなくなるからです。これは、形式が不明であるため、不明な形式を保護するために Spring が実行できることはほとんどないためです。さらに、JSR-356 はメッセージを傍受する方法を提供しないため、セキュリティはかなり侵襲的です。

15.7.1. WebSocket の設定

Spring Security 4.0 は、Spring メッセージング抽象化による WebSockets の認可サポートを導入しました。Java 構成を使用して認証を設定するには、AbstractSecurityWebSocketMessageBrokerConfigurer を継承して MessageSecurityMetadataSourceRegistry を設定するだけです。例:

@Configuration
public class WebSocketSecurityConfig
      extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/user/**").authenticated() (3)
    }
}

これにより、次のことが保証されます。

1 受信 CONNECT メッセージには、同一起源ポリシーを実施するための有効な CSRF トークンが必要です
2SecurityContextHolder には、受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。
3 メッセージには適切な認可が必要です。具体的には、「/user/」で始まる受信メッセージには ROLE_USER が必要です。認可の詳細については、WebSocket 認証を参照してください。

Spring Security は、WebSockets を保護するための XML 名前空間サポートも提供します。比較可能な XML ベースの構成は次のようになります。

<websocket-message-broker> (1) (2)
    (3)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>

これにより、次のことが保証されます。

1 受信 CONNECT メッセージには、同一起源ポリシーを実施するための有効な CSRF トークンが必要です
2SecurityContextHolder には、受信リクエストの simpUser ヘッダー属性内のユーザーが入力されます。
3 メッセージには適切な認可が必要です。具体的には、「/user/」で始まる受信メッセージには ROLE_USER が必要です。認可の詳細については、WebSocket 認証を参照してください。

15.7.2. WebSocket 認証

WebSockets は、WebSocket 接続が確立されたときに HTTP リクエストで見つかった同じ認証情報を再利用します。これは、HttpServletRequest 上の Principal が WebSockets に引き渡されることを意味します。Spring Security を使用している場合、HttpServletRequest の Principal は自動的にオーバーライドされます。

より具体的には、ユーザーが WebSocket アプリケーションに対して認証されたことを確認するには、HTTP ベースの Web アプリケーションを認証するように Spring Security をセットアップすることだけが必要です。

15.7.3. WebSocket 認証

Spring Security 4.0 は、Spring メッセージング抽象化による WebSockets の認可サポートを導入しました。Java 構成を使用して認証を設定するには、AbstractSecurityWebSocketMessageBrokerConfigurer を継承して MessageSecurityMetadataSourceRegistry を設定するだけです。例:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .nullDestMatcher().authenticated() (1)
                .simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
                .simpDestMatchers("/app/**").hasRole("USER") (3)
                .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
                .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
                .anyMessage().denyAll(); (6)

    }
}

これにより、次のことが保証されます。

1 宛先のないメッセージ(つまり、メッセージタイプが MESSAGE または SUBSCRIBE 以外のもの)では、ユーザーの認証が必要になります
2 誰でも /user/queue/errors にサブスクライブできます
3「/app/」で始まる宛先を持つすべてのメッセージには、ロール ROLE_USER が必要です。
4SUBSCRIBE タイプの「/user/」または「/topic/friends/」で始まるメッセージには、ROLE_USER が必要です
5 タイプ MESSAGE または SUBSCRIBE の他のメッセージは拒否されます。6 のため、この手順は必要ありませんが、特定のメッセージタイプでどのように一致するかを示しています。
6 その他のメッセージは拒否されます。これは、メッセージを見逃さないようにするための良いアイデアです。

Spring Security は、WebSockets を保護するための XML 名前空間サポートも提供します。比較可能な XML ベースの構成は次のようになります。

<websocket-message-broker>
    (1)
    <intercept-message type="CONNECT" access="permitAll" />
    <intercept-message type="UNSUBSCRIBE" access="permitAll" />
    <intercept-message type="DISCONNECT" access="permitAll" />

    <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> (2)
    <intercept-message pattern="/app/**" access="hasRole('USER')" />      (3)

    (4)
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
    <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />

    (5)
    <intercept-message type="MESSAGE" access="denyAll" />
    <intercept-message type="SUBSCRIBE" access="denyAll" />

    <intercept-message pattern="/**" access="denyAll" /> (6)
</websocket-message-broker>

これにより、次のことが保証されます。

<
1 タイプ CONNECT、UNSUBSCRIBE、または DISCONNECT のメッセージでは、ユーザーの認証が必要です。