Spring Security: Method-Level Security

Phenny MwaisakaPhenny Mwaisaka
3 min read

Introduction

Spring Security provides robust mechanisms for securing applications at both the web request level and the method level. While request-level security handles URL-based access control, method-level security offers finer-grained control over individual methods in your application. This article explores the key annotations and techniques for implementing method-level security in Spring applications.

Enabling Method-Level Security

To use method-level security in your Spring application, you must first enable it in your security configuration:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    // Other security configurations
}

The @EnableMethodSecurity annotation activates Spring's method-level security features. This annotation replaces the older @EnableGlobalMethodSecurity (deprecated as of Spring Security 6.0).

Method Security Annotations

1. @Secured Annotation

The @Secured annotation is the simplest way to secure methods based on user roles.

@Secured("ROLE_ADMIN")
public void deleteUser(int userId) {
    // Method logic here
}

Key points about @Secured:

  • Only checks role-based authorization

  • Roles must be prefixed with "ROLE_"

  • Supports multiple roles (e.g., @Secured({"ROLE_ADMIN", "ROLE_SUPERADMIN"}))

  • Does not support SpEL expressions

2. @PreAuthorize Annotation

The @PreAuthorize annotation offers more flexibility by using Spring Expression Language (SpEL) to define access conditions.

@PreAuthorize("hasRole('ROLE_ADMIN') or (hasRole('ROLE_USER') and #userId == authentication.principal.id)")
public void updateUser(int userId) {
    // Method logic here
}

Key features of @PreAuthorize:

  • Evaluates before method execution

  • Supports complex expressions using SpEL

  • Can access method parameters (using #parameterName syntax)

  • Common expressions include:

    • hasRole('ROLE_NAME') or hasAuthority('AUTHORITY')

    • hasAnyRole('ROLE1', 'ROLE2')

    • principal, authentication objects

    • Custom security expressions

3. @PostAuthorize Annotation

The @PostAuthorize annotation checks authorization after method execution, allowing you to filter return values.

@PostAuthorize("returnObject.ownerId == authentication.principal.id")
public User getUserDetails(int userId) {
    return userService.findById(userId);
}

Key aspects of @PostAuthorize:

  • Evaluates after method execution

  • Can access the return value via returnObject

  • Useful for filtering sensitive data

  • Be aware it executes the method first, then checks authorization

4. @PreFilter and @PostFilter Annotations

Two additional annotations provide collection filtering:

@PreFilter filters collection arguments before method execution:

@PreFilter("filterObject.owner == authentication.name")
public void updateAccounts(List<Account> accounts) {
    // Method logic
}

@PostFilter filters collection return values:

@PostFilter("filterObject.owner == authentication.name")
public List<Account> getAllAccounts() {
    // Method logic
}

Best Practices and Additional Considerations

  1. Performance Implications:

    • @PostAuthorize, @PostFilter, and @PreFilter can impact performance with large collections

    • Consider filtering at the database level for large datasets

  2. Method Security with Spring Data Repositories:

  •   @Repository
      public interface UserRepository extends JpaRepository<User, Long> {
          @PreAuthorize("#entity.owner == authentication.name")
          <S extends User> S save(S entity);
      }
    
  • Custom Security Expressions:
    You can create custom security expressions by extending SecurityExpressionRoot:

  •   public class CustomSecurityExpressionRoot extends SecurityExpressionRoot {
          public boolean isMember(Long organizationId) {
              // Custom logic
          }
      }
    
  • Testing Method Security:
    Always test secured methods with different user roles:

  1.  @Test
     @WithMockUser(roles = "ADMIN")
     public void testAdminAccess() {
         // Test admin-accessible methods
     }
    
  2. Exception Handling:
    Method security throws:

    • AccessDeniedException when authorization fails

    • AuthenticationCredentialsNotFoundException when no authentication exists

Migration from @EnableGlobalMethodSecurity

If you're upgrading from an older Spring Security version:

  • Replace @EnableGlobalMethodSecurity with @EnableMethodSecurity

  • The new version enables prePostEnabled by default

  • jsr250Enabled and securedEnabled are disabled by default

My Takeaways

By combining these annotations with Spring's expression language, you can create sophisticated security rules that match your business requirements while keeping your code clean and maintainable.

Remember to:

  • Choose the appropriate annotation for your use case

  • Consider performance implications for post-execution filtering

  • Thoroughly test all security constraints

  • Document your security requirements alongside the code

Method-level security complements request-level security to create a comprehensive security strategy for your Spring applications.


If you loved this article, do consider following me :)

0
Subscribe to my newsletter

Read articles from Phenny Mwaisaka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Phenny Mwaisaka
Phenny Mwaisaka