@PreAuthorize, @Secured, method security — control access at the method level.
Published September 21, 2026
RBAC restricts access based on roles assigned to users. Spring Security supports two levels: URL-based (in SecurityFilterChain) and method-level (@PreAuthorize).
In Spring Security:
ROLE_ (e.g., ROLE_ADMIN)user:write, report:read)// hasRole("ADMIN") checks for GrantedAuthority = "ROLE_ADMIN"
// hasAuthority("user:write") checks for GrantedAuthority = "user:write" exactly
@PreAuthorize("hasRole('ADMIN')")
void deleteUser(String userId) { ... } // evaluated BEFORE the method runs
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
void updateProfile(String userId, ProfileUpdate update) { ... } // can reference method arguments directly
@PostAuthorize("returnObject.ownerId == authentication.principal.id")
Document getDocument(String docId) { ... } // evaluated AFTER the method runs, can inspect the return value
@PreAuthorize runs its SpEL (Spring Expression Language) check before the method body executes, and can reference method parameters directly (#userId) — the standard choice for most authorization checks, since it prevents the method from running at all if the check fails. @PostAuthorize runs after, with access to returnObject — necessary specifically when the authorization decision depends on data only available in the result (e.g. "can this user see this document" when "whose document it is" is itself part of what the method fetches).
@Secured("ROLE_ADMIN") void deleteUser(String userId) { ... }
Predates @PreAuthorize/@PostAuthorize, supports only simple role checks (no SpEL expressions, no access to method arguments or return values) — simpler to read for the common "just check a role" case, but far less flexible. Most modern Spring Security code defaults to @PreAuthorize even for simple role checks, reserving @Secured mainly for legacy codebases already using it consistently.
@Configuration
@EnableMethodSecurity // required — these annotations do nothing without it
class SecurityConfig { ... }
None of @PreAuthorize/@PostAuthorize/@Secured have any effect until method security is explicitly enabled at the configuration level — a common "why isn't my @PreAuthorize doing anything" debugging dead-end is simply a missing @EnableMethodSecurity.
@PreAuthorize("hasPermission(#document, 'EDIT')")
void editDocument(Document document) { ... }
class DocumentPermissionEvaluator implements PermissionEvaluator {
public boolean hasPermission(Authentication auth, Object target, Object permission) {
Document doc = (Document) target;
User user = (User) auth.getPrincipal();
return doc.getOwnerId().equals(user.getId()) || user.hasRole("ADMIN");
// arbitrarily complex logic — not limited to a simple role check
}
}
When authorization depends on a relationship between the specific resource and the specific user ("can this user edit this document," not just "does this user have an EDIT role globally") rather than a simple role check, a custom PermissionEvaluator plugged into the hasPermission() SpEL function is the standard extension point — this is the mechanism behind genuinely fine-grained, per-resource authorization rather than coarse role gates.
@Document("users")
public class User implements UserDetails {
private String email;
private String passwordHash;
private Set<String> roles = new HashSet<>(); // e.g., ["USER", "ADMIN"]
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
}
@Override
public String getUsername() { return email; }
@Override
public String getPassword() { return passwordHash; }
// Other UserDetails methods default to true for active accounts
}
http.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/courses/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/lessons/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/instructor/**").hasAnyRole("ADMIN", "INSTRUCTOR")
.anyRequest().authenticated()
);
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {}
@RestController
public class CourseController {
@GetMapping("/api/v1/courses")
public List<CourseDto> listCourses() {
return courseService.list(); // public — no annotation needed
}
@PostMapping("/api/v1/courses")
@PreAuthorize("hasRole('ADMIN')")
public CourseDto create(@RequestBody @Valid CourseRequest req) {
return courseService.create(req);
}
@DeleteMapping("/api/v1/courses/{id}")
@PreAuthorize("hasRole('ADMIN') or @courseService.isOwner(#id, authentication.name)")
public void delete(@PathVariable String id) {
courseService.delete(id);
}
}
For complex rules, implement PermissionEvaluator:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
if (target instanceof String resourceId && permission instanceof String action) {
// Check if user has permission to perform action on resource
String userId = auth.getName();
return resourceAccessService.canAccess(userId, resourceId, action);
}
return false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId,
String targetType, Object permission) {
return false;
}
}
// Usage:
@PreAuthorize("hasPermission(#lessonId, 'read')")
public LessonDto getLesson(String lessonId) { ... }
@Service
public class OrderService {
public List<Order> getMyOrders() {
String username = SecurityContextHolder.getContext()
.getAuthentication().getName();
return orderRepository.findByUserId(username);
}
}
// Or inject via @AuthenticationPrincipal in controllers:
@GetMapping("/orders")
public List<OrderDto> getOrders(@AuthenticationPrincipal UserDetails user) {
return orderService.getOrdersFor(user.getUsername());
}
hasRole('ADMIN') and hasAuthority('ROLE_ADMIN') — they're equivalent but hasRole auto-prepends ROLE_.@PreAuthorize vs @Secured: @PreAuthorize supports SpEL and is more flexible; @Secured only supports role names.The configuration above is straightforward to write. The bugs usually come from a few recurring misunderstandings:
ROLE_ prefix mismatch. hasRole("ADMIN") checks for the authority ROLE_ADMIN. hasAuthority("ADMIN") checks for exactly ADMIN. If your UserDetails returns ADMIN without the prefix, hasRole("ADMIN") silently denies everyone. Pick one convention: roles as ROLE_* authorities, and fine-grained permissions (invoice:write) as plain authorities checked with hasAuthority.@PreAuthorize does nothing unless @EnableMethodSecurity is present. It also only works through Spring's proxy, so a call from one method to another in the same class skips the check entirely. Put secured methods on beans called from outside, or secure the entry point.hasRole('USER') on /api/orders/** lets any user read any order, for example by changing the ID in the URL. Role checks answer "what kind of action?". You still need ownership checks for "on whose data?", in @PreAuthorize("@orderSecurity.isOwner(#id, authentication)"), a PermissionEvaluator, or the query itself (findByIdAndOwnerId).ROLE_EU_SUPPORT_READONLY_BILLING) becomes unmanageable. Keep a small set of roles, map them to permissions, and check permissions in code. Changing what a role can do then becomes a data change, not a code change.If admins should automatically have every manager permission, and managers every user permission, declare a hierarchy instead of listing roles repeatedly:
@Bean
static RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy("""
ROLE_ADMIN > ROLE_MANAGER
ROLE_MANAGER > ROLE_USER
""");
}
With method security, expose the hierarchy to the expression handler (Spring Security 6 picks up a RoleHierarchy bean for @PreAuthorize), so that hasRole('USER') also passes for managers and admins.
Authorization is security-critical, and easy to break during refactoring, so test it explicitly:
@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderSecurityTest {
@Autowired MockMvc mvc;
@Test @WithMockUser(roles = "USER")
void userCannotDeleteOrders() throws Exception {
mvc.perform(delete("/api/orders/42").with(csrf())).andExpect(status().isForbidden());
}
@Test @WithMockUser(roles = "ADMIN")
void adminCanDeleteOrders() throws Exception {
mvc.perform(delete("/api/orders/42").with(csrf())).andExpect(status().isNoContent());
}
@Test
void anonymousIsRejected() throws Exception {
mvc.perform(get("/api/orders/42")).andExpect(status().isUnauthorized());
}
}
Test the denials as carefully as the permissions. A test suite that only checks that admins can do things never notices when a change accidentally lets everyone do them.
Q: What's the difference between hasRole and hasAuthority?
A: hasRole("X") checks for the authority ROLE_X, adding the prefix automatically. hasAuthority("X") checks for the exact string X. They refer to the same list of granted authorities. Roles are simply authorities that follow the ROLE_ naming convention.
Q: Why doesn't my @PreAuthorize annotation work?
A: Common causes: method security isn't enabled (@EnableMethodSecurity), the method is called from inside the same class (bypassing the security proxy), the method isn't public, or the bean isn't managed by Spring. Also check the ROLE_ prefix expectations in the expression.
Q: RBAC or ABAC: which should I use? A: RBAC (role-based) is simple, and fits most applications when combined with ownership checks. ABAC (attribute-based) evaluates attributes of the user, the resource and the context (department, classification, time, location). It's worth it when rules depend on many such attributes. Many systems use RBAC for coarse access, and a few attribute-based checks for sensitive operations.
Q: Where should authorization checks live: controller, service or repository? A: Put coarse checks at the entry points (URL rules or controller methods), and object-level checks where the object is loaded, usually the service layer, or the repository query itself. Checks near the data are harder to bypass through a new endpoint that someone forgets to secure.
Q: How do you give users different permissions in different organizations or projects?
A: Store role assignments scoped to a resource: (user, organization, role) rather than (user, role). Checks then ask "does this user have permission P in organization O?". Spring Security's PermissionEvaluator, or a custom policy bean, can implement that lookup.