Implement RBAC with Spring Security roles, authorities, and method-level security annotations.
Published March 3, 2025
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
@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.