メソッドのセキュリティ
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 のサポート、カスタムアノテーション、およびいくつかの構成ポイントを含む、他の多くのユースケースもサポートしています。次の使用例について学習することを検討してください。
メソッドセキュリティの仕組みとそれを使用する理由を理解する
@PreAuthorize
および@PostAuthorize
を使用した認証方法@PreFilter
および@PostFilter
によるフィルタリング方法JSR-250 アノテーションによる認証メソッド
AspectJ 式による認証メソッド
SpEL 式の処理のカスタマイズ
カスタム認証システムとの統合
メソッドセキュリティの仕組み
Spring Security のメソッド認証サポートは、次の場合に便利です。
詳細な認可ロジックを抽出します。たとえば、メソッドのパラメーターと戻り値が認可の決定にコントリビュートする場合です。
サービス層でのセキュリティの強化
スタイル的には、
HttpSecurity
ベースの構成よりもアノテーションベースの構成を好む
また、Method Security は Spring AOP を使用して構築されているため、その表現力をすべて利用して、必要に応じて Spring Security のデフォルトをオーバーライドできます。
すでに記述されていたように、Spring XML 構成ファイル内の @Configuration
クラスまたは <sec:method-security/>
に @EnableMethodSecurity
を追加することから始めます。
このアノテーションと XML 要素は、それぞれ
|
メソッド認可は、メソッド認可前とメソッド認可後を組み合わせたものです。次の方法でアノテーションが付けられたサービス 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
への特定の呼び出しは次のようになります。
Spring AOP は、
readCustomer
のプロキシメソッドを呼び出します。プロキシの他のアドバイザーのうち、@PreAuthorize
ポイントカットと一致するAuthorizationManagerBeforeMethodInterceptor
(Javadoc) を呼び出します。インターセプターは
PreAuthorizeAuthorizationManager#check
(Javadoc) を呼び出します認可マネージャーは
MethodSecurityExpressionHandler
を使用してアノテーションの SpEL 式を解析し、Supplier<Authentication>
とMethodInvocation
を含むMethodSecurityExpressionRoot
から対応するEvaluationContext
を構築します。インターセプターはこのコンテキストを使用して式を評価します。具体的には、
Supplier
からAuthentication
を読み取り、権限のコレクションにpermission:read
が含まれているかどうかを確認します。評価に合格すると、Spring AOP はメソッドの呼び出しを開始します。
そうでない場合、インターセプターは
AuthorizationDeniedEvent
を発行し、AccessDeniedException
(Javadoc) をスローします。ExceptionTranslationFilter
はこれをキャッチし、レスポンスに 403 ステータスコードを返します。メソッドが戻った後、Spring AOP は
@PostAuthorize
pointcut に一致するAuthorizationManagerAfterMethodInterceptor
(Javadoc) を呼び出し、上記と同じように動作しますが、PostAuthorizeAuthorizationManager
(Javadoc) を使用します。評価が合格した場合 (この場合、戻り値はログインしているユーザーのもの)、処理は通常どおり続行されます。
そうでない場合、インターセプターは
AuthorizationDeniedEvent
をパブリッシュし、AccessDeniedException
(Javadoc) をスローします。これをExceptionTranslationFilter
がキャッチして、レスポンスに 403 ステータスコードを返します。
メソッドが HTTP リクエストのコンテキストで呼び出されない場合は、AccessDeniedException を自分で処理する必要がある可能性があります。 |
複数のアノテーションが連続して計算される
上で示したように、メソッド呼び出しに複数のメソッドセキュリティアノテーションが含まれる場合、それらのそれぞれは一度に 1 つずつ処理されます。これは、それらがまとめて "anded" されていると考えることができることを意味します。つまり、呼び出しが認可されるには、すべてのアノテーションインスペクションが認可に合格する必要があります。
繰り返しのアノテーションはサポートされていません
ただし、同じメソッドで同じアノテーションを繰り返すことはサポートされていません。例: 同じメソッドに @PreAuthorize
を 2 回配置することはできません。
代わりに、SpEL のブール値サポート、または別の Bean への委譲のサポートを使用してください。
各アノテーションには独自のポイントカットがあります
各アノテーションには独自のポイントカットインスタンスがあり、メソッドとそれを囲むクラスから開始して、オブジェクト階層全体にわたってそのアノテーションまたは対応するメタアノテーションを検索します。
詳細は AuthorizationMethodPointcuts
(Javadoc) で確認できます。
各アノテーションには独自のメソッドインターセプタがあります
各アノテーションには、独自の専用メソッドインターセプターがあります。その理由は、物事をより構成しやすくするためです。例: 必要に応じて、Spring Security デフォルトを無効にして、@PostAuthorize
メソッドインターセプターのみを公開できます。
メソッドインターセプタは次のとおりです。
@PreAuthorize
の場合、Spring Security はAuthorizationManagerBeforeMethodInterceptor#preAuthorize
(Javadoc) を使用し、その後PreAuthorizeAuthorizationManager
(Javadoc) を使用します。@PostAuthorize
の場合、Spring Security はAuthorizationManagerBeforeMethodInterceptor#postAuthorize
(Javadoc) を使用し、その後PostAuthorizeAuthorizationManager
(Javadoc) を使用します。@PreFilter
の場合、Spring Security はPreFilterAuthorizationMethodInterceptor
(Javadoc) を使用します@PostFilter
の場合、Spring Security はPostFilterAuthorizationMethodInterceptor
(Javadoc) を使用します@Secured
の場合、Spring Security はAuthorizationManagerBeforeMethodInterceptor#secured
(Javadoc) を使用し、その後SecuredAuthorizationManager
(Javadoc) を使用します。JSR-250 アノテーションの場合、Spring Security は
AuthorizationManagerBeforeMethodInterceptor#jsr250
(Javadoc) を使用し、その後Jsr250AuthorizationManager
(Javadoc) を使用します。
一般に、次のリストは、@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')")
@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 は、Kotlin 固有のデータ型ではまだサポートされていません。そのため、Java スニペットのみが表示されます。 |
メソッドセキュリティがアクティブな場合、次のようにメソッドに @PreFilter
(Javadoc) アノテーションを付けることができます。
Java
@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;
}
}
これは、式 filterObject.owner == authentication.name
が失敗する accounts
からの値をフィルターで除外することを目的としています。filterObject
は accounts
内の各 account
を表し、各 account
をテストするために使用されます。
次に、次の方法でクラスをテストして、認可ルールが適用されていることを確認できます。
Java
@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);
}
@PreFilter はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。 |
@PreFilter
は、配列、コレクション、マップ、ストリーム (ストリームが開いている限り) をサポートします。
例: 上記の updateAccounts
宣言は、次の他の 4 つの宣言と同じように機能します。
Java
@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)
その結果、上記のメソッドには、owner
属性がログインユーザーの name
と一致する Account
インスタンスのみが含まれることになります。
@PostFilter
によるフィルタリングメソッドの結果
@PostFilter は、Kotlin 固有のデータ型ではまだサポートされていません。そのため、Java スニペットのみが表示されます。 |
メソッドセキュリティがアクティブな場合、次のようにメソッドに @PostFilter
(Javadoc) アノテーションを付けることができます。
Java
@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;
}
}
これは、式 filterObject.owner == authentication.name
が失敗した場合に戻り値から値をフィルターで除外することを目的としています。filterObject
は accounts
内の各 account
を表し、各 account
をテストするために使用されます。
次に、次のようにクラスをテストして、認可ルールが適用されていることを確認できます。
Java
@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");
}
@PostFilter はメタアノテーションにすることもでき、クラスまたはインターフェースレベルで定義し、SpEL 認証式を使用することもできます。 |
@PostFilter
は、配列、コレクション、マップ、ストリーム (ストリームが開いている限り) をサポートします。
例: 上記の readAccounts
宣言は、次の他の 3 つの宣言と同じように機能します。
@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)
その結果、上記のメソッドは、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 PrePostTemplateDefaults prePostTemplateDefaults() {
return new PrePostTemplateDefaults();
}
companion object {
@Bean
fun prePostTemplateDefaults(): PrePostTemplateDefaults {
return PrePostTemplateDefaults()
}
}
これで、@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
などの特定のアノテーションのみを有効にしたい場合もあります。
これは次の方法で行うことができます。
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
の動作を次のように置き換えることができます。
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>
|
式処理のカスタマイズ
あるいは、3 番目に、各 SpEL 式の処理方法をカスタマイズできます。これを行うには、次のようにカスタム MethodSecurityExpressionHandler
(Javadoc) を公開します。
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>
|
DefaultMessageSecurityExpressionHandler
をサブクラス化して、デフォルト以外の独自のカスタム認証式を追加することもできます。
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 を返す場合にのみ呼び出すことができます。 |
上記の |
メソッドパラメーターの使用
さらに、Spring Security はメソッドパラメーターを検出するメカニズムを提供するため、SpEL 式でもメソッドパラメーターにアクセスできます。
完全な参照として、Spring Security は DefaultSecurityParameterNameDiscoverer
を使用してパラメーター名を検出します。デフォルトでは、メソッドに対して次のオプションが試行されます。
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 属性をサポートするようにカスタマイズできます。メソッドの少なくとも 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 属性をサポートするようにカスタマイズできます。-parameters
引数を使用してコードをコンパイルする場合、標準の JDK リフレクション API を使用してパラメーター名が検出されます。これはクラスとインターフェースの両方で機能します。最後に、デバッグシンボルを使用してコードをコンパイルすると、パラメーター名はデバッグシンボルを使用して検出されます。インターフェースにはパラメーター名に関するデバッグ情報がないため、これは機能しません。インターフェースの場合は、アノテーションまたは
-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 が String
、Integer
、その他の型を含むすべての戻りオブジェクトをプロキシしようとすることを意味することに注意してください。これは多くの場合、望ましい動作ではありません。
メソッドが int
、String
、Double
やそれらの型のコレクションなどの値型を返すクラスまたはインターフェースで @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())
}
独自の |
プログラムによるプロキシ
特定のオブジェクトをプログラムでプロキシすることもできます。
これを実現するには、提供されている 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)
}
この機能はまだ Spring AOT をサポートしていません |
コレクションのプロキシ
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
アドバイザーを公開するため、通常は機能をアクティブ化するために何もする必要はありません。
|
カスタムアドバイス
適用したいセキュリティアドバイスがある場合は、次のように独自の 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)
}
}
ただし、Jackson を使用している場合は、次のような直列化エラーが発生する可能性があります。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 循環につながる直接的な自己参照
これは、Jackson が CGLIB プロキシと連携する方法によるものです。これに対処するには、User
クラスの先頭に次のアノテーションを追加します。
Java
Kotlin
@JsonSerialize(as = User.class)
public class User {
}
@JsonSerialize(`as` = User::class)
class User
最後に、各フィールドに対してスローされた AccessDeniedException
をキャッチするためのカスタムインターセプターを公開する必要があります。これは次のように実行できます。
Java
Kotlin
@Component
public class AccessDeniedExceptionInterceptor implements AuthorizationAdvisor {
private final AuthorizationAdvisor advisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
return invocation.proceed();
} catch (AccessDeniedException ex) {
return null;
}
}
@Override
public Pointcut getPointcut() {
return this.advisor.getPointcut();
}
@Override
public Advice getAdvice() {
return this;
}
@Override
public int getOrder() {
return this.advisor.getOrder() - 1;
}
}
@Component
class AccessDeniedExceptionInterceptor: AuthorizationAdvisor {
var advisor: AuthorizationAdvisor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
@Throws(Throwable::class)
fun invoke(invocation: MethodInvocation): Any? {
return try {
invocation.proceed()
} catch (ex:AccessDeniedException) {
null
}
}
val pointcut: Pointcut
get() = advisor.getPointcut()
val advice: Advice
get() = this
val order: Int
get() = advisor.getOrder() - 1
}
次に、ユーザーの認証レベルに基づいて異なる JSON 直列化が表示されます。user:read
権限を持っていない場合は、次のように表示されます。
{
"name" : "name",
"email" : null
}
そしてもし彼らにその権限があれば、次のことがわかるでしょう:
{
"name" : "name",
"email" : "email"
}
また、権限のないユーザーに JSON キーを公開したくない場合は、Spring Boot プロパティ |
認可が拒否された場合のフォールバック値の提供
必要な権限なしでメソッドが呼び出されたときに、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)
1 | null 値を返す MethodAuthorizationDeniedHandler の実装を作成する |
2 | NullMethodAuthorizationDeniedHandler を Bean として登録する |
3 | メソッドに @HandleAuthorizationDenied をアノテーションし、NullMethodAuthorizationDeniedHandler を handlerClass 属性に渡します。 |
そして、AccessDeniedException
ではなく null
値が返されることを確認できます。
|
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 の実装を作成する |
2 | EmailMaskingMethodAuthorizationDeniedHandler を 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 とデータレイヤー、トレース、メトリクスなどのアプリケーションの他の側面との統合についてさらに詳しく読むこともできます。