Friday, March 17, 2023
HomeJavaSpring @EnableMethodSecurity Annotation | Baeldung

Spring @EnableMethodSecurity Annotation | Baeldung


1. Overview

With Spring Safety, we are able to configure the authentication and authorization of an software for strategies comparable to our endpoints. For instance, if a consumer has authentication on our area, we are able to profile his use of an software by making use of restrictions on current strategies.

Utilizing @EnableGlobalMethodSecurity annotation has been a normal till model 5.6 when @EnableMethodSecurity launched a extra versatile means of configuring authorization for technique safety.

On this tutorial, we’ll see how @EnableMethodSecurity replaces the previous annotation. We’ll additionally see the distinction between its predecessor and a few code examples.

2. @EnableMethodSecurity vs. @EnableGlobalMethodSecurity

We will perceive extra about this matter if we first take a look at how technique authorization works with @EnableGlobalMethodSecurity.

2.1. @EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity is a practical interface we’d like alongside @EnableWebSecurity to create our safety layer and get technique authorization.

Let’s create an instance configuration class:

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

All technique safety implementations use a MethodInterceptor that triggers when authorization is requiredOn this case, the GlobalMethodSecurityConfiguration class is the bottom configuration for enabling world technique safety.

The methodSecurityInterceptor() technique creates the MethodInterceptor bean utilizing metadata for the totally different authorization sorts we might need to use.

Spring Safety helps three in-built technique safety annotations:

  • prePostEnabled for Spring pre/submit annotations
  • securedEnabled for Spring @Secured annotation
  • jsr250Enabled for normal Java @RoleAllowed annotation

Moreover, inside the methodSecurityInterceptor(), it’s also set:

The framework has a voting mechanism to disclaim or grant entry to a selected technique. We will test this out for example for the Jsr250Voter:

@Override
public int vote(Authentication authentication, Object object, Assortment<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 (helps(attribute)) {
            jsr250AttributeFound = true;
            // Try and discover 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 Safety pulls the metadata attributes from the present technique, for instance, our REST endpoint. Lastly, it checks them towards the user-granted authorities.

We also needs to observe the chance for a voter to not assist the voting system and abstain.

Our AccessDecisionManager then evaluates all of the responses from the obtainable voters:

for (AccessDecisionVoter voter : getDecisionVoters()) {
    int end result = voter.vote(authentication, object, configAttributes);
    change (end 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", "Entry is denied"));
}

If we need to customise our beans, we are able to prolong the GlobalMethodSecurityConfiguration classFor instance, we might need a customized safety expression as an alternative of the Spring EL in-built with Spring Safety. Or we might need to make our customized safety voter.

2.2. @EnableMethodSecurity

With @EnableMethodSecurity, we are able to see the intention of Spring Safety to maneuver to a bean-based configuration for the authorization sorts.

As an alternative of a world configuration, we now have one for each kind. Let’s have a look at, for instance, the Jsr250MethodSecurityConfiguration:

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

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

The MethodInterceptor primarily comprises an AuthorizationManager, which now delegates the accountability of checking and returning an AuthorizationDecision object with the ultimate resolution to the suitable implementation, on this case, the AuthenticatedAuthorizationManager:

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

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

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

The MethodInterceptor throws an AccesDeniedException if we do not have entry to a useful resource:

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

3. @EnableMethodSecurity options

@EnableMethodSecurity brings each minor and main enhancements in comparison with the earlier legacy implementation.

3.1. Minor Enhancements

All authorization sorts are nonetheless supported. For example, it nonetheless complies with JSR-250. Nonetheless, we need not add prePostEnabled to the annotation because it now defaults to true:

@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)

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

3.2. Main Enhancements

The GlobalMethodSecurityConfiguration class shouldn’t be in use anymore. Spring Safety replaces it with segmented configurations and an AuthorizationManager, which implies we are able to outline our authorization beans with out extending any base configuration class.

It is price noting that the AuthorizationManager interface is generic and may adapt to any object, though normal safety applies to MethodInvocation:

AuthorizationDecision test(Provider<Authentication> authentication, T object);

General, this offers us with fine-grained authorization utilizing delegation. So, in follow, now we have an AuthorizationManager for each kind. After all, we are able to additionally construct our personal.

Moreover, this additionally signifies that @EnableMethodSecurity would not permit @AspectJ annotation with an AspectJ technique interceptor like within the legacy implementation:

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

Nonetheless, we nonetheless have full AOP assist. For instance, let’s take a look on the interceptor utilized by Jsr250MethodSecurityConfiguration we mentioned earlier:

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

4. Customized AuthorizationManager Software

So let’s take a look at the way to create a customized authorization supervisor.

Suppose now we have endpoints for which we need to apply a coverage. We need to authorize a consumer provided that he has entry to that coverage. In any other case, we’ll block the consumer.

As a primary step, we outline our consumer by including a discipline to entry a restricted coverage:

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

    // getters and setters
}

Now, let’s examine our authentication layer to outline customers in our system. For that, we’ll create a customized UserDetailService. We’ll use an in-memory map to retailer customers:

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

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

    @Override
    public UserDetails loadUserByUsername(last String username) throws UsernameNotFoundException {
        return Optionally available.ofNullable(map.get(username))
          .orElseThrow(() -> new UsernameNotFoundException("Person " + username + " doesn't exists"));
    }

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

As soon as the consumer exists in our system, we need to prohibit the knowledge he can entry by checking if he has entry to some restricted coverage.

To exhibit, we create a Java annotation @Coverage to use on strategies and a coverage enum:

@Goal(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Coverage {
    PolicyEnum worth();
}
public enum PolicyEnum {
    RESTRICTED, OPEN
}

Let’s create the service to which we need to apply this coverage:

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

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

We won’t use an in-built authorization supervisor, such because the Jsr250AuthorizationManager. It would not know when and the way to intercept the service coverage test. So, let’s outline our customized supervisor:

public class CustomAuthorizationManager<T> implements AuthorizationManager<MethodInvocation> {
    ...
    @Override
    public AuthorizationDecision test(Provider<Authentication> authentication, MethodInvocation methodInvocation) {
        if (hasAuthentication(authentication.get()))  (coverage == PolicyEnum.RESTRICTED && consumer.hasAccessToRestrictedPolicy())).isPresent());
        
        return new AuthorizationDecision(false);
    }

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

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

When the service technique is triggered, we double-check that the consumer has authentication. Then, we grant entry if the coverage is open. In case of restriction, we test if the consumer has entry to the restricted coverage.

For that, we have to outline a MethodInterceptor that shall be in place, for instance, earlier than the execution, nevertheless it may be after. So let’s wrap it along with our safety 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.construct();
    }

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

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

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
          .disable()
          .authorizeRequests()
          .anyRequest()
          .authenticated()
          .and()
          .sessionManagement()
          .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        return http.construct();
    }

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

We’re utilizing AuthorizationManagerBeforeMethodInterceptor. It matches our coverage service sample and makes use of the customized authorization supervisor.

Moreover, we additionally must make our AuthenticationManager conscious of the customized UserDetailsService. Then, when Spring Safety intercepts the service technique, we are able to entry our customized consumer and test the consumer’s coverage entry.

5. Assessments

Let’s outline 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 Check with our software to mock the tactic safety:

@SpringBootTest(courses = EnableMethodSecurityApplication.class)
public class EnableMethodSecurityTest {
    @Autowired
    personal WebApplicationContext context;

    personal MockMvc mvc;

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

    @Check
    @WithUserDetails(worth = "admin")
    public void whenAdminAccessOpenEndpoint_thenOk() throws Exception {
        mvc.carry out(get("/openPolicy"))
          .andExpect(standing().isOk());
    }

    @Check
    @WithUserDetails(worth = "admin")
    public void whenAdminAccessRestrictedEndpoint_thenOk() throws Exception {
        mvc.carry out(get("/restrictedPolicy"))
          .andExpect(standing().isOk());
    }

    @Check
    @WithUserDetails()
    public void whenUserAccessOpenEndpoint_thenOk() throws Exception {
        mvc.carry out(get("/openPolicy"))
          .andExpect(standing().isOk());
    }

    @Check
    @WithUserDetails()
    public void whenUserAccessRestrictedEndpoint_thenIsForbidden() throws Exception {
        mvc.carry out(get("/restrictedPolicy"))
          .andExpect(standing().isForbidden());
    }
}

All responses needs to be licensed, besides the one with the consumer invoking a service for which he has no entry to the restricted coverage.

6. Conclusion

On this article, we have seen the primary options of @EnableMethodSecurity and the way it replaces @EnableGlobalMethodSecurity.

We additionally realized the variations between these annotations by going by means of the implementation stream. Then, we mentioned how @EnableMethodSecurity gives extra flexibility with bean-based configurations. Lastly, we understood the way to create a customized authorization supervisor and MVC checks.

As at all times, we are able to discover working code examples over on GitHub.

November Low cost Launch 2022 – Backside

We’re lastly operating a Black Friday launch. All Programs are 30% off till December 2nd:

>> GET ACCESS NOW



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments