1. Overview

With Spring Security, we can configure the authentication and authorization of an application for methods such as our endpoints. For example, if a user has authentication on our domain, we can profile his use of an application by applying restrictions on existing methods.

Using @EnableGlobalMethodSecurity annotation has been a standard until version 5.6 when @EnableMethodSecurity introduced a more flexible way of configuring authorization for method security.

In this tutorial, we’ll see how @EnableMethodSecurity replaces the @EnableGlobalMethodSecurity annotation. We’ll also see the difference between them and some code examples.

2. @EnableMethodSecurity vs. @EnableGlobalMethodSecurity

Let’s check out how method authorization works with @EnableMethodSecurity and @EnableGlobalMethodSecurity.

2.1. @EnableMethodSecurity

With @EnableMethodSecurity, we can see the intention of Spring Security to move to a bean-based configuration for the authorization types.

Instead of a global configuration, we now have one for every type. Let’s see, for example, the Jsr250MethodSecurityConfiguration:

@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MethodSecurityConfiguration {
    // ...
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    Advisor jsr250AuthorizationMethodInterceptor() {
        return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
    }

    @Autowired(required = false)
    void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
        this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
    }
}

The MethodInterceptor essentially contains an AuthorizationManager, which now delegates the responsibility of checking and returning an AuthorizationDecision object with the final decision to the appropriate implementation, in this case, the AuthenticatedAuthorizationManager:

@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
    boolean granted = isGranted(authentication.get());
    return new AuthorityAuthorizationDecision(granted, this.authorities);
}

private boolean isGranted(Authentication authentication) {
    return authentication != null && authentication.isAuthenticated() && isAuthorized(authentication);
}

private boolean isAuthorized(Authentication authentication) {
    Set<String> authorities = AuthorityUtils.authorityListToSet(this.authorities);
    for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
        if (authorities.contains(grantedAuthority.getAuthority())) {
            return true;
        }
    }
    return false;
}

The MethodInterceptor throws an AccesDeniedException if we don’t have access to a resource:

AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi);
if (decision != null && !decision.isGranted()) {
    // ...
    throw new AccessDeniedException("Access Denied");
}

2.2. @EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity is a functional interface we need alongside @EnableWebSecurity to create our security layer and get method authorization.

Let’s create an example configuration class:

@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Configuration
public class SecurityConfig {
    // security beans
}

All method security implementations use a MethodInterceptor that triggers when authorization is required*.* In this case, the GlobalMethodSecurityConfiguration class is the base configuration for enabling global method security.

The methodSecurityInterceptor() method creates the MethodInterceptor bean using metadata for the different authorization types we may want to use.

Spring Security supports three in-built method security annotations:

  • prePostEnabled for Spring pre/post annotations
  • securedEnabled for Spring @Secured annotation
  • jsr250Enabled for standard Java @RoleAllowed annotation

Furthermore, within the methodSecurityInterceptor(), it is also set:

The framework has a voting mechanism to deny or grant access to a specific method. We can check this out as an example for the Jsr250Voter:

@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> definition) {
    boolean jsr250AttributeFound = false;
    for (ConfigAttribute attribute : definition) {
        if (Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE.equals(attribute)) {
            return ACCESS_GRANTED;
        }
        if (Jsr250SecurityConfig.DENY_ALL_ATTRIBUTE.equals(attribute)) {
            return ACCESS_DENIED;
        }
        if (supports(attribute)) {
            jsr250AttributeFound = true;
            // Attempt to find a matching granted authority
            for (GrantedAuthority authority : authentication.getAuthorities()) {
                if (attribute.getAttribute().equals(authority.getAuthority())) {
                    return ACCESS_GRANTED;
                }
            }
        }
    }
    return jsr250AttributeFound ? ACCESS_DENIED : ACCESS_ABSTAIN;
}

When voting, Spring Security pulls the metadata attributes from the current method, for example, our REST endpoint. Finally, it checks them against the user-granted authorities.

We should also note the possibility for a voter not to support the voting system and abstain.

Our AccessDecisionManager then evaluates all the responses from the available voters:

for (AccessDecisionVoter voter : getDecisionVoters()) {
    int result = voter.vote(authentication, object, configAttributes);
    switch (result) {
        case AccessDecisionVoter.ACCESS_GRANTED:
            return;
        case AccessDecisionVoter.ACCESS_DENIED:
            deny++;
            break;
        default:
            break;
    }
}
if (deny > 0) {
    throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}

If we want to customize our beans, we can extend the GlobalMethodSecurityConfiguration class*.* For example, we may want a custom security expression instead of the Spring EL in-built with Spring Security. Or we may want to make our custom security voter.

3. @EnableMethodSecurity Features

@EnableMethodSecurity brings both minor and major improvements compared to the previous legacy implementation.

3.1. Minor Improvements

All authorization types are still supported. For instance, it still complies with JSR-250. However, we don’t need to add prePostEnabled to the annotation as it now defaults to true:

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)

We need to set prePostEnabled to false if we want to disable it.

3.2. Major Improvements

The GlobalMethodSecurityConfiguration class is not in use anymore. *Spring Security replaces it with segmented configurations and an AuthorizationManager, which means we can define our authorization beans without extending any base configuration class*.

It’s worth noting that the AuthorizationManager interface is generic and can adapt to any object, although standard security applies to MethodInvocation:

AuthorizationDecision check(Supplier<Authentication> authentication, T object);

Overall, this provides us with fine-grained authorization using delegation. So, in practice, we have an AuthorizationManager for every type. Of course, we can also build our own.

Furthermore, this also means that @EnableMethodSecurity doesn’t allow @AspectJ annotation with an AspectJ method interceptor like in the legacy implementation:

public final class AspectJMethodSecurityInterceptor extends MethodSecurityInterceptor {
    public Object invoke(JoinPoint jp) throws Throwable {
        return super.invoke(new MethodInvocationAdapter(jp));
    }
    // ...
}

However, we still have full AOP support. For example, let’s have a look at the interceptor used by Jsr250MethodSecurityConfiguration we discussed earlier:

public final class AuthorizationManagerBeforeMethodInterceptor
  implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
    // ...
    public AuthorizationManagerBeforeMethodInterceptor(
      Pointcut pointcut, AuthorizationManager<MethodInvocation> authorizationManager) {
        Assert.notNull(pointcut, "pointcut cannot be null");
        Assert.notNull(authorizationManager, "authorizationManager cannot be null");
        this.pointcut = pointcut;
        this.authorizationManager = authorizationManager;
    }
    
    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        attemptAuthorization(mi);
        return mi.proceed();
    }
}

4. Custom AuthorizationManager Application

So let’s look at how to create a custom authorization manager.

Suppose we have endpoints for which we want to apply a policy. We want to authorize a user only if he has access to that policy. Otherwise, we’ll block the user.

As a first step, we define our user by adding a field to access a restricted policy:

public class SecurityUser implements UserDetails {
    private String userName;
    private String password;
    private List<GrantedAuthority> grantedAuthorityList;
    private boolean accessToRestrictedPolicy;

    // getters and setters
}

Now, let’s see our authentication layer to define users in our system. For that, we’ll create a custom UserDetailService. We’ll use an in-memory map to store users:

public class CustomUserDetailService implements UserDetailsService {
    private final Map<String, SecurityUser> userMap = new HashMap<>();

    public CustomUserDetailService(BCryptPasswordEncoder bCryptPasswordEncoder) {
        userMap.put("user", createUser("user", bCryptPasswordEncoder.encode("userPass"), false, "USER"));
        userMap.put("admin", createUser("admin", bCryptPasswordEncoder.encode("adminPass"), true, "ADMIN", "USER"));
    }

    @Override
    public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
        return Optional.ofNullable(map.get(username))
          .orElseThrow(() -> new UsernameNotFoundException("User " + username + " does not exists"));
    }

    private SecurityUser createUser(String userName, String password, boolean withRestrictedPolicy, String... role) {
        return SecurityUser.builder().withUserName(userName)
          .withPassword(password)
          .withGrantedAuthorityList(Arrays.stream(role)
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList()))
          .withAccessToRestrictedPolicy(withRestrictedPolicy);
    }
}

Once the user exists in our system, we want to restrict the information he can access by checking if he has access to some restricted policy.

To demonstrate, we create a Java annotation @Policy to apply on methods and a policy enum:

@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Policy {
    PolicyEnum value();
}
public enum PolicyEnum {
    RESTRICTED, OPEN
}

Let’s create the service to which we want to apply this policy:

@Service
public class PolicyService {
    @Policy(PolicyEnum.OPEN)
    public String openPolicy() {
        return "Open Policy Service";
    }

    @Policy(PolicyEnum.RESTRICTED)
    public String restrictedPolicy() {
        return "Restricted Policy Service";
    }
}

We can’t use an in-built authorization manager, such as the Jsr250AuthorizationManager. It wouldn’t know when and how to intercept the service policy check. So, let’s define our custom manager:

public class CustomAuthorizationManager<T> implements AuthorizationManager<MethodInvocation> {
    ...
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
        if (hasAuthentication(authentication.get())) {
            Policy policyAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), Policy.class);
            SecurityUser user = (SecurityUser) authentication.get().getPrincipal();
            return new AuthorizationDecision(Optional.ofNullable(policyAnnotation)
              .map(Policy::value).filter(policy -> policy == PolicyEnum.OPEN 
                || (policy == PolicyEnum.RESTRICTED && user.hasAccessToRestrictedPolicy())).isPresent());
        }
        return new AuthorizationDecision(false);
    }

    private boolean hasAuthentication(Authentication authentication) {
        return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated();
    }

    private boolean isNotAnonymous(Authentication authentication) {
        return !this.trustResolver.isAnonymous(authentication);
    }
}

When the service method is triggered, we double-check that the user has authentication. Then, we grant access if the policy is open. In case of restriction, we check if the user has access to the restricted policy.

For that, we need to define a MethodInterceptor that will be in place, for example, before the execution, but it could also be after. So let’s wrap it together with our security configuration class:

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager(
      HttpSecurity httpSecurity, UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
        return new CustomUserDetailService(bCryptPasswordEncoder);
    }

    @Bean
    public AuthorizationManager<MethodInvocation> authorizationManager() {
        return new CustomAuthorizationManager<>();
    }

    @Bean
    @Role(ROLE_INFRASTRUCTURE)
    public Advisor authorizationManagerBeforeMethodInterception(AuthorizationManager<MethodInvocation> authorizationManager) {
        JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
        pattern.setPattern("com.baeldung.enablemethodsecurity.services.*");
        return new AuthorizationManagerBeforeMethodInterceptor(pattern, authorizationManager);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
          .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry.anyRequest().authenticated())
          .sessionManagement(httpSecuritySessionManagementConfigurer -> 
                                                  httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

We are using AuthorizationManagerBeforeMethodInterceptor. It matches our policy service pattern and uses the custom authorization manager.

Furthermore, we also need to make our AuthenticationManager aware of the custom UserDetailsService. Then, when Spring Security intercepts the service method, we can access our custom user and check the user’s policy access.

5. Tests

Let’s define a REST controller:

@RestController
public class ResourceController {
    // ...
    @GetMapping("/openPolicy")
    public String openPolicy() {
        return policyService.openPolicy();
    }

    @GetMapping("/restrictedPolicy")
    public String restrictedPolicy() {
        return policyService.restrictedPolicy();
    }
}

We’ll use Spring Boot Test with our application to mock the method security:

@SpringBootTest(classes = EnableMethodSecurityApplication.class)
public class EnableMethodSecurityTest {
    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @BeforeEach
    public void setup() {
        mvc = MockMvcBuilders.webAppContextSetup(context)
          .apply(springSecurity())
          .build();
    }

    @Test
    @WithUserDetails(value = "admin")
    public void whenAdminAccessOpenEndpoint_thenOk() throws Exception {
        mvc.perform(get("/openPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails(value = "admin")
    public void whenAdminAccessRestrictedEndpoint_thenOk() throws Exception {
        mvc.perform(get("/restrictedPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails()
    public void whenUserAccessOpenEndpoint_thenOk() throws Exception {
        mvc.perform(get("/openPolicy"))
          .andExpect(status().isOk());
    }

    @Test
    @WithUserDetails()
    public void whenUserAccessRestrictedEndpoint_thenIsForbidden() throws Exception {
        mvc.perform(get("/restrictedPolicy"))
          .andExpect(status().isForbidden());
    }
}

All responses should be authorized, except the one with the user invoking a service for which he has no access to the restricted policy.

6. Conclusion

In this article, we’ve seen the main features of @EnableMethodSecurity and how it replaces @EnableGlobalMethodSecurity.

We also learned the differences between these annotations by going through the implementation flow*.* Then, we discussed how @EnableMethodSecurity offers more flexibility with bean-based configurations*.* Finally, we understood how to create a custom authorization manager and MVC tests.

As always, we can find working code examples over on GitHub.