Spring Security
Method Security
@Secured
Java Configuration
Troubleshooting

Spring Security, Method Security annotation Secured is not working java config

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

@Secured failures in Spring projects are usually configuration issues, not annotation bugs. Method security depends on proxy based interception, so one missing switch can make protected methods execute without checks. The fix is to enable the right method security mode and verify call paths actually pass through a Spring proxy.

Enabling Method Security in Modern Spring

For Spring Boot 3 and Spring Security 6, use @EnableMethodSecurity with securedEnabled = true.

java
1import org.springframework.context.annotation.Configuration;
2import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
3
4@Configuration
5@EnableMethodSecurity(securedEnabled = true)
6public class MethodSecurityConfig {
7}

For older stacks, the equivalent was @EnableGlobalMethodSecurity(securedEnabled = true). If you migrated and kept only the old annotation in incompatible versions, security advice may never activate.

Correct Role Mapping with @Secured

@Secured expects role style authorities, usually prefixed with ROLE_.

java
1import org.springframework.security.access.annotation.Secured;
2import org.springframework.stereotype.Service;
3
4@Service
5public class ReportService {
6
7    @Secured("ROLE_ADMIN")
8    public String exportFinancialReport() {
9        return "sensitive-report";
10    }
11}

If your authentication stores authorities like ADMIN without the prefix, access checks will fail. Either store ROLE_ADMIN or customize authority mapping consistently.

Web Security Configuration Example

A minimal security filter chain helps verify authentication plus method authorization together.

java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.security.config.Customizer;
4import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5import org.springframework.security.core.userdetails.User;
6import org.springframework.security.core.userdetails.UserDetailsService;
7import org.springframework.security.provisioning.InMemoryUserDetailsManager;
8import org.springframework.security.web.SecurityFilterChain;
9
10@Configuration
11public class SecurityConfig {
12
13    @Bean
14    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
15        http
16            .authorizeHttpRequests(auth -> auth
17                .requestMatchers("/health").permitAll()
18                .anyRequest().authenticated()
19            )
20            .httpBasic(Customizer.withDefaults())
21            .csrf(csrf -> csrf.disable());
22
23        return http.build();
24    }
25
26    @Bean
27    UserDetailsService users() {
28        var admin = User.withUsername("admin")
29            .password("{noop}password")
30            .roles("ADMIN")
31            .build();
32
33        var user = User.withUsername("user")
34            .password("{noop}password")
35            .roles("USER")
36            .build();
37
38        return new InMemoryUserDetailsManager(admin, user);
39    }
40}

When this is active, calling a @Secured("ROLE_ADMIN") method as user should return forbidden.

Proxy Boundaries and Self Invocation

The most common hidden issue is self invocation. If one method in a bean calls another secured method in the same bean using this, the call bypasses proxy interception and security advice does not run.

Bad pattern:

java
1@Service
2public class BillingService {
3
4    public void monthlyJob() {
5        this.recalculate();
6    }
7
8    @Secured("ROLE_ADMIN")
9    public void recalculate() {
10        // protected logic
11    }
12}

Better pattern is splitting secured logic into a second bean and calling that bean through Spring injection.

java
1@Service
2public class BillingAdminService {
3
4    @Secured("ROLE_ADMIN")
5    public void recalculate() {
6        // protected logic
7    }
8}
9
10@Service
11public class BillingJobService {
12    private final BillingAdminService billingAdminService;
13
14    public BillingJobService(BillingAdminService billingAdminService) {
15        this.billingAdminService = billingAdminService;
16    }
17
18    public void monthlyJob() {
19        billingAdminService.recalculate();
20    }
21}

Testing Method Security

Add integration tests that assert both allowed and denied paths. This catches config regressions early.

java
1import static org.junit.jupiter.api.Assertions.assertThrows;
2
3import org.junit.jupiter.api.Test;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.boot.test.context.SpringBootTest;
6import org.springframework.security.access.AccessDeniedException;
7import org.springframework.security.test.context.support.WithMockUser;
8
9@SpringBootTest
10class ReportServiceTests {
11
12    @Autowired
13    ReportService reportService;
14
15    @Test
16    @WithMockUser(roles = "ADMIN")
17    void adminCanCall() {
18        reportService.exportFinancialReport();
19    }
20
21    @Test
22    @WithMockUser(roles = "USER")
23    void userIsDenied() {
24        assertThrows(AccessDeniedException.class, () -> reportService.exportFinancialReport());
25    }
26}

Troubleshooting Checklist for Real Projects

When behavior still looks wrong, turn on Spring Security debug logs and inspect authentication authorities at runtime. Many teams spend hours on annotations while the real issue is an empty security context in async execution or scheduled jobs.

If secured methods run from @Async or scheduler threads, propagate security context intentionally or authenticate explicitly for that workflow. Method checks only run when an authentication object is present for the active thread.

properties
logging.level.org.springframework.security=DEBUG

Use this level during investigation and reduce log verbosity afterward to avoid noisy production output.

Common Pitfalls

  • Forgetting securedEnabled = true. Fix by enabling method security explicitly.
  • Using wrong role format. Fix by aligning @Secured values and granted authorities with the ROLE_ convention.
  • Calling secured methods inside the same bean. Fix by moving secured logic to another injected bean.
  • Relying only on controller tests. Fix by adding service level method security tests.
  • Mixing legacy and modern configuration styles inconsistently. Fix by standardizing configuration for your Spring version.

Summary

  • @Secured works when method security is enabled and calls pass through proxies.
  • Role naming must match granted authorities.
  • Self invocation bypasses security interception.
  • Integration tests should assert allowed and denied access paths.
  • Version aligned configuration is essential after framework upgrades.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.