最新の安定バージョンについては、Spring Security 6.5.3 を使用してください! |
メソッドのセキュリティ
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 はAuthenticationManagerBeforeMethodInterceptor#preAuthorize
(Javadoc) を使用し、その後PreAuthorizeAuthorizationManager
(Javadoc) を使用します。@PostAuthorize
の場合、Spring Security はAuthenticationManagerAfterMethodInterceptor#postAuthorize
(Javadoc) を使用し、その後PostAuthorizeAuthorizationManager
(Javadoc) を使用します。@PreFilter
の場合、Spring Security はPreFilterAuthorizationMethodInterceptor
(Javadoc) を使用します@PostFilter
の場合、Spring Security はPostFilterAuthorizationMethodInterceptor
(Javadoc) を使用します@Secured
の場合、Spring Security はAuthenticationManagerBeforeMethodInterceptor#secured
(Javadoc) を使用し、その後SecuredAuthorizationManager
(Javadoc) を使用します。JSR-250 アノテーションの場合、Spring Security は
AuthenticationManagerBeforeMethodInterceptor#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 new RoleHierarchyImpl("ROLE_ADMIN > permission:read");
}
companion object {
@Bean
fun roleHierarchy(): RoleHierarchy {
return RoleHierarchyImpl("ROLE_ADMIN > permission:read")
}
}
<bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
<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
}
}
これにより、メソッド定義がより読みやすくなります。
特定のアノテーションの有効化
@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 言語にもアクセスできます。
カスタム認証マネージャーの使用
メソッドをプログラム的に承認する 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
アプローチを使用する必要があります。
@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 とデータレイヤー、トレース、メトリクスなどのアプリケーションの他の側面との統合についてさらに詳しく読むこともできます。