Spring Securityのメソッドセキュリティは、コントローラーやサービス層のメソッド単位で細かいアクセス制御を実現する強力な機能です。本記事では、@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilterアノテーションの使い方と、SpEL(Spring Expression Language)による柔軟な認可条件の記述方法を解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| Spring Security |
6.4.x |
| ビルドツール |
Maven または Gradle |
| IDE |
VS Code または IntelliJ IDEA |
事前に以下の知識があると理解がスムーズです。
- Spring Securityの基本概念(認証・認可の違い)
- SecurityFilterChainの基本設定
- Spring AOPの基礎知識
メソッドセキュリティとは#
メソッドセキュリティは、URLパターンベースの認可に加えて、サービス層やリポジトリ層のメソッド単位でアクセス制御を行う仕組みです。以下のような場面で特に有効です。
| ユースケース |
説明 |
| 細粒度のアクセス制御 |
メソッドの引数や戻り値に基づいた認可判定 |
| ビジネスロジックの保護 |
サービス層での認可により、コントローラーをバイパスされても安全 |
| リソース所有者の検証 |
「自分のデータのみ操作可能」といったルールの実装 |
| コレクションのフィルタリング |
権限に応じて戻り値のリストから不正なデータを除外 |
URLベース認可との比較#
| 観点 |
URLベース認可 |
メソッドセキュリティ |
| 認可の粒度 |
粗い(エンドポイント単位) |
細かい(メソッド単位) |
| 設定場所 |
設定クラスに集中 |
メソッド宣言に分散(ローカル) |
| 設定方法 |
DSL |
アノテーション |
| 認可定義 |
プログラム的 |
SpEL |
| パラメータ利用 |
困難 |
容易 |
両者は排他的ではなく、組み合わせて使用することが推奨されます。URLベースで大まかな認可を行い、メソッドセキュリティで細かい制御を追加する構成が一般的です。
@EnableMethodSecurityの有効化#
メソッドセキュリティを使用するには、設定クラスに@EnableMethodSecurityアノテーションを付与します。
1
2
3
4
5
6
7
8
|
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// メソッドセキュリティが有効化される
}
|
@EnableMethodSecurityはデフォルトで以下のアノテーションを有効化します。
| アノテーション |
デフォルト |
説明 |
@PreAuthorize |
有効 |
メソッド実行前の認可チェック |
@PostAuthorize |
有効 |
メソッド実行後の認可チェック |
@PreFilter |
有効 |
引数コレクションのフィルタリング |
@PostFilter |
有効 |
戻り値コレクションのフィルタリング |
@Secured |
無効 |
シンプルなロールベース認可 |
JSR-250(@RolesAllowed等) |
無効 |
標準アノテーション |
オプションの有効化#
@SecuredやJSR-250アノテーションを使用する場合は、明示的に有効化します。
1
2
3
4
|
@Configuration
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig {
}
|
@PreAuthorizeによるメソッド実行前の認可#
@PreAuthorizeは、メソッド実行前に認可条件をチェックします。条件を満たさない場合、AccessDeniedExceptionがスローされ、メソッドは実行されません。
基本的な使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// ROLE_ADMIN権限を持つユーザーのみ実行可能
userRepository.deleteById(userId);
}
@PreAuthorize("hasAuthority('USER_READ')")
public User getUser(Long userId) {
// USER_READ権限を持つユーザーのみ実行可能
return userRepository.findById(userId).orElseThrow();
}
@PreAuthorize("isAuthenticated()")
public User getCurrentUser() {
// 認証済みユーザーのみ実行可能
return getCurrentAuthenticatedUser();
}
}
|
メソッド引数を利用した認可#
SpELを使用して、メソッドの引数にアクセスできます。引数は#引数名で参照します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Service
public class AccountService {
@PreAuthorize("#username == authentication.name")
public Account getAccount(String username) {
// 自分のアカウント情報のみ取得可能
return accountRepository.findByUsername(username).orElseThrow();
}
@PreAuthorize("#account.owner == authentication.name or hasRole('ADMIN')")
public void updateAccount(Account account) {
// 自分のアカウント、または管理者のみ更新可能
accountRepository.save(account);
}
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void changePassword(Long userId, String newPassword) {
// 管理者または自分自身のパスワードのみ変更可能
userRepository.updatePassword(userId, newPassword);
}
}
|
複合条件の記述#
SpELの論理演算子を使用して、複雑な条件を記述できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Service
public class DocumentService {
@PreAuthorize("hasRole('ADMIN') and hasAuthority('DOCUMENT_DELETE')")
public void deleteDocument(Long documentId) {
// ROLE_ADMINかつDOCUMENT_DELETE権限が必要
documentRepository.deleteById(documentId);
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER') or @documentSecurity.isOwner(#documentId)")
public Document getDocument(Long documentId) {
// 管理者、マネージャー、またはドキュメントの所有者のみアクセス可能
return documentRepository.findById(documentId).orElseThrow();
}
@PreAuthorize("hasAuthority('DOCUMENT_CREATE') and #document.status == 'DRAFT'")
public void publishDocument(Document document) {
// DOCUMENT_CREATE権限を持ち、ドキュメントがDRAFT状態の場合のみ実行可能
document.setStatus("PUBLISHED");
documentRepository.save(document);
}
}
|
@PostAuthorizeによるメソッド実行後の認可#
@PostAuthorizeは、メソッド実行後に戻り値に基づいて認可チェックを行います。戻り値はreturnObjectで参照できます。
基本的な使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service
public class OrderService {
@PostAuthorize("returnObject.customer.username == authentication.name or hasRole('ADMIN')")
public Order getOrder(Long orderId) {
// メソッド実行後、戻り値の所有者チェック
return orderRepository.findById(orderId).orElseThrow();
}
@PostAuthorize("returnObject.status != 'CONFIDENTIAL' or hasAuthority('VIEW_CONFIDENTIAL')")
public Report getReport(Long reportId) {
// 機密レポートはVIEW_CONFIDENTIAL権限が必要
return reportRepository.findById(reportId).orElseThrow();
}
}
|
@PostAuthorizeの注意点#
@PostAuthorizeはメソッド実行後にチェックを行うため、以下の点に注意が必要です。
| 注意点 |
説明 |
| データベース書き込み |
書き込み操作には使用しない(チェック前に変更が確定する可能性) |
| トランザクション |
@Transactionalと併用する場合、トランザクション順序に注意 |
| パフォーマンス |
不要なメソッド実行が発生する可能性 |
書き込み操作の場合は、先に@PostAuthorizeを使用してデータを読み取り、その後書き込み操作を行うパターンが推奨されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service
public class AccountService {
@PostAuthorize("returnObject.owner == authentication.name")
public Account readAccount(Long id) {
return accountRepository.findById(id).orElseThrow();
}
@PreAuthorize("hasRole('USER')")
public void updateAccount(Long id, AccountUpdateRequest request) {
Account account = readAccount(id); // ここで所有者チェック
account.update(request);
accountRepository.save(account);
}
}
|
@PreFilterによる引数コレクションのフィルタリング#
@PreFilterは、メソッドに渡されるコレクション引数から、条件を満たさない要素を除外します。各要素はfilterObjectで参照できます。
基本的な使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Service
public class BatchService {
@PreFilter("filterObject.owner == authentication.name")
public List<Account> updateAccounts(List<Account> accounts) {
// 自分が所有するアカウントのみがaccountsに残る
return accounts.stream()
.map(accountRepository::save)
.toList();
}
@PreFilter("filterObject.status == 'PENDING'")
public void processOrders(List<Order> orders) {
// PENDING状態の注文のみ処理される
orders.forEach(orderProcessor::process);
}
}
|
対応するコレクション型#
@PreFilterは以下のコレクション型をサポートしています。
| 型 |
filterObjectの参照 |
| 配列 |
各要素 |
| Collection |
各要素 |
| Map |
エントリの値(filterObject.valueでアクセス) |
| Stream |
各要素(ストリームがオープンの場合のみ) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Service
public class DocumentService {
@PreFilter("filterObject.value.owner == authentication.name")
public Map<String, Document> updateDocuments(Map<String, Document> documents) {
// Mapの場合はfilterObject.valueで値にアクセス
documents.values().forEach(documentRepository::save);
return documents;
}
@PreFilter("filterObject.owner == authentication.name")
public void processDocuments(Document[] documents) {
// 配列もサポート
Arrays.stream(documents).forEach(documentRepository::save);
}
}
|
@PostFilterによる戻り値コレクションのフィルタリング#
@PostFilterは、メソッドの戻り値コレクションから、条件を満たさない要素を除外します。
基本的な使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service
public class ProjectService {
@PostFilter("filterObject.members.contains(authentication.name) or hasRole('ADMIN')")
public List<Project> getAllProjects() {
// 自分がメンバーのプロジェクト、または管理者は全プロジェクトを取得
return projectRepository.findAll();
}
@PostFilter("filterObject.visibility == 'PUBLIC' or filterObject.owner == authentication.name")
public List<Document> searchDocuments(String keyword) {
// 公開ドキュメントまたは自分のドキュメントのみ返却
return documentRepository.searchByKeyword(keyword);
}
}
|
パフォーマンス上の注意#
@PostFilterはインメモリでフィルタリングを行うため、大量データの場合はパフォーマンスに影響します。可能であれば、データ層(SQL)でフィルタリングすることを推奨します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Service
public class OptimizedProjectService {
// 推奨:データ層でフィルタリング
public List<Project> getMyProjects() {
String username = SecurityContextHolder.getContext()
.getAuthentication().getName();
return projectRepository.findByMembersContaining(username);
}
// 非推奨:大量データのインメモリフィルタリング
@PostFilter("filterObject.members.contains(authentication.name)")
public List<Project> getAllProjectsFiltered() {
return projectRepository.findAll();
}
}
|
@Securedによるシンプルなロールベース認可#
@Securedは、SpELを使用しないシンプルなロールベースの認可を提供します。@PreAuthorizeの前身であり、現在は@PreAuthorizeの使用が推奨されています。
有効化と基本的な使用例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
}
@Service
public class AdminService {
@Secured("ROLE_ADMIN")
public void adminOnlyOperation() {
// ROLE_ADMIN権限が必要
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void managementOperation() {
// ROLE_ADMINまたはROLE_MANAGERが必要(OR条件)
}
}
|
@Securedと@PreAuthorizeの比較#
| 観点 |
@Secured |
@PreAuthorize |
| SpELサポート |
なし |
あり |
| メソッド引数アクセス |
不可 |
可能 |
| 論理演算 |
OR条件のみ |
AND/OR/NOT |
| 柔軟性 |
低い |
高い |
新規開発では@PreAuthorizeの使用を推奨します。
SpELによる高度な認可表現#
Spring Expression Language(SpEL)を使用することで、柔軟な認可条件を記述できます。
利用可能なフィールドとメソッド#
| フィールド/メソッド |
説明 |
authentication |
現在のAuthenticationオブジェクト |
principal |
Authentication#getPrincipalの値 |
permitAll |
常に許可 |
denyAll |
常に拒否 |
hasRole('ROLE') |
指定ロールを持つか(ROLE_プレフィックス自動付与) |
hasAnyRole('R1', 'R2') |
いずれかのロールを持つか |
hasAuthority('AUTH') |
指定権限を持つか |
hasAnyAuthority('A1', 'A2') |
いずれかの権限を持つか |
isAuthenticated() |
認証済みか |
isAnonymous() |
匿名ユーザーか |
isRememberMe() |
Remember-Me認証か |
isFullyAuthenticated() |
完全認証か(Remember-Meでない) |
実践的なSpEL例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Service
public class AdvancedSecurityService {
// 認証情報の詳細にアクセス
@PreAuthorize("authentication.name == #username")
public UserProfile getProfile(String username) {
return userProfileRepository.findByUsername(username).orElseThrow();
}
// JWTクレームへのアクセス(OAuth2リソースサーバーの場合)
@PreAuthorize("principal.claims['tenant_id'] == #tenantId")
public List<Resource> getTenantResources(String tenantId) {
return resourceRepository.findByTenantId(tenantId);
}
// 複数条件の組み合わせ
@PreAuthorize("""
hasRole('ADMIN') or
(hasRole('USER') and #request.amount <= 10000) or
(hasRole('MANAGER') and #request.amount <= 100000)
""")
public void processPayment(PaymentRequest request) {
paymentProcessor.process(request);
}
// 時間ベースの制限
@PreAuthorize("T(java.time.LocalTime).now().isBefore(T(java.time.LocalTime).of(18, 0))")
public void businessHoursOnlyOperation() {
// 18時前のみ実行可能
}
}
|
カスタムBeanを使用した認可ロジック#
複雑な認可ロジックは、カスタムBeanに委譲することで可読性と再利用性を向上できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@Component("authz")
public class AuthorizationLogic {
private final ProjectRepository projectRepository;
public AuthorizationLogic(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
public boolean isProjectMember(Long projectId, Authentication authentication) {
return projectRepository.findById(projectId)
.map(project -> project.getMembers().contains(authentication.getName()))
.orElse(false);
}
public boolean canAccessDocument(Long documentId, Authentication authentication) {
// 複雑な認可ロジック
return checkOwnership(documentId, authentication) ||
checkTeamMembership(documentId, authentication) ||
checkPublicAccess(documentId);
}
public boolean hasMinimumLevel(Authentication authentication, int requiredLevel) {
// ユーザーのレベルベースのチェック
return getUserLevel(authentication) >= requiredLevel;
}
}
@Service
public class ProjectService {
@PreAuthorize("@authz.isProjectMember(#projectId, authentication)")
public Project getProject(Long projectId) {
return projectRepository.findById(projectId).orElseThrow();
}
@PreAuthorize("@authz.canAccessDocument(#documentId, authentication)")
public Document getDocument(Long documentId) {
return documentRepository.findById(documentId).orElseThrow();
}
@PreAuthorize("@authz.hasMinimumLevel(authentication, 5)")
public void advancedOperation() {
// レベル5以上のユーザーのみ実行可能
}
}
|
メタアノテーションによる再利用#
頻繁に使用する認可条件は、メタアノテーションとして定義することで再利用性を高められます。
基本的なメタアノテーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.security.access.prepost.PreAuthorize;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public @interface IsManagement {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.owner == authentication.name")
public @interface RequireOwnership {
}
|
使用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Service
public class AdminService {
@IsAdmin
public void adminOperation() {
// @PreAuthorize("hasRole('ADMIN')")と同等
}
@IsManagement
public void managementOperation() {
// @PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")と同等
}
@RequireOwnership
public Account getAccount(Long id) {
return accountRepository.findById(id).orElseThrow();
}
}
|
テンプレート式を使用したメタアノテーション#
Spring Security 6.4以降では、テンプレート式を使用してパラメータ化されたメタアノテーションを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
}
// パラメータ化されたメタアノテーション
@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("hasAuthority('{value}')")
public @interface HasPermission {
String value();
}
@Service
public class FlexibleService {
@HasRole("ADMIN")
public void adminTask() {
// hasRole('ADMIN')と評価される
}
@HasPermission("document:write")
public void writeDocument() {
// hasAuthority('document:write')と評価される
}
}
|
クラスレベルでのアノテーション適用#
メソッドセキュリティアノテーションは、クラスレベルでも適用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Service
@PreAuthorize("hasRole('USER')")
public class UserScopedService {
// クラスレベルの@PreAuthorizeが適用される
public void operation1() {
}
public void operation2() {
}
// メソッドレベルのアノテーションで上書き
@PreAuthorize("hasRole('ADMIN')")
public void adminOperation() {
}
}
|
RoleHierarchyとの連携#
RoleHierarchyを使用することで、ロールの階層関係を定義し、SpEL式を簡潔にできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
ROLE_USER > ROLE_GUEST
""");
}
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(
RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
}
@Service
public class HierarchicalService {
@PreAuthorize("hasRole('USER')")
public void userOperation() {
// ROLE_USER, ROLE_MANAGER, ROLE_ADMINがアクセス可能
}
@PreAuthorize("hasRole('MANAGER')")
public void managerOperation() {
// ROLE_MANAGER, ROLE_ADMINがアクセス可能
}
}
|
テスト方法#
メソッドセキュリティのテストには、@WithMockUserや@WithUserDetailsアノテーションを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void deleteUser_withAdminRole_succeeds() {
// ROLE_ADMINを持つユーザーとしてテスト
assertThatCode(() -> userService.deleteUser(1L))
.doesNotThrowAnyException();
}
@Test
@WithMockUser(roles = "USER")
void deleteUser_withUserRole_throwsAccessDenied() {
// ROLE_USERでは権限不足
assertThatThrownBy(() -> userService.deleteUser(1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(username = "owner")
void getAccount_withOwner_succeeds() {
// アカウント所有者としてテスト
Account account = userService.getAccount("owner");
assertThat(account).isNotNull();
}
@Test
@WithMockUser(username = "other")
void getAccount_withNonOwner_throwsAccessDenied() {
// 非所有者としてテスト
assertThatThrownBy(() -> userService.getAccount("owner"))
.isInstanceOf(AccessDeniedException.class);
}
}
|
メソッドセキュリティの処理フロー#
メソッドセキュリティの内部動作を理解することで、効果的な設計が可能になります。
sequenceDiagram
participant Client
participant Proxy as Spring AOP Proxy
participant PreAuth as PreAuthorizeInterceptor
participant Service as Service Method
participant PostAuth as PostAuthorizeInterceptor
Client->>Proxy: メソッド呼び出し
Proxy->>PreAuth: @PreAuthorizeチェック
alt 認可成功
PreAuth->>Service: メソッド実行
Service-->>PostAuth: 戻り値
PostAuth->>PostAuth: @PostAuthorizeチェック
alt 認可成功
PostAuth-->>Client: 結果返却
else 認可失敗
PostAuth-->>Client: AccessDeniedException
end
else 認可失敗
PreAuth-->>Client: AccessDeniedException
endインターセプターの実行順序#
メソッドセキュリティの各アノテーションは、それぞれ専用のインターセプターによって処理されます。
| アノテーション |
実行順序 |
インターセプター |
@PreFilter |
100 |
PreFilterAuthorizationMethodInterceptor |
@PreAuthorize |
200 |
AuthorizationManagerBeforeMethodInterceptor |
@PostAuthorize |
500 |
AuthorizationManagerAfterMethodInterceptor |
@PostFilter |
600 |
PostFilterAuthorizationMethodInterceptor |
実践的なユースケース#
マルチテナントアプリケーションでの認可#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Service
public class TenantAwareService {
@PreAuthorize("@tenantAuthz.belongsToTenant(#tenantId, authentication)")
public List<Resource> getResources(String tenantId) {
return resourceRepository.findByTenantId(tenantId);
}
@PreAuthorize("""
@tenantAuthz.belongsToTenant(#resource.tenantId, authentication) and
hasAuthority('RESOURCE_WRITE')
""")
public Resource createResource(Resource resource) {
return resourceRepository.save(resource);
}
}
@Component("tenantAuthz")
public class TenantAuthorization {
public boolean belongsToTenant(String tenantId, Authentication authentication) {
if (authentication.getPrincipal() instanceof TenantUser tenantUser) {
return tenantUser.getTenantId().equals(tenantId);
}
return false;
}
}
|
承認ワークフローでの段階的認可#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Service
public class ApprovalService {
@PreAuthorize("hasRole('USER') and @workflow.canSubmit(#request, authentication)")
public Request submitRequest(Request request) {
request.setStatus(RequestStatus.PENDING);
return requestRepository.save(request);
}
@PreAuthorize("hasRole('MANAGER') and @workflow.canApprove(#requestId, authentication)")
public Request approveRequest(Long requestId) {
Request request = requestRepository.findById(requestId).orElseThrow();
request.setStatus(RequestStatus.APPROVED);
return requestRepository.save(request);
}
@PreAuthorize("hasRole('ADMIN') or @workflow.isRequester(#requestId, authentication)")
public Request cancelRequest(Long requestId) {
Request request = requestRepository.findById(requestId).orElseThrow();
request.setStatus(RequestStatus.CANCELLED);
return requestRepository.save(request);
}
}
|
まとめ#
Spring Securityのメソッドセキュリティを活用することで、以下のメリットを得られます。
| メリット |
説明 |
| 細粒度のアクセス制御 |
メソッド単位、さらには引数・戻り値レベルでの認可 |
| 宣言的なセキュリティ |
ビジネスロジックと認可ロジックの分離 |
| 柔軟なSpEL |
複雑な認可条件を簡潔に記述 |
| 再利用性 |
メタアノテーションによる認可ルールの共通化 |
| テスト容易性 |
@WithMockUserによる容易なテスト |
URLベースの認可と組み合わせることで、多層的で堅牢なセキュリティを実現できます。
参考リンク#