@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.