メソッドのセキュリティ

Spring Security は、リクエストレベルでの認可のモデリングに加えて、メソッドレベルでのモデリングもサポートします。

次のように、任意の @Configuration クラスに @EnableMethodSecurity のアノテーションを付けるか、任意の XML 構成ファイルに <method-security> を追加することで、アプリケーション内でこれをアクティブ化できます。

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity
@EnableMethodSecurity
<sec:method-security/>

その後、Spring 管理のクラスまたはメソッドに @PreAuthorize@PostAuthorize@PreFilter@PostFilter のアノテーションをすぐに付けて、入力パラメーターや戻り値を含むメソッドの呼び出しを承認できるようになります。

Spring Boot スターターセキュリティは、デフォルトではメソッドレベルの認可をアクティブ化しません。

Method Security は、AspectJ のサポートカスタムアノテーション、およびいくつかの構成ポイントを含む、他の多くのユースケースもサポートしています。次の使用例について学習することを検討してください。

メソッドセキュリティの仕組み

Spring Security のメソッド認証サポートは、次の場合に便利です。

  • 詳細な認可ロジックを抽出します。たとえば、メソッドのパラメーターと戻り値が認可の決定にコントリビュートする場合です。

  • サービス層でのセキュリティの強化

  • スタイル的には、HttpSecurity ベースの構成よりもアノテーションベースの構成を好む

また、Method Security は Spring AOP を使用して構築されているため、その表現力をすべて利用して、必要に応じて Spring Security のデフォルトをオーバーライドできます。

すでに記述されていたように、Spring XML 構成ファイル内の @Configuration クラスまたは <sec:method-security/> に @EnableMethodSecurity を追加することから始めます。

このアノテーションと XML 要素は、それぞれ @EnableGlobalMethodSecurity と <sec:global-method-security/> に置き換わります。これらは次のような改善を提供します。

  1. メタデータソース、構成属性、意思決定マネージャー、投票者の代わりに、簡略化された AuthorizationManager API を使用します。これにより、再利用とカスタマイズが簡単になります。

  2. Bean をカスタマイズするために GlobalMethodSecurityConfiguration を継承する必要はなく、直接 Bean ベースの構成を優先します

  3. ネイティブ Spring AOP を使用して構築され、抽象化を削除し、Spring AOP ビルドブロックを使用してカスタマイズできるようにします

  4. 競合するアノテーションをチェックして、明確なセキュリティ構成を確保します

  5. JSR-250 に準拠

  6. デフォルトで @PreAuthorize@PostAuthorize@PreFilter@PostFilter を有効にします

@EnableGlobalMethodSecurity または <global-method-security/> を使用している場合、これらは現在非推奨となっているため、移行することをお勧めします。

メソッド認可は、メソッド認可前とメソッド認可後を組み合わせたものです。次の方法でアノテーションが付けられたサービス Bean について考えてみましょう。

  • Java

  • Kotlin

@Service
public class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    public Customer readCustomer(String id) { ... }
}
@Service
open class MyCustomerService {
    @PreAuthorize("hasAuthority('permission:read')")
    @PostAuthorize("returnObject.owner == authentication.name")
    fun readCustomer(val id: String): Customer { ... }
}

Method Security がアクティブ化されている場合、MyCustomerService#readCustomer への特定の呼び出しは次のようになります。

methodsecurity
  1. Spring AOP は、readCustomer のプロキシメソッドを呼び出します。プロキシの他のアドバイザーのうち、@PreAuthorize ポイントカットと一致する AuthorizationManagerBeforeMethodInterceptor (Javadoc) を呼び出します。

  2. インターセプターは PreAuthorizeAuthorizationManager#check (Javadoc) を呼び出します

  3. 認可マネージャーは MethodSecurityExpressionHandler を使用してアノテーションの SpEL 式を解析し、Supplier<Authentication> と MethodInvocation を含む MethodSecurityExpressionRoot から対応する EvaluationContext を構築します。

  4. インターセプターはこのコンテキストを使用して式を評価します。具体的には、Supplier から Authentication を読み取り、権限のコレクションに permission:read が含まれているかどうかを確認します。

  5. 評価に合格すると、Spring AOP はメソッドの呼び出しを開始します。

  6. そうでない場合、インターセプターは AuthorizationDeniedEvent を発行し、AccessDeniedException (Javadoc) をスローします。ExceptionTranslationFilter はこれをキャッチし、レスポンスに 403 ステータスコードを返します。

  7. メソッドが戻った後、Spring AOP は @PostAuthorize pointcut に一致する AuthorizationManagerAfterMethodInterceptor (Javadoc) を呼び出し、上記と同じように動作しますが、PostAuthorizeAuthorizationManager (Javadoc) を使用します。

  8. 評価が合格した場合 (この場合、戻り値はログインしているユーザーのもの)、処理は通常どおり続行されます。

  9. そうでない場合、インターセプターは AuthorizationDeniedEvent をパブリッシュし、AccessDeniedException (Javadoc) をスローします。これを ExceptionTranslationFilter がキャッチして、レスポンスに 403 ステータスコードを返します。

メソッドが HTTP リクエストのコンテキストで呼び出されない場合は、AccessDeniedException を自分で処理する必要がある可能性があります。

複数のアノテーションが連続して計算される

上で示したように、メソッド呼び出しに複数のメソッドセキュリティアノテーションが含まれる場合、それらのそれぞれは一度に 1 つずつ処理されます。これは、それらがまとめて "anded" されていると考えることができることを意味します。つまり、呼び出しが認可されるには、すべてのアノテーションインスペクションが認可に合格する必要があります。

繰り返しのアノテーションはサポートされていません

ただし、同じメソッドで同じアノテーションを繰り返すことはサポートされていません。例: 同じメソッドに @PreAuthorize を 2 回配置することはできません。

代わりに、SpEL のブール値サポート、または別の Bean への委譲のサポートを使用してください。

各アノテーションには独自のポイントカットがあります

各アノテーションには独自のポイントカットインスタンスがあり、メソッドとそれを囲むクラスから開始して、オブジェクト階層全体にわたってそのアノテーションまたは対応するメタアノテーションを検索します。

各アノテーションには独自のメソッドインターセプタがあります

各アノテーションには、独自の専用メソッドインターセプターがあります。その理由は、物事をより構成しやすくするためです。例: 必要に応じて、Spring Security デフォルトを無効にして、@PostAuthorize メソッドインターセプターのみを公開できます。

メソッドインターセプタは次のとおりです。

一般に、次のリストは、@EnableMethodSecurity を追加したときに Spring Security が公開するインターセプターを表すものと考えることができます。

  • Java

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preAuthorizeMethodInterceptor() {
    return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postAuthorizeMethodInterceptor() {
    return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor preFilterMethodInterceptor() {
    return AuthorizationManagerBeforeMethodInterceptor.preFilter();
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor postFilterMethodInterceptor() {
    return AuthorizationManagerAfterMethodInterceptor.postFilter();
}

複雑な SpEL 式よりも権限の付与を優先する

多くの場合、次のような複雑な SpEL 式を導入したくなることがあります。

  • Java

@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
Kotlin
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")

ただし、代わりに ROLE_ADMIN を持つユーザーに permission:read を付与することもできます。これを行う 1 つの方法は、次のように RoleHierarchy を使用することです。

  • Java

  • Kotlin

  • XML

@Bean
static RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read");
}
companion object {
    @Bean
    fun roleHierarchy(): RoleHierarchy {
        return RoleHierarchyImpl.fromHierarchy("ROLE_ADMIN > permission:read")
    }
}
<bean id="roleHierarchy"
        class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" factory-method="fromHierarchy">
    <constructor-arg value="ROLE_ADMIN > permission:read"/>
</bean>

それを MethodSecurityExpressionHandler インスタンスに設定します。これにより、次のようなより単純な @PreAuthorize 式を作成できるようになります。

  • Java

  • Kotlin

@PreAuthorize("hasAuthority('permission:read')")
@PreAuthorize("hasAuthority('permission:read')")

または、可能であれば、アプリケーション固有の認可ロジックをログイン時に付与される権限に適応させます。

リクエストレベルの認可とメソッドレベルの認可の比較

リクエストレベルの認可よりもメソッドレベルの認可を優先する必要があるのはどのような場合ですか ? その一部は好みに左右されます。ただし、決定する際には、次のそれぞれの長所リストを考慮してください。

request-level

method-level

認可型

coarse-grained

fine-grained

構成のロケーション

構成クラスで宣言される

メソッド宣言に対してローカル

構成スタイル

DSL

アノテーション

認可の定義

プログラム的な

SpEL

主なトレードオフは、認可ルールをどこに適用するかであるようです。

アノテーションベースのメソッドセキュリティを使用する場合、アノテーションのないメソッドは保護されないことに留意することが重要です。これを防ぐには、HttpSecurity インスタンスでキャッチオール認可ルールを宣言します。

アノテーションによる承認

Spring Security がメソッドレベルの認可サポートを有効にする主な方法は、メソッド、クラス、インターフェースに追加できるアノテーションを使用することです。

@PreAuthorize によるメソッド呼び出しの認可

メソッドセキュリティがアクティブですの場合、次のようにメソッドに @PreAuthorize (Javadoc) アノテーションを付けることができます。

  • Java

  • Kotlin

@Component
public class BankService {
	@PreAuthorize("hasRole('ADMIN')")
	public Account readAccount(Long id) {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
	}
}
@Component
open class BankService {
	@PreAuthorize("hasRole('ADMIN')")
	fun readAccount(val id: Long): Account {
        // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority
	}
}

これは、指定された式 hasRole('ADMIN') が合格した場合にのみメソッドを呼び出すことができることを意味します。

次に、次のようにクラスをテストして、認可ルールが適用されていることを確認できます。

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(roles="ADMIN")
@Test
void readAccountWithAdminRoleThenInvokes() {
    Account account = this.bankService.readAccount("12345678");
    // ... assertions
}

@WithMockUser(roles="WRONG")
@Test
void readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
        () -> this.bankService.readAccount("12345678"));
}
@WithMockUser(roles="ADMIN")
@Test
fun readAccountWithAdminRoleThenInvokes() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}

@WithMockUser(roles="WRONG")
@Test
fun readAccountWithWrongRoleThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PreAuthorize はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。

@PreAuthorize は必要な権限を宣言するのに非常に役立ちますが、メソッドパラメーターを含むより複雑な式を評価するためにも使用できます。

@PostAuthorize による認証方法の結果

メソッドセキュリティがアクティブな場合、次のようにメソッドに @PostAuthorize (Javadoc) アノテーションを付けることができます。

  • Java

  • Kotlin

@Component
public class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@PostAuthorize("returnObject.owner == authentication.name")
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

これは、指定された式 returnObject.owner == authentication.name が合格した場合にのみメソッドが値を返すことができることを示します。returnObject は、返される Account オブジェクトを表します。

その後、クラスをテストして、認可ルールが適用されていることを確認できます。

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountWhenOwnedThenReturns() {
    Account account = this.bankService.readAccount("12345678");
    // ... assertions
}

@WithMockUser(username="wrong")
@Test
void readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(
        () -> this.bankService.readAccount("12345678"));
}
@WithMockUser(username="owner")
@Test
fun readAccountWhenOwnedThenReturns() {
    val account: Account = this.bankService.readAccount("12345678")
    // ... assertions
}

@WithMockUser(username="wrong")
@Test
fun readAccountWhenNotOwnedThenAccessDenied() {
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy {
        this.bankService.readAccount("12345678")
    }
}
@PostAuthorize はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。

@PostAuthorize は、安全でない直接オブジェクト参照 [OWASP] (英語) に対して防御する場合に特に役立ちます。実際、次のようにメタアノテーションとして定義できます。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
annotation class RequireOwnership

代わりに、次の方法でサービスにアノテーションを付けることができます。

  • Java

  • Kotlin

@Component
public class BankService {
	@RequireOwnership
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@RequireOwnership
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

その結果、上記のメソッドは、owner 属性がログインユーザーの name と一致する場合にのみ Account を返します。そうでない場合、Spring Security は AccessDeniedException をスローし、403 ステータスコードを返します。

@PreFilter を使用したメソッドパラメーターのフィルタリング

メソッドセキュリティがアクティブな場合、次のようにメソッドに @PreFilter (Javadoc) アノテーションを付けることができます。

  • Java

  • Kotlin

@Component
public class BankService {
	@PreFilter("filterObject.owner == authentication.name")
	public Collection<Account> updateAccounts(Account... accounts) {
        // ... `accounts` will only contain the accounts owned by the logged-in user
        return updated;
	}
}
@Component
open class BankService {
	@PreFilter("filterObject.owner == authentication.name")
	fun updateAccounts(vararg accounts: Account): Collection<Account> {
        // ... `accounts` will only contain the accounts owned by the logged-in user
        return updated
	}
}

これは、式 filterObject.owner == authentication.name が失敗する accounts からの値をフィルターで除外することを目的としています。filterObject は accounts 内の各 account を表し、各 account をテストするために使用されます。

次に、次の方法でクラスをテストして、認可ルールが適用されていることを確認できます。

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void updateAccountsWhenOwnedThenReturns() {
    Account ownedBy = ...
    Account notOwnedBy = ...
    Collection<Account> updated = this.bankService.updateAccounts(ownedBy, notOwnedBy);
    assertThat(updated).containsOnly(ownedBy);
}
@Autowired
lateinit var bankService: BankService

@WithMockUser(username="owner")
@Test
fun updateAccountsWhenOwnedThenReturns() {
    val ownedBy: Account = ...
    val notOwnedBy: Account = ...
    val updated: Collection<Account> = bankService.updateAccounts(ownedBy, notOwnedBy)
    assertThat(updated).containsOnly(ownedBy)
}
@PreFilter はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。

@PreFilter は、配列、コレクション、マップ、ストリーム (ストリームが開いている限り) をサポートします。

例: 上記の updateAccounts 宣言は、次の他の 4 つの宣言と同じように機能します。

  • Java

  • Kotlin

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Account[] accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Collection<Account> accounts)

@PreFilter("filterObject.value.owner == authentication.name")
public Collection<Account> updateAccounts(Map<String, Account> accounts)

@PreFilter("filterObject.owner == authentication.name")
public Collection<Account> updateAccounts(Stream<Account> accounts)
@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Array<Account>): Collection<Account>

@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Collection<Account>): Collection<Account>

@PreFilter("filterObject.value.owner == authentication.name")
fun updateAccounts(accounts: Map<String, Account>): Collection<Account>

@PreFilter("filterObject.owner == authentication.name")
fun updateAccounts(accounts: Stream<Account>): Collection<Account>

その結果、上記のメソッドには、owner 属性がログインユーザーの name と一致する Account インスタンスのみが含まれることになります。

@PostFilter によるフィルタリングメソッドの結果

メソッドセキュリティがアクティブな場合、次のようにメソッドに @PostFilter (Javadoc) アノテーションを付けることができます。

  • Java

  • Kotlin

@Component
public class BankService {
	@PostFilter("filterObject.owner == authentication.name")
	public Collection<Account> readAccounts(String... ids) {
        // ... the return value will be filtered to only contain the accounts owned by the logged-in user
        return accounts;
	}
}
@Component
open class BankService {
	@PreFilter("filterObject.owner == authentication.name")
	fun readAccounts(vararg ids: String): Collection<Account> {
        // ... the return value will be filtered to only contain the accounts owned by the logged-in user
        return accounts
	}
}

これは、式 filterObject.owner == authentication.name が失敗した場合に戻り値から値をフィルターで除外することを目的としています。filterObject は accounts 内の各 account を表し、各 account をテストするために使用されます。

次に、次のようにクラスをテストして、認可ルールが適用されていることを確認できます。

  • Java

  • Kotlin

@Autowired
BankService bankService;

@WithMockUser(username="owner")
@Test
void readAccountsWhenOwnedThenReturns() {
    Collection<Account> accounts = this.bankService.updateAccounts("owner", "not-owner");
    assertThat(accounts).hasSize(1);
    assertThat(accounts.get(0).getOwner()).isEqualTo("owner");
}
@Autowired
lateinit var bankService: BankService

@WithMockUser(username="owner")
@Test
fun readAccountsWhenOwnedThenReturns() {
    val accounts: Collection<Account> = bankService.updateAccounts("owner", "not-owner")
    assertThat(accounts).hasSize(1)
    assertThat(accounts[0].owner).isEqualTo("owner")
}
@PostFilter はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。

@PostFilter は、配列、コレクション、マップ、ストリーム (ストリームが開いている限り) をサポートします。

例: 上記の readAccounts 宣言は、次の他の 3 つの宣言と同じように機能します。

  • Java

  • Kotlin

@PostFilter("filterObject.owner == authentication.name")
public Collection<Account> readAccounts(String... ids)

@PostFilter("filterObject.owner == authentication.name")
public Account[] readAccounts(String... ids)

@PostFilter("filterObject.value.owner == authentication.name")
public Map<String, Account> readAccounts(String... ids)

@PostFilter("filterObject.owner == authentication.name")
public Stream<Account> readAccounts(String... ids)
@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Collection<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Array<Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Map<String, Account>

@PostFilter("filterObject.owner == authentication.name")
fun readAccounts(vararg ids: String): Stream<Account>

その結果、上記のメソッドは、owner 属性がログインユーザーの name と一致する Account インスタンスを返します。

メモリ内フィルタリングは明らかにコストがかかるため、代わりにデータ層でデータをフィルタリングする方が良いかどうかを検討してください。

@Secured によるメソッド呼び出しの認可

@Secured (Javadoc) は、呼び出しを承認するための従来のオプションです。@PreAuthorize がこれに取って代わり、代わりに推奨されます。

@Secured アノテーションを使用するには、まずメソッドセキュリティ宣言を次のように変更して有効にする必要があります。

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity(securedEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
<sec:method-security secured-enabled="true"/>

これにより、Spring Security は、@Secured アノテーションが付けられたメソッド、クラス、インターフェースを認可する、対応するメソッドインターセプターを公開します。

JSR-250 アノテーションを使用したメソッド呼び出しの認可

JSR-250 (英語) アノテーションを使用したい場合は、Spring Security もそれをサポートしています。@PreAuthorize の方が表現力が豊かなのでおすすめです。

JSR-250 アノテーションを使用するには、まずメソッドセキュリティ宣言を次のように変更して有効にする必要があります。

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity(jsr250Enabled = true)
@EnableMethodSecurity(jsr250Enabled = true)
<sec:method-security jsr250-enabled="true"/>

これにより、Spring Security は、@RolesAllowed@PermitAll@DenyAll アノテーションが付けられたメソッド、クラス、インターフェースを認可する、対応するメソッドインターセプターを公開します。

クラスまたはインターフェースレベルでのアノテーションの宣言

クラスおよびインターフェースレベルでメソッドセキュリティアノテーションを付けることもサポートされています。

次のようなクラスレベルの場合:

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    fun endpoint(): String { ... }
}

その場合、すべてのメソッドはクラスレベルの動作を継承します。

または、クラスレベルとメソッドレベルの両方で次のように宣言されている場合:

  • Java

  • Kotlin

@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
public class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public String endpoint() { ... }
}
@Controller
@PreAuthorize("hasAuthority('ROLE_USER')")
open class MyController {
    @GetMapping("/endpoint")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    fun endpoint(): String { ... }
}

その後、アノテーションを宣言したメソッドがクラスレベルのアノテーションをオーバーライドします。

同じことがインターフェースにも当てはまりますが、クラスが 2 つの異なるインターフェースからアノテーションを継承する場合、起動は失敗します。これは、Spring Security ではどちらを使用するかを判断できないためです。

このような場合、具象メソッドにアノテーションを追加することで曖昧さを解決できます。

メタアノテーションの使用

Method Security はメタアノテーションをサポートします。これは、アプリケーション固有のユースケースに基づいて、任意のアノテーションを取得して読みやすさを向上できることを意味します。

例: 次のように @PreAuthorize("hasRole('ADMIN')") を @IsAdmin に単純化できます。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
annotation class IsAdmin

その結果、セキュリティで protected メソッドで、代わりに次のことができるようになります。

  • Java

  • Kotlin

@Component
public class BankService {
	@IsAdmin
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@IsAdmin
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

これにより、メソッド定義がより読みやすくなります。

メタアノテーション式のテンプレート化

メタアノテーションテンプレートの使用を選択することもできます。これにより、より強力なアノテーション定義が可能になります。

まず、次の Bean を公開します。

  • Java

  • Kotlin

@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
	return new AnnotationTemplateExpressionDefaults();
}
companion object {
    @Bean
    fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults {
        return AnnotationTemplateExpressionDefaults()
    }
}

これで、@IsAdmin の代わりに、次のように @HasRole のようなより強力なものを作成できます。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
public @interface HasRole {
	String value();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('{value}')")
annotation class HasRole(val value: String)

その結果、セキュリティで protected メソッドで、代わりに次のことができるようになります。

  • Java

  • Kotlin

@Component
public class BankService {
	@HasRole("ADMIN")
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@HasRole("ADMIN")
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

これはメソッド変数とすべてのアノテーション型でも機能しますが、結果として得られる SpEL 式が正しいものになるように、引用符を正しく扱うように注意する必要があります。

例: 次の @HasAnyRole アノテーションを考えてみましょう。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
public @interface HasAnyRole {
	String[] roles();
}
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({roles})")
annotation class HasAnyRole(val roles: Array<String>)

その場合、引用符は式ではなく、次のようにパラメーター値で使用する必要があることに気づくでしょう。

  • Java

  • Kotlin

@Component
public class BankService {
	@HasAnyRole(roles = { "'USER'", "'ADMIN'" })
	public Account readAccount(Long id) {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}
@Component
open class BankService {
	@HasAnyRole(roles = arrayOf("'USER'", "'ADMIN'"))
	fun readAccount(val id: Long): Account {
        // ... is only returned if the `Account` belongs to the logged in user
	}
}

置き換えられると、式は @PreAuthorize("hasAnyRole('USER', 'ADMIN')") になります。

特定のアノテーションの有効化

@EnableMethodSecurity の事前構成をオフにして、独自の構成に置き換えることができます。AuthorizationManager または Pointcut をカスタマイズする場合は、これを行うことを選択できます。または、@PostAuthorize などの特定のアノテーションのみを有効にしたい場合もあります。

これは次の方法で行うことができます。

@PostAuthorize 構成のみ
  • Java

  • Kotlin

  • XML

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize() {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize();
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize() : Advisor {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize()
	}
}
<sec:method-security pre-post-enabled="false"/>

<aop:config/>

<bean id="postAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
	factory-method="postAuthorize"/>

上記のスニペットは、最初に Method Security の事前構成を無効にし、次に @PostAuthorize インターセプター自体を公開することでこれを実現します。

<intercept-methods> による認証

Spring Security のアノテーションベースのサポートの使用はメソッドのセキュリティのために推奨されますが、XML を使用して Bean 認可ルールを宣言することもできます。

代わりに XML 構成で宣言する必要がある場合は、次のように <intercept-methods> を使用できます。

  • XML

<bean class="org.mycompany.MyController">
    <intercept-methods>
        <protect method="get*" access="hasAuthority('read')"/>
        <protect method="*" access="hasAuthority('write')"/>
    </intercept-methods>
</bean>
これは、プレフィックスまたは名前による照合方法のみをサポートします。ニーズがそれよりも複雑な場合は、代わりにアノテーションサポートを使用してください

プログラムによるメソッドの承認

すでに見たように、メソッドセキュリティ SpEL 式を使用して重要な認可ルールを指定できる方法はいくつかあります。

ロジックを SpEL ベースではなく Java ベースにする方法はいくつかあります。これにより、Java 言語全体にアクセスできるようになり、テスト容易性とフロー制御が向上します。

SpEL でのカスタム Bean の使用

メソッドをプログラム的に認証する最初の方法は、2 段階のプロセスです。

まず、次のように MethodSecurityExpressionOperations インスタンスを受け取るメソッドを持つ Bean を宣言します。

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public boolean decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): boolean {
        // ... authorization logic
    }
}

次に、次の方法でアノテーション内でその Bean を参照します。

  • Java

  • Kotlin

@Controller
public class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    public String endpoint() {
        // ...
    }
}
@Controller
open class MyController {
    @PreAuthorize("@authz.decide(#root)")
    @GetMapping("/endpoint")
    fun String endpoint() {
        // ...
    }
}

Spring Security は、メソッド呼び出しごとに Bean 上で指定されたメソッドを呼び出します。

これの優れた点は、すべての認可ロジックが別個のクラスにあり、個別に単体テストを行って正確性を検証できることです。完全な Java 言語にもアクセスできます。

Boolean を返すだけでなく、コードが決定を控えていることを示すために null を返すこともできます。

決定の性質に関する詳細情報を含める場合は、代わりに次のようなカスタム AuthorizationDecision を返すことができます。

  • Java

  • Kotlin

@Component("authz")
public class AuthorizationLogic {
    public AuthorizationDecision decide(MethodSecurityExpressionOperations operations) {
        // ... authorization logic
        return new MyAuthorizationDecision(false, details);
    }
}
@Component("authz")
open class AuthorizationLogic {
    fun decide(val operations: MethodSecurityExpressionOperations): AuthorizationDecision {
        // ... authorization logic
        return MyAuthorizationDecision(false, details)
    }
}

または、カスタム AuthorizationDeniedException インスタンスをスローします。ただし、スタックトレースを生成するコストが発生しないため、オブジェクトを返すことが推奨されることに注意してください。

その後、認可結果の処理方法をカスタマイズするときに、カスタム詳細にアクセスできます。

カスタム認証マネージャーの使用

メソッドをプログラム的に承認する 2 番目の方法は、カスタム AuthorizationManager を作成することです。

まず、次のように認可マネージャーインスタンスを宣言します。

  • Java

  • Kotlin

@Component
public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        // ... authorization logic
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) {
        // ... authorization logic
    }
}
@Component
class MyAuthorizationManager : AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> {
    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocation): AuthorizationDecision {
        // ... authorization logic
    }

    override fun check(authentication: Supplier<Authentication>, invocation: MethodInvocationResult): AuthorizationDecision {
        // ... authorization logic
    }
}

次に、AuthorizationManager を実行するタイミングに対応するポイントカットを使用してメソッドインターセプターを公開します。例: @PreAuthorize と @PostAuthorize の動作を次のように置き換えることができます。

@PreAuthorize および @PostAuthorize 構成のみ
  • Java

  • Kotlin

  • XML

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
    @Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor preAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	Advisor postAuthorize(MyAuthorizationManager manager) {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
	}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
   	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun preAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager)
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	fun postAuthorize(val manager: MyAuthorizationManager) : Advisor {
		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager)
	}
}
<sec:method-security pre-post-enabled="false"/>

<aop:config/>

<bean id="preAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor"
	factory-method="preAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>

<bean id="postAuthorize"
	class="org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor"
	factory-method="postAuthorize">
    <constructor-arg ref="myAuthorizationManager"/>
</bean>

AuthorizationInterceptorsOrder で指定された順序定数を使用して、Spring Security メソッドインターセプターの間にインターセプターを配置できます。

式処理のカスタマイズ

あるいは、3 番目に、各 SpEL 式の処理方法をカスタマイズできます。これを行うには、次のようにカスタム MethodSecurityExpressionHandler (Javadoc) を公開します。

カスタム MethodSecurityExpressionHandler
  • Java

  • Kotlin

  • XML

@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
	DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
	handler.setRoleHierarchy(roleHierarchy);
	return handler;
}
companion object {
	@Bean
	fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
		val handler = DefaultMethodSecurityExpressionHandler()
		handler.setRoleHierarchy(roleHierarchy)
		return handler
	}
}
<sec:method-security>
	<sec:expression-handler ref="myExpressionHandler"/>
</sec:method-security>

<bean id="myExpressionHandler"
		class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
	<property name="roleHierarchy" ref="roleHierarchy"/>
</bean>

static メソッドを使用して MethodSecurityExpressionHandler を公開し、Spring Security のメソッドセキュリティ @Configuration クラスを初期化する前に Spring がそれを公開するようにします。

DefaultMessageSecurityExpressionHandler をサブクラス化して、デフォルト以外の独自のカスタム認証式を追加することもできます。

AOT での作業

Spring Security は、アプリケーションコンテキスト内のすべての Bean をスキャンして、@PreAuthorize または @PostAuthorize を使用するメソッドを探します。見つかった場合、セキュリティ式内で使用されるすべての Bean を解決し、その Bean の適切なランタイムヒントを登録します。@AuthorizeReturnObject を使用するメソッドが見つかった場合、メソッドの戻り値の型内で @PreAuthorize および @PostAuthorize アノテーションを再帰的に検索し、それに応じて登録します。

例: 次の Spring Boot アプリケーションを検討してください。

  • Java

  • Kotlin

@Service
public class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    public Account getAccountById(String accountId) {
        // ...
    }

}

public class Account {

    private final String accountNumber;

    // ...

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    public String getAccountNumber() {
        return this.accountNumber;
    }

    @AuthorizeReturnObject (5)
    public User getUser() {
        return new User("John Doe");
    }

}

public class User {

    private final String fullName;

    // ...

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    public String getFullName() {
        return this.fullName;
    }

}
@Service
class AccountService { (1)

    @PreAuthorize("@authz.decide()") (2)
    @AuthorizeReturnObject (3)
    fun getAccountById(accountId: String): Account {
        // ...
    }

}

class Account(private val accountNumber: String) {

    @PreAuthorize("@accountAuthz.canViewAccountNumber()") (4)
    fun getAccountNumber(): String {
        return this.accountNumber
    }

    @AuthorizeReturnObject (5)
    fun getUser(): User {
        return User("John Doe")
    }

}

class User(private val fullName: String) {

    @PostAuthorize("@myOtherAuthz.decide()") (6)
    fun getFullName(): String {
        return this.fullName
    }

}
1Spring Security は AccountService Bean を見つける
2@PreAuthorize を使用するメソッドを見つけると、式内で使用されている Bean 名(この場合は authz)を解決し、Bean クラスのランタイムヒントを登録します。
3@AuthorizeReturnObject を使用するメソッドを見つけると、メソッドの戻り値の型を調べて @PreAuthorize または @PostAuthorize があるかどうかを確認します。
4 次に、別の Bean 名を持つ @PreAuthorize を見つけます: accountAuthz; ランタイムヒントは Bean クラスにも登録されています
5 別の @AuthorizeReturnObject を見つけると、メソッドの戻り値の型を再度調べます。
6 ここで、@PostAuthorize が、さらに別の Bean 名 myOtherAuthz を使用して見つかります。ランタイムヒントは、Bean クラスにも登録されています。

メソッドの実際の戻り値の型は消去されたジェネリクス型に隠されている可能性があるため、Spring Security では事前にその型を判別できない場合が多くあります。

次のサービスを検討してください。

  • Java

  • Kotlin

@Service
public class AccountService {

    @AuthorizeReturnObject
    public List<Account> getAllAccounts() {
        // ...
    }

}
@Service
class AccountService {

    @AuthorizeReturnObject
    fun getAllAccounts(): List<Account> {
        // ...
    }

}

この場合、ジェネリクス型は消去されるため、@PreAuthorize と @PostAuthorize を確認するために Account にアクセスする必要があることが Spring Security には事前にわかりません。

これに対処するには、次のように PrePostAuthorizeExpressionBeanHintsRegistrar (Javadoc) を公開します。

  • Java

  • Kotlin

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegistrar registerTheseToo() {
    return new PrePostAuthorizeExpressionBeanHintsRegistrar(Account.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(): SecurityHintsRegistrar {
    return PrePostAuthorizeExpressionBeanHintsRegistrar(Account::class.java)
}

AspectJ による認証

カスタムポイントカットを使用したマッチングメソッド

Spring AOP に基づいて構築されているため、リクエストレベルの認可と同様に、アノテーションに関連しないパターンを宣言できます。これには、メソッドレベルの認可ルールを一元化できるという潜在的な利点があります。

例: 次のように、独自の Advisor を公開するか、<protect-pointcut> を使用して AOP 式をサービス層の認可ルールに一致させることができます。

  • Java

  • Kotlin

  • XML

import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
    AspectJExpressionPointcut pattern = new AspectJExpressionPointcut()
    pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
    return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
}
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole

companion object {
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    fun protectServicePointcut(): Advisor {
        val pattern = AspectJExpressionPointcut()
        pattern.setExpression("execution(* com.mycompany.*Service.*(..))")
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("USER"))
    }
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.*(..))" access="hasRole('USER')"/>
</sec:method-security>

AspectJ バイトウィービングとの統合

AspectJ を使用して Spring Security アドバイスを Bean のバイトコードに織り込むことで、パフォーマンスが向上する場合があります。

AspectJ を設定した後、@EnableMethodSecurity アノテーションまたは <method-security> 要素で、AspectJ を使用していることを非常に簡単に記述することができます。

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
@EnableMethodSecurity(mode=AdviceMode.ASPECTJ)
<sec:method-security mode="aspectj"/>

その結果、Spring Security はアドバイザーを AspectJ アドバイスとして公開し、それに応じて組み込むことができるようになります。

順序の指定

すでに記述されていたように、各アノテーションには Spring AOP メソッドインターセプターがあり、これらのそれぞれには Spring AOP アドバイザチェーン内に位置があります。

つまり、@PreFilter メソッドインターセプターの次数は 100、@PreAuthorize の次数は 200 などとなります。

これに注意することが重要な理由は、Integer.MAX_VALUE の順序を持つ @EnableTransactionManagement などの他の AOP ベースのアノテーションがあるためです。つまり、デフォルトではアドバイザチェーンの最後に配置されます。

場合によっては、Spring Security の前に他のアドバイスを実行することが有益な場合があります。例: @Transactional および @PostAuthorize のアノテーションが付けられたメソッドがある場合、AccessDeniedException によってロールバックが発生するように、@PostAuthorize の実行時にトランザクションが開いたままにしておくことが必要な場合があります。

メソッド認可アドバイスが実行される前に @EnableTransactionManagement にトランザクションを開かせるには、次のように @EnableTransactionManagement の順序を設定します。

  • Java

  • Kotlin

  • XML

@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>

最も早いメソッドインターセプタ (@PreFilter) は 100 の次数に設定されているため、ゼロに設定すると、トランザクションアドバイスがすべての Spring Security アドバイスよりも前に実行されることを意味します。

SpEL で認可を表現する

SpEL を使用した例をいくつか見てきましたため、ここで API についてもう少し詳しく説明します。

Spring Security は、すべての認証フィールドとメソッドをルートオブジェクトのセットにカプセル化します。最も一般的なルートオブジェクトは SecurityExpressionRoot と呼ばれ、MethodSecurityExpressionRoot の基礎となります。Spring Security は、認可式の評価を準備するときに、このルートオブジェクトを MethodSecurityEvaluationContext に提供します。

認可式のフィールドとメソッドの使用

これにより最初に提供されるのは、SpEL 式に対する認可フィールドとメソッドの強化されたセットです。以下に、最も一般的な方法の概要を示します。

  • permitAll - このメソッドを呼び出すには認可は必要ありません。この場合、Authentication はセッションから取得されないことに注意してください。

  • denyAll - このメソッドはいかなる状況でも許可されません。この場合、Authentication はセッションから取得されないことに注意してください。

  • hasAuthority - このメソッドでは、Authentication が指定された値と一致する GrantedAuthority を持っている必要があります。

  • hasRole - ROLE_ またはデフォルトのプレフィックスとして設定されているものをプレフィックスとする hasAuthority のショートカット

  • hasAnyAuthority - このメソッドでは、Authentication が指定された値のいずれかに一致する GrantedAuthority を持っている必要があります。

  • hasAnyRole - ROLE_ またはデフォルトのプレフィックスとして設定されているものをプレフィックスとする hasAnyAuthority のショートカット

  • hasPermission - オブジェクトレベルの認証を行うための PermissionEvaluator インスタンスへのフック

最も一般的なフィールドを簡単に説明します。

  • authentication - このメソッド呼び出しに関連付けられた Authentication インスタンス

  • principal - このメソッド呼び出しに関連付けられた Authentication#getPrincipal 

パターン、ルール、組み合わせる方法を学習したため、このより複雑な例で何が起こっているかを理解できるはずです。

リクエストを承認する
  • Java

  • Kotlin

  • XML

@Component
public class MyService {
    @PreAuthorize("denyAll") (1)
    MyResource myDeprecatedMethod(...);

    @PreAuthorize("hasRole('ADMIN')") (2)
    MyResource writeResource(...)

    @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
    MyResource deleteResource(...)

    @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
    MyResource readResource(...);

	@PreAuthorize("@authz.check(authentication, #root)")
    MyResource shareResource(...);
}
@Component
open class MyService {
    @PreAuthorize("denyAll") (1)
    fun myDeprecatedMethod(...): MyResource

    @PreAuthorize("hasRole('ADMIN')") (2)
    fun writeResource(...): MyResource

    @PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") (3)
    fun deleteResource(...): MyResource

    @PreAuthorize("principal.claims['aud'] == 'my-audience'") (4)
    fun readResource(...): MyResource

    @PreAuthorize("@authz.check(#root)")
    fun shareResource(...): MyResource
}
<sec:method-security>
    <protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> (1)
    <protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> (2)
    <protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> (3)
    <protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> (4)
    <protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> (5)
</sec:method-security>
1 このメソッドは、いかなる理由でも誰も呼び出すことはできません
2 このメソッドは、ROLE_ADMIN 権限を付与された Authentication によってのみ呼び出すことができます。
3 このメソッドは、db および ROLE_ADMIN 権限を付与された Authentication によってのみ呼び出すことができます。
4 このメソッドは、"my-audience" に等しい aud クレームを持つ Princpal によってのみ呼び出すことができます。
5 このメソッドは、Bean authz の check メソッドが true を返す場合にのみ呼び出すことができます。

上記の authz のような Bean を使用して、プログラムによる認証を追加できます。

メソッドパラメーターの使用

さらに、Spring Security はメソッドパラメーターを検出するメカニズムを提供するため、SpEL 式でもメソッドパラメーターにアクセスできます。

完全な参照として、Spring Security は DefaultSecurityParameterNameDiscoverer を使用してパラメーター名を検出します。デフォルトでは、メソッドに対して次のオプションが試行されます。

  1. Spring Security の @P アノテーションがメソッドへの単一の引数に存在する場合、その値が使用されます。次の例では、@P アノテーションを使用します。

    • Java

    • Kotlin

    import org.springframework.security.access.method.P;
    
    ...
    
    @PreAuthorize("hasPermission(#c, 'write')")
    public void updateContact(@P("c") Contact contact);
    import org.springframework.security.access.method.P
    
    ...
    
    @PreAuthorize("hasPermission(#c, 'write')")
    fun doSomething(@P("c") contact: Contact?)

    この式の目的は、現在の Authentication がこの Contact インスタンスに対して特に write 権限を持っていることを要求することです。

    バックグラウンドでは、これは AnnotationParameterNameDiscoverer を使用して実装されます。これは、指定されたアノテーションの value 属性をサポートするようにカスタマイズできます。

  2. メソッドの少なくとも 1 つのパラメーターに Spring Data's @Param アノテーションが存在する場合、その値が使用されます。次の例では、@Param アノテーションを使用します。

    • Java

    • Kotlin

    import org.springframework.data.repository.query.Param;
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);
    import org.springframework.data.repository.query.Param
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    fun findContactByName(@Param("n") name: String?): Contact?

    この式の目的は、呼び出しが認可されるためには name が Authentication#getName と等しいことを要求することです。

    バックグラウンドでは、これは AnnotationParameterNameDiscoverer を使用して実装されます。これは、指定されたアノテーションの value 属性をサポートするようにカスタマイズできます。

  3. -parameters 引数を使用してコードをコンパイルする場合、標準の JDK リフレクション API を使用してパラメーター名が検出されます。これはクラスとインターフェースの両方で機能します。

  4. 最後に、デバッグシンボルを使用してコードをコンパイルすると、パラメーター名はデバッグシンボルを使用して検出されます。インターフェースにはパラメーター名に関するデバッグ情報がないため、これは機能しません。インターフェースの場合は、アノテーションまたは -parameters アプローチを使用する必要があります。

任意のオブジェクトの承認

Spring Security は、メソッドセキュリティアノテーションが付けられたオブジェクトのラップもサポートします。

これを実現する最も簡単な方法は、認証するオブジェクトを返すメソッドに @AuthorizeReturnObject アノテーションを付けることです。

例: 次の User クラスを検討してください。

  • Java

  • Kotlin

public class User {
	private String name;
	private String email;

	public User(String name, String email) {
		this.name = name;
		this.email = email;
	}

	public String getName() {
		return this.name;
	}

    @PreAuthorize("hasAuthority('user:read')")
    public String getEmail() {
		return this.email;
    }
}
class User (val name:String, @get:PreAuthorize("hasAuthority('user:read')") val email:String)

次のようなインターフェースがあるとします。

  • Java

  • Kotlin

public class UserRepository {
	@AuthorizeReturnObject
    Optional<User> findByName(String name) {
		// ...
    }
}
class UserRepository {
    @AuthorizeReturnObject
    fun findByName(name:String?): Optional<User?>? {
        // ...
    }
}

その後、findById から返される User は、他の Spring セキュリティ保護コンポーネントと同様に保護されます。

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    Optional<User> securedUser = users.findByName("name");
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> securedUser.get().getEmail());
}
import jdk.incubator.vector.VectorOperators.Test
import java.nio.file.AccessDeniedException
import java.util.*

@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy{securedUser.get().getEmail()}
}

クラスレベルでの @AuthorizeReturnObject の使用

@AuthorizeReturnObject はクラスレベルに配置できます。ただし、これは Spring Security が StringInteger、その他の型を含むすべての戻りオブジェクトをプロキシしようとすることを意味することに注意してください。これは多くの場合、望ましい動作ではありません。

メソッドが intStringDouble やそれらの型のコレクションなどの値型を返すクラスまたはインターフェースで @AuthorizeReturnObject を使用する場合は、次のように適切な AuthorizationAdvisorProxyFactory.TargetVisitor も公開する必要があります。

  • Java

  • Kotlin

@Bean
static Customizer<AuthorizationAdvisorProxyFactory> skipValueTypes() {
    return (factory) -> factory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
}
@Bean
open fun skipValueTypes() = Customizer<AuthorizationAdvisorProxyFactory> {
    it.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())
}

独自の AuthorizationAdvisorProxyFactory.TargetVisitor を設定して、任意の型のセットのプロキシをカスタマイズできます。

プログラムによるプロキシ

特定のオブジェクトをプログラムでプロキシすることもできます。

これを実現するには、提供されている AuthorizationProxyFactory インスタンスをオートワイヤーします。これは、構成したメソッドセキュリティインターセプターに基づいています。@EnableMethodSecurity を使用している場合、これは、デフォルトで @PreAuthorize@PostAuthorize@PreFilter@PostFilter のインターセプターが含まれることを意味します。

次の方法でユーザーのインスタンスをプロキシできます。

  • Java

  • Kotlin

@Autowired
AuthorizationProxyFactory proxyFactory;

@Test
void getEmailWhenProxiedThenAuthorizes() {
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Autowired
var proxyFactory:AuthorizationProxyFactory? = null

@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

手作業による構築

Spring Security のデフォルトとは異なるものが必要な場合は、独自のインスタンスを定義することもできます。

たとえば、AuthorizationProxyFactory インスタンスを次のように定義します。

  • Java

  • Kotlin

import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import static org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize;
// ...

AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes());
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.preAuthorize

// ...

val proxyFactory: AuthorizationProxyFactory = AuthorizationProxyFactory(preAuthorize())
// and if needing to skip value types
proxyFactory.setTargetVisitor(TargetVisitor.defaultsSkipValueTypes())

次に、User の任意のインスタンスを次のようにラップできます。

  • Java

  • Kotlin

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
    User user = new User("name", "email");
    assertThat(user.getEmail()).isNotNull();
    User securedUser = proxyFactory.proxy(user);
    assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail);
}
@Test
fun getEmailWhenProxiedThenAuthorizes() {
    val proxyFactory: AuthorizationProxyFactory = AuthorizationAdvisorProxyFactory.withDefaults()
    val user: User = User("name", "email")
    assertThat(user.getEmail()).isNotNull()
    val securedUser: User = proxyFactory.proxy(user)
    assertThatExceptionOfType(AccessDeniedException::class.java).isThrownBy(securedUser::getEmail)
}

コレクションのプロキシ

AuthorizationProxyFactory は、要素型をプロキシすることで Java コレクション、ストリーム、配列、オプション、反復子をサポートし、値型をプロキシすることでマップをサポートします。

つまり、オブジェクトの List をプロキシする場合、以下も機能します。

  • Java

@Test
void getEmailWhenProxiedThenAuthorizes() {
	AuthorizationProxyFactory proxyFactory = AuthorizationAdvisorProxyFactory.withDefaults();
    List<User> users = List.of(ada, albert, marie);
    List<User> securedUsers = proxyFactory.proxy(users);
	securedUsers.forEach((securedUser) ->
        assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(securedUser::getEmail));
}

プロキシクラス

限られた状況では、Class 自体をプロキシすることが有益な場合があり、AuthorizationProxyFactory もこれをサポートしています。これは、プロキシを作成するための Spring Framework サポートで ProxyFactory#getProxyClass を呼び出すこととほぼ同等です。

これが便利なのは、Spring AOT のように、事前にプロキシクラスを構築する必要がある場合です。

すべてのメソッドセキュリティアノテーションのサポート

AuthorizationProxyFactory は、アプリケーションで有効になっているメソッドセキュリティアノテーションをサポートします。これは、Bean として公開されている AuthorizationAdvisor クラスに基づいています。

@EnableMethodSecurity はデフォルトで @PreAuthorize@PostAuthorize@PreFilter@PostFilter アドバイザーを公開するため、通常は機能をアクティブ化するために何もする必要はありません。

returnObject または filterObject を使用する SpEL 式はプロキシの背後に配置されるため、オブジェクトに完全にアクセスできます。

カスタムアドバイス

適用したいセキュリティアドバイスがある場合は、次のように独自の AuthorizationAdvisor を公開できます。

  • Java

  • Kotlin

@EnableMethodSecurity
class SecurityConfig {
    @Bean
    static AuthorizationAdvisor myAuthorizationAdvisor() {
        return new AuthorizationAdvisor();
    }
}
@EnableMethodSecurity
internal class SecurityConfig {
    @Bean
    fun myAuthorizationAdvisor(): AuthorizationAdvisor {
        return AuthorizationAdvisor()
    }
]

そして、Spring Security は、オブジェクトをプロキシするときに AuthorizationProxyFactory が追加するアドバイスセットにそのアドバイザーを追加します。

Jackson での作業

この機能の強力な使用箇所の 1 つは、次のようにコントローラーから保護された値を返すことです。

  • Java

  • Kotlin

@RestController
public class UserController {
    @Autowired
    AuthorizationProxyFactory proxyFactory;

    @GetMapping
    User currentUser(@AuthenticationPrincipal User user) {
        return this.proxyFactory.proxy(user);
    }
}
@RestController
class UserController  {
    @Autowired
    var proxyFactory: AuthorizationProxyFactory? = null

    @GetMapping
    fun currentUser(@AuthenticationPrincipal user:User?): User {
        return proxyFactory.proxy(user)
    }
}

次のような MethodAuthorizationDeniedHandler を追加する必要があります。

  • Java

  • Kotlin

@Component
public class Null implements MethodAuthorizationDeniedHandler {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
public class User {
	...
}
@Component
class Null : MethodAuthorizationDeniedHandler {
    override fun handleDeniedInvocation(methodInvocation: MethodInvocation?, authorizationResult: AuthorizationResult?): Any? {
        return null
    }
}

// ...

@HandleAuthorizationDenied(handlerClass = Null.class)
open class User {
	...
}

次に、ユーザーの認証レベルに基づいて異なる JSON 直列化が表示されます。user:read 権限を持っていない場合は、次のように表示されます。

{
    "name" : "name",
    "email" : null
}

そしてもし彼らにその権限があれば、次のことがわかるでしょう:

{
    "name" : "name",
    "email" : "email"
}

また、権限のないユーザーに JSON キーを公開したくない場合は、Spring Boot プロパティ spring.jackson.default-property-inclusion=non_null を追加して、直列化から null 値を除外することもできます。

AOT での作業

Spring Security は、アプリケーションコンテキスト内のすべての Bean をスキャンして、@AuthorizeReturnObject を使用するメソッドを探します。見つかった場合は、事前に適切なプロキシクラスを作成して登録します。また、@AuthorizeReturnObject を使用する他のネストされたオブジェクトを再帰的に検索し、それに応じて登録します。

例: 次の Spring Boot アプリケーションを検討してください。

  • Java

  • Kotlin

@SpringBootApplication
public class MyApplication {
	@RestController
    public static class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        Message getMessage() { (2)
			return new Message(someUser, "hello!");
        }
    }

	public static class Message { (3)
		User to;
		String text;

		// ...

        @AuthorizeReturnObject
        public User getTo() { (4)
			return this.to;
        }

		// ...
	}

	public static class User { (5)
		// ...
	}

	public static void main(String[] args) {
		SpringApplication.run(MyApplication.class);
	}
}
@SpringBootApplication
open class MyApplication {
	@RestController
    open class MyController { (1)
		@GetMapping
        @AuthorizeReturnObject
        fun getMessage():Message { (2)
			return Message(someUser, "hello!")
        }
    }

	open class Message { (3)
		val to: User
		val test: String

		// ...

        @AuthorizeReturnObject
        fun getTo(): User { (4)
			return this.to
        }

		// ...
	}

	open class User { (5)
		// ...
	}

	fun main(args: Array<String>) {
		SpringApplication.run(MyApplication.class)
	}
}
1- まず、Spring Security は MyController Bean を見つけます
2@AuthorizeReturnObject を使用するメソッドを見つけて、戻り値である Message をプロキシし、そのプロキシクラスを RuntimeHints に登録します。
3- 次に、Message を走査して、@AuthorizeReturnObject が使用されているかどうかを確認します。
4@AuthorizeReturnObject を使用するメソッドを見つけて、戻り値である User をプロキシし、そのプロキシクラスを RuntimeHints に登録します。
5- 最後に、User を走査して @AuthorizeReturnObject を使用しているかどうかを確認します。何も見つからない場合、アルゴリズムは完了します。

プロキシクラスは消去されたジェネリクス型に隠されている可能性があるため、Spring Security が事前にプロキシクラスを判別できない場合がよくあります。

MyController への次の変更を検討してください。

  • Java

  • Kotlin

@RestController
public static class MyController {
    @GetMapping
    @AuthorizeReturnObject
    List<Message> getMessages() {
        return List.of(new Message(someUser, "hello!"));
    }
}
@RestController
static class MyController {
    @AuthorizeReturnObject
    @GetMapping
    fun getMessages(): Array<Message> = arrayOf(Message(someUser, "hello!"))
}

この場合、ジェネリクス型は消去されるため、実行時に Message をプロキシする必要があることは Spring Security には事前にわかりません。

これに対処するには、次のように AuthorizeProxyFactoryHintsRegistrar を公開します。

  • Java

  • Kotlin

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static SecurityHintsRegsitrar registerTheseToo(AuthorizationProxyFactory proxyFactory) {
	return new AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message.class);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun registerTheseToo(proxyFactory: AuthorizationProxyFactory?): SecurityHintsRegistrar {
    return AuthorizeReturnObjectHintsRegistrar(proxyFactory, Message::class.java)
}

Spring Security はそのクラスを登録し、以前と同じようにその型を走査します。

認可が拒否された場合のフォールバック値の提供

必要な権限なしでメソッドが呼び出されたときに、AuthorizationDeniedException をスローしたくないシナリオがいくつかあります。代わりに、マスクされた結果などの後処理された結果、またはメソッドを呼び出す前に認可が拒否された場合のデフォルト値を返すことが必要になる場合があります。

Spring Security は、@HandleAuthorizationDenied (Javadoc) を使用してメソッド呼び出し時に拒否された認可の処理をサポートします。ハンドラーは、メソッド呼び出し自体からスローされた AuthorizationDeniedException (Javadoc) だけでなく、@PreAuthorize および @PostAuthorize アノテーションで発生した拒否された認可にも機能します。

前のセクションの例を考えてみましょう。ただし、AccessDeniedException を null 戻り値に変換するために AccessDeniedExceptionInterceptor を作成する代わりに、@HandleAuthorizationDenied の handlerClass 属性を使用します。

  • Java

  • Kotlin

public class NullMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return null;
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public NullMethodAuthorizationDeniedHandler nullMethodAuthorizationDeniedHandler() {
        return new NullMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class NullMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler { (1)

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return null
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean (2)
    fun nullMethodAuthorizationDeniedHandler(): NullMethodAuthorizationDeniedHandler {
        return MaskMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PreAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = NullMethodAuthorizationDeniedHandler::class) val email:String) (3)
1null 値を返す MethodAuthorizationDeniedHandler の実装を作成する
2NullMethodAuthorizationDeniedHandler を Bean として登録する
3 メソッドに @HandleAuthorizationDenied をアノテーションし、NullMethodAuthorizationDeniedHandler を handlerClass 属性に渡します。

そして、AccessDeniedException ではなく null 値が返されることを確認できます。

@Bean メソッドを作成する代わりに、クラスに @Component をアノテーションすることもできます。

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenNullEmail() {
    Optional<User> securedUser = users.findByName("name");
    assertThat(securedUser.get().getEmail()).isNull();
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenNullEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    assertThat(securedUser.get().getEmail()).isNull()
}

メソッド呼び出しからの拒否結果の使用

拒否された結果から派生した安全な結果を返す必要があるシナリオがいくつかあります。例: ユーザーにメールアドレスを表示する権限がない場合は、元のメールアドレスに何らかのマスキングを適用する必要があります。つまり、[ メールで保護 ] (英語) use******@example.com になります。

このようなシナリオでは、引数として MethodInvocationResult (Javadoc) を持つ MethodAuthorizationDeniedHandler から handleDeniedInvocationResult をオーバーライドできます。前の例を続けますが、null を返す代わりに、メールのマスクされた値を返します。

  • Java

  • Kotlin

public class EmailMaskingMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler { (1)

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return "***";
    }

    @Override
    public Object handleDeniedInvocationResult(MethodInvocationResult methodInvocationResult, AuthorizationResult authorizationResult) {
        String email = (String) methodInvocationResult.getResult();
        return email.replaceAll("(^[^@]{3}|(?!^)\\G)[^@]", "$1*");
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean (2)
    public EmailMaskingMethodAuthorizationDeniedHandler emailMaskingMethodAuthorizationDeniedHandler() {
        return new EmailMaskingMethodAuthorizationDeniedHandler();
    }

}

public class User {
    // ...

    @PostAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler.class)
    public String getEmail() {
        return this.email;
    }
}
class EmailMaskingMethodAuthorizationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        return "***"
    }

    override fun handleDeniedInvocationResult(methodInvocationResult: MethodInvocationResult, authorizationResult: AuthorizationResult): Any {
        val email = methodInvocationResult.result as String
        return email.replace("(^[^@]{3}|(?!^)\\G)[^@]".toRegex(), "$1*")
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun emailMaskingMethodAuthorizationDeniedHandler(): EmailMaskingMethodAuthorizationDeniedHandler {
        return EmailMaskingMethodAuthorizationDeniedHandler()
    }

}

class User (val name:String, @PostAuthorize(value = "hasAuthority('user:read')") @HandleAuthorizationDenied(handlerClass = EmailMaskingMethodAuthorizationDeniedHandler::class) val email:String) (3)
1 不正な結果値のマスクされた値を返す MethodAuthorizationDeniedHandler の実装を作成する
2EmailMaskingMethodAuthorizationDeniedHandler を Bean として登録する
3 メソッドに @HandleAuthorizationDenied をアノテーションし、EmailMaskingMethodAuthorizationDeniedHandler を handlerClass 属性に渡します。

そして、AccessDeniedException の代わりにマスクされたメールが返されることを確認できます。

元の拒否された値にアクセスできるため、それを正しく処理し、呼び出し元に返さないようにしてください。

  • Java

  • Kotlin

@Autowired
UserRepository users;

@Test
void getEmailWhenProxiedThenMaskedEmail() {
    Optional<User> securedUser = users.findByName("name");
    // email is [email protected] (英語)  
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com");
}
@Autowired
var users:UserRepository? = null

@Test
fun getEmailWhenProxiedThenMaskedEmail() {
    val securedUser: Optional<User> = users.findByName("name")
    // email is [email protected] (英語)  
    assertThat(securedUser.get().getEmail()).isEqualTo("use******@example.com")
}

MethodAuthorizationDeniedHandler を実装する場合、返すことができる型に関していくつかのオプションがあります。

  • null 値。

  • メソッドの戻り値の型を考慮する null 以外の値。

  • 例外(通常は AuthorizationDeniedException のインスタンス)をスローします。これがデフォルトの動作です。

  • リアクティブアプリケーション用の Mono 型。

ハンドラーはアプリケーションコンテキストに Bean として登録する必要があるため、より複雑なロジックが必要な場合は、ハンドラーに依存関係を注入できることに注意してください。さらに、認可決定に関連する詳細については、MethodInvocation または MethodInvocationResult、および AuthorizationResult を使用できます。

利用可能なパラメーターに基づいて何を返すかを決定する

異なるメソッドに複数のマスク値がある可能性があるシナリオを考えてみましょう。各メソッドのハンドラーを作成しなければならないとしたら、あまり生産的ではありませんが、そうすることはまったく問題ありません。このような場合、パラメーターを介して渡された情報を使用して、何をするかを決定できます。たとえば、カスタム @Mask アノテーションと、そのアノテーションを検出して返すマスク値を決定するハンドラーを作成できます。

  • Java

  • Kotlin

import org.springframework.core.annotation.AnnotationUtils;

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Mask {

    String value();

}

public class MaskAnnotationDeniedHandler implements MethodAuthorizationDeniedHandler {

    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
        return mask.value();
    }

}

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public MaskAnnotationDeniedHandler maskAnnotationDeniedHandler() {
        return new MaskAnnotationDeniedHandler();
    }

}

@Component
public class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("***")
    public String foo() {
        return "foo";
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
    @Mask("???")
    public String bar() {
        return "bar";
    }

}
import org.springframework.core.annotation.AnnotationUtils

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Mask(val value: String)

class MaskAnnotationDeniedHandler : MethodAuthorizationDeniedHandler {

    override fun handleDeniedInvocation(methodInvocation: MethodInvocation, authorizationResult: AuthorizationResult): Any {
        val mask = AnnotationUtils.getAnnotation(methodInvocation.method, Mask::class.java)
        return mask.value
    }

}

@Configuration
@EnableMethodSecurity
class SecurityConfig {

    @Bean
    fun maskAnnotationDeniedHandler(): MaskAnnotationDeniedHandler {
        return MaskAnnotationDeniedHandler()
    }

}

@Component
class MyService {

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("***")
    fun foo(): String {
        return "foo"
    }

    @PreAuthorize(value = "hasAuthority('user:read')")
    @HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
    @Mask("???")
    fun bar(): String {
        return "bar"
    }

}

アクセスが拒否された場合の戻り値は、@Mask アノテーションに基づいて決定されます。

  • Java

  • Kotlin

@Autowired
MyService myService;

@Test
void fooWhenDeniedThenReturnStars() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("***");
}

@Test
void barWhenDeniedThenReturnQuestionMarks() {
    String value = this.myService.foo();
    assertThat(value).isEqualTo("???");
}
@Autowired
var myService: MyService

@Test
fun fooWhenDeniedThenReturnStars() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("***")
}

@Test
fun barWhenDeniedThenReturnQuestionMarks() {
    val value: String = myService.foo()
    assertThat(value).isEqualTo("???")
}

メタアノテーションサポートとの組み合わせ

メソッド内のアノテーションを減らして簡素化するために、@HandleAuthorizationDenied を他のアノテーションと組み合わせることもできます。前のセクションの例を参考にして、@HandleAuthorizationDenied と @Mask をマージしてみましょう。

  • Java

  • Kotlin

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler.class)
public @interface Mask {

    String value();

}

@Mask("***")
public String myMethod() {
    // ...
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@HandleAuthorizationDenied(handlerClass = MaskAnnotationDeniedHandler::class)
annotation class Mask(val value: String)

@Mask("***")
fun myMethod(): String {
    // ...
}

これで、メソッドでマスク動作が必要な場合に、両方のアノテーションを追加することを覚えておく必要がなくなりました。使用方法の詳細については、メタアノテーションのサポートセクションを必ず参照してください。

@EnableGlobalMethodSecurity からの移行

@EnableGlobalMethodSecurity を使用している場合は、@EnableMethodSecurity に移行する必要があります。

グローバルメソッドセキュリティメソッドセキュリティに置き換える

@EnableGlobalMethodSecurity (Javadoc) <global-method-security> は非推奨となり、それぞれ @EnableMethodSecurity (Javadoc) <method-security> が推奨されます。新しいアノテーションと XML 要素は、デフォルトで Spring の事前投稿アノテーションを有効にし、内部で AuthorizationManager を使用します。

これは、次の 2 つのリストが関数に同等であることを意味します。

  • Java

  • Kotlin

  • XML

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>

および:

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>

プリポストアノテーションを使用しないアプリケーションの場合は、望ましくない動作がアクティブにならないように、必ずオフにしてください。

例: 次のようなリスト:

  • Java

  • Kotlin

  • XML

@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>

次のように変更する必要があります。

  • Java

  • Kotlin

  • XML

@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>

DefaultMethodSecurityExpressionHandler をサブクラス化する代わりにカスタム @Bean を使用する

パフォーマンスの最適化として、Authentication の代わりに Supplier<Authentication> を取る新しいメソッドが MethodSecurityExpressionHandler に導入されました。

これにより、Spring Security は Authentication のルックアップを延期でき、@EnableGlobalMethodSecurity の代わりに @EnableMethodSecurity を使用すると自動的に利用されます。

ただし、コードが DefaultMethodSecurityExpressionHandler を継承し、createSecurityExpressionRoot(Authentication, MethodInvocation) をオーバーライドしてカスタム SecurityExpressionRoot インスタンスを返すとします。これは、@EnableMethodSecurity がセットアップする配置が代わりに createEvaluationContext(Supplier<Authentication>, MethodInvocation) を呼び出すため、機能しなくなります。

幸いなことに、このようなレベルのカスタマイズは不要なことがよくあります。代わりに、必要な認証方法を使用してカスタム Bean を作成できます。

例: @PostAuthorize("hasAuthority('ADMIN')") のカスタム評価が必要だとしましょう。次のようなカスタム @Bean を作成できます。

  • Java

  • Kotlin

class MyAuthorizer {
	boolean isAdmin(MethodSecurityExpressionOperations root) {
		boolean decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}
class MyAuthorizer {
	fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
		val decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}

そして、次のようにアノテーションで参照します。

  • Java

  • Kotlin

@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")

DefaultMethodSecurityExpressionHandler をサブクラス化したい

DefaultMethodSecurityExpressionHandler のサブクラス化を継続する必要がある場合でも、そうすることができます。代わりに、次のように createEvaluationContext(Supplier<Authentication>, MethodInvocation) メソッドをオーバーライドします。

  • Java

  • Kotlin

@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    @Override
    public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
		StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
        MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
        MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate);
        context.setRootObject(root);
        return context;
    }
}
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
    override fun createEvaluationContext(val authentication: Supplier<Authentication>,
        val mi: MethodInvocation): EvaluationContext {
		val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext
        val delegate = context.getRootObject().getValue() as MethodSecurityExpressionOperations
        val root = MySecurityExpressionRoot(delegate)
        context.setRootObject(root)
        return context
    }
}

参考文献

アプリケーションのリクエストを保護しました。まだ保護していない場合は、リクエストを保護してください。アプリケーションのテストや、Spring Security とデータレイヤートレース、メトリクスなどのアプリケーションの他の側面との統合についてさらに詳しく読むこともできます。