Description of the Bug

When injecting a bean inside the @EnableGlobalMethodSecurity Configuration class, any method marked with @Cacheable within that bean isn't registered by the Cache abstraction. Surprisingly simply changing the name of the Cache Configuration bean can simply fix that behavior (e.g: Configuration file names CacheConfiguration + MethodSecurityConfiguration won't work, but ZCacheConfiguration + MethodSecurityConfiguration will work). Spring Version: 2.5.5 JDK Version: 11.0.3

Steps to reproduce

  • Create a simple Spring Boot project with Web Security dependency
  • Enable Cache by creating a configuration bean (CacheConfiguration.java)
@Configuration
@EnableCaching
public class CacheConfiguration extends CachingConfigurerSupport { }
  • Create a sample Service with a @Cacheable method (ExampleService.java)
@Service
public class ExampleService {

    @Cacheable(value = "example")
    public UUID getCacheableString(UUID key) {
        return UUID.randomUUID();
    }
} 
  • Enable GlobalMethodSecurityConfiguration (MethodSecurityConfiguration .java) and inject the previous service inside
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    private final ExampleService service;

    public MethodSecurityConfiguration(@Autowired ExampleService service) {
        this.service = service;
    }
}
  • Check behavior of Cache Abstraction by enabling TRACE logs in application.properties logging.level.org.springframework.cache=TRACE

Logs

Spring Security @EnableCache doesn't work with @GlobalMethodSecurityConfiguration

Expected behavior

@Cacheable method getCacheableString should be registered in the Cache Abstraction and the Cache should work.

Example Solutions

  • Simply rename file CacheConfiguration to ZCacheConfiguration, and the @Cacheable method is magically registered.
  • Or, remove the injection of ExampleService inside MethodSecurityConfiguration

Logs

Spring Security @EnableCache doesn't work with @GlobalMethodSecurityConfiguration

Sample

https://github.com/LionelClaudon/issue-with-global-method-security

Note: This may be the same issue that was described here: https://github.com/spring-projects/spring-security/issues/5293

Comment From: sjohnr

Thanks for the report and the minimal sample @LionelClaudon. Based on your sample, I think it's worth noting that this seems to specifically stem from the use of extends GlobalMethodSecurityConfiguration on your application-supplied MethodSecurityConfiguration class.

Some possible workarounds for you to try (in your real-world application):

  1. Remove the use of GlobalMethodSecurityConfiguration as a base class, while leaving @EnableGlobalMethodSecurity(prePostEnabled = true)
  2. Separate this configuration into two config classes, one extending GlobalMethodSecurityConfiguration (and not depending on the service) and another that depends on the service with @Cacheable

Comment From: ABoat365

I also encountered the same problem. The solution is to write a separate class that does not use caching.

Comment From: dbouclier

Hello, I'm also getting this issue when using cache in a custom permission evaluator (like in #5293)

As described in the documentation https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#_globalmethodsecurityconfiguration

it's difficult to "remove he use of GlobalMethodSecurityConfiguration as a base class, while leaving @EnableGlobalMethodSecurity(prePostEnabled = true)" when you need to define a custom permission evaluator. or "Separate this configuration into two config classes"

The only workaround working is to declare the @EnableCaching after the @EnableGlobalMethodSecurity (which is not obvious and prone to nasty bug) OR on the class @SpringBootApplication (which introduce also more code, for example in the integration tests using @SpringBootTest too exclude the caching)

Comment From: jzheaux

Working with advisors can be tricky since they are initialized so early in the startup process. For example, bean wiring will not add a particular advisor (like the caching advisor) if it is still in the process of creation itself. This situation arises in your case because EnableGlobalMethodSecurity publishes an advisor that is dependent on MethodSecurityConfiguration, which is dependent on ExampleService, which itself is dependent on the caching advisor.

This is why it works if you change the name to ZCacheConfiguration because it places it later in the alphabetized list of beans to be advised (the cache advisor is already initialized by then). If I change it to BCacheConfiguration the error behavior still happens, for example, but if I change it to NCacheConfiguration so that it comes after MethodSecurityConfiguration, the issue is also resolved. (@dbouclier offered other options that get at the same idea)

These aren't ideal ways to solve race conditions like these. Instead, stop using @EnableGlobalMethodSecurity and GlobalMethodSecurityConfiguration. Change to @EnableMethodSecurity and static bean publishing for any beans that you want @EnableMethodSecurity to use.

This allows Spring to wait on constructing and advising both ExampleService and MethodSecurityConfiguration until all advisors are constructed.

I imagine that the reason MethodSecurityConfiguration is dependent on ExampleService is because there is some method security component that requires it. In that case, applying the above principle would look similar to this:

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfiguration {
    @Bean 
    static MethodSecurityExpressionHandler methodSecurityExpressionHandler(ExampleService exampleService) {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(...);
        return handler;
    }
}