Spring @EnableMethodSecurity annotation

1. Overview

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

using the @EnableGlobalMethodSecurity Annotations have been a standard until version 5.6 when @EnableMethodSecurity Introduced a more flexible way to configure authorization for method security.

In this tutorial, we will see how @EnableMethodSecurity in place of the old annotation. We’ll also look at the difference between its predecessor and some code examples.

2. @EnableMethodSecurity Vs @EnableGlobalMethodSecurity

We can understand more about this topic if we first look at how legal authority works @EnableGlobalMethodSecurity,

2.1. @EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity is a functional interface that we need @EnableWebSecurity To build our security layer and get method authorization.

Let’s create an example configuration class:

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

All method security implementations use a methodinterceptor which triggers when authorization is required, in this matter, Global method security configuration The base configuration is to enable class global method security.

Method SecurityInterceptor() makes method methodinterceptor The bean is accessing the metadata for the various authorization types we want to use.

Spring Security supports three built-in method security annotations:

  • prepost enabled for spring pre/post annotation
  • safe enabled for spring @Safe Comment
  • jsr 250 enabled for standard java @RoleAllowed Comment

In addition, within Method SecurityInterceptor()It is also prescribed:

  • access decision managerwhich “decides” whether to grant access using a voting-based mechanism
  • Authentication Manager, which we get from security context and responsible for authentication
  • after invocation managerwhich is responsible for providing handlers for pre/post expressions

The framework has a polling mechanism to deny or grant access to a specific method. we can see this as an example 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 polling, Spring Security pulls metadata attributes from the current method, for example, our REST endpoint. Finally, it checks them against the authorizations provided by the user.

We should also consider the possibility of voters not supporting the voting system and abstaining from voting.

Our access decision manager Then evaluates all responses from 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 Global method security configuration Class, For example, we want a custom security expression instead of the built in Spring EL with Spring Security. Or we may want to create our custom security voter.

2.2. @EnableMethodSecurity

with @EnableMethodSecurityWe can see Spring Security’s intent to move to bean-based configuration for authorization types.

Instead of a global configuration, we now have one for each 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());
    }
}

methodinterceptor necessarily include a authorization managerwhich now assigns the responsibility of checking and returning authorization decision Objection with final decision of proper implementationin this matter, authenticated authorization manager,

@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;
}

methodinterceptor throws one AccessDeniedException If we don’t have access to the resource:

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

3. @EnableMethodSecurity features

@EnableMethodSecurity Brings both small and large improvements compared to previous legacy implementations.

3.1. minor improvements

All authorization types are still supported. For example, it is still JSR-250 compliant. However, we do not need to add prepost enabled This is now the default as annotation Truth:

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)

we need to set prepost enabled To false If we want to deactivate it.

3.2. major reform

Global method security configuration Class is no longer in use. Spring Security replaces it with chunked configuration and a authorization managerwhich means we can define our authorization beans without extending any base configuration class,

It is worth noting that authorization manager The interface is generic and can be adapted to any object, although standard security applies method invocation,

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

Overall, it gives us micro-authorization using delegation. So, in practice, we have a authorization manager for all types. Of course, we can also manufacture our own.

Furthermore, it also means @EnableMethodSecurity does not allow @AspectJ annotation with a Aspect J In legacy implementations like method interceptors:

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 take 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 authorization manager application

So let’s see 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 will block the user.

As a first step, we define our user by adding a field to access the 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 look at our authentication layer for defining users in our system. For that, we’ll create a custom UserDetailService, We’ll use an in-memory map to store the 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 a user is present in our system, we want to restrict that information by checking whether he can access any restricted policies.

To demonstrate, we create a Java annotation @Policy Methods and policy to apply to calculation:

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

Let’s create the service on 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 cannot use the built-in authorization manager, such as Jsr250AuthorizationManager, He won’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 invoked, we double check that the user has authentication. Then, if the policy is open, we grant access. In case of restriction, we check whether the user has access to the restricted policy.

For that, we need to define a method interceptor For example, this will happen before execution, but it can also happen after. So let’s wrap it up 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()
          .disable()
          .authorizeRequests()
          .anyRequest()
          .authenticated()
          .and()
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

we are using method interceptor before authorization manager, It matches our Policy Service pattern and uses a custom Authorization Manager.

In addition, we also need to make our authentication manager aware of custom user details service, Then, when Spring Security intercepts the service method, we can access our custom user and check the user’s access policy.

5. Test

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 will use Spring Boot Test with our application to mock method protection:

@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 must be authorized, except when the user invokes a service to which he has no access by restricted policy.

6. conclusion

In this article, we have seen its main features @EnableMethodSecurity and how does it change @EnableGlobalMethodSecurity.

We also learned the differences between these annotations through the implementation flow, Then, we discussed how @EnableMethodSecurity Provides greater flexibility with bean-based configuration, Finally, we understood how to create custom Authorization Manager and MVC tests.

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

       

Leave a Comment