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ベースの認可と組み合わせることで、多層的で堅牢なセキュリティを実現できます。

参考リンク