Spring Boot
Role-Based Access
Spring Security implements role-based access control (RBAC) through GrantedAuthority. Roles prefix with ROLE_ by convention. Access rules are declared in SecurityFilterChain using requestMatchers, or on individual methods using @PreAuthorize, @PostAuthorize, @Secured, and @RolesAllowed. This entry covers SecurityFilterChain rules, method security, hierarchical roles, and dynamic permission loading.
SecurityFilterChain URL Authorization
Declare URL-level access rules in SecurityFilterChain using requestMatchers. Rules are evaluated in order β the first match wins. Place specific paths before broad patterns. hasRole() checks for the ROLE_ prefix automatically; hasAuthority() checks the exact string.
Java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// ββ Public endpoints ββββββββββββββββββββββββββββββββββ
.requestMatchers(
"/api/v1/auth/**",
"/actuator/health",
"/actuator/info",
"/v3/api-docs/**",
"/swagger-ui/**"
).permitAll()
// ββ Admin only ββββββββββββββββββββββββββββββββββββββββ
.requestMatchers("/api/v1/admin/**")
.hasRole("ADMIN")
// ββ Admin or Manager ββββββββββββββββββββββββββββββββββ
.requestMatchers("/api/v1/reports/**")
.hasAnyRole("ADMIN", "MANAGER")
// ββ Specific HTTP method + role βββββββββββββββββββββββ
.requestMatchers(HttpMethod.DELETE,
"/api/v1/products/**")
.hasRole("ADMIN")
.requestMatchers(HttpMethod.POST,
"/api/v1/products/**")
.hasAnyRole("ADMIN", "MANAGER")
.requestMatchers(HttpMethod.GET,
"/api/v1/products/**")
.hasAnyRole("USER", "MANAGER", "ADMIN")
// ββ Fine-grained authority (not role) βββββββββββββββββ
.requestMatchers("/api/v1/billing/**")
.hasAuthority("BILLING_READ")
// ββ All other requests need authentication βββββββββββββ
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter,
UsernamePasswordAuthenticationFilter.class)
.build();
}
}Method Security with @PreAuthorize
@PreAuthorize evaluates a Spring Expression Language (SpEL) expression before the method runs. Enable it with @EnableMethodSecurity. It is the most flexible method-level security annotation β expressions can check roles, authorities, and method parameters.
Java
// ββ Enable method security ββββββββββββββββββββββββββββββββββββββββββββ
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // enables @PreAuthorize / @PostAuthorize
securedEnabled = true, // enables @Secured
jsr250Enabled = true // enables @RolesAllowed
)
public class MethodSecurityConfig {}
// ββ Service with @PreAuthorize βββββββββββββββββββββββββββββββββββββββββ
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepo;
// ββ Simple role check βββββββββββββββββββββββββββββββββββββββββββββ
@PreAuthorize("hasRole('ADMIN')")
public void delete(Long id) {
productRepo.deleteById(id);
}
// ββ Multiple roles (OR) βββββββββββββββββββββββββββββββββββββββββββ
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public ProductResponse create(CreateProductRequest request) {
return ProductResponse.from(
productRepo.save(Product.from(request)));
}
// ββ Fine-grained authority ββββββββββββββββββββββββββββββββββββββββ
@PreAuthorize("hasAuthority('PRODUCT_WRITE')")
public ProductResponse update(Long id, UpdateProductRequest req) {
Product p = productRepo.findById(id).orElseThrow();
p.apply(req);
return ProductResponse.from(productRepo.save(p));
}
// ββ Access method parameter in expression βββββββββββββββββββββββββ
@PreAuthorize("hasRole('ADMIN') or #ownerId == authentication.principal.id")
public List<ProductResponse> findByOwner(Long ownerId) {
return productRepo.findByOwnerId(ownerId)
.stream().map(ProductResponse::from).toList();
}
// ββ @PostAuthorize β check return value βββββββββββββββββββββββββββ
@PostAuthorize("hasRole('ADMIN') or " +
"returnObject.ownerId == authentication.principal.id")
public ProductResponse findById(Long id) {
return productRepo.findById(id)
.map(ProductResponse::from)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// ββ Compound expression βββββββββββββββββββββββββββββββββββββββββββ
@PreAuthorize("isAuthenticated() and " +
"(hasRole('ADMIN') or " +
" (hasRole('MANAGER') and #request.price <= 1000))")
public ProductResponse createWithLimit(
CreateProductRequest request) {
return ProductResponse.from(
productRepo.save(Product.from(request)));
}
}@Secured and @RolesAllowed
@Secured is a Spring annotation that accepts a list of role strings. @RolesAllowed is the JSR-250 equivalent. Both are simpler than @PreAuthorize but cannot express compound logic or access method parameters. Prefer @PreAuthorize for new code.
Java
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
// ββ @Secured β Spring annotation βββββββββββββββββββββββββββββββββ
@Secured("ROLE_ADMIN")
public void cancelAny(Long orderId) {
orderRepo.findById(orderId)
.ifPresent(o -> {
o.setStatus(OrderStatus.CANCELLED);
orderRepo.save(o);
});
}
// ββ @Secured β multiple roles (OR) ββββββββββββββββββββββββββββββββ
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public List<OrderResponse> findAll(Pageable pageable) {
return orderRepo.findAll(pageable)
.map(OrderResponse::from)
.toList();
}
// ββ @RolesAllowed β JSR-250 βββββββββββββββββββββββββββββββββββββββ
@RolesAllowed("ROLE_USER")
public OrderResponse findMyOrder(Long orderId,
@AuthenticationPrincipal AuthenticatedUser principal) {
return orderRepo.findByIdAndUserId(orderId, principal.getId())
.map(OrderResponse::from)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// ββ @RolesAllowed β multiple roles ββββββββββββββββββββββββββββββββ
@RolesAllowed({"ROLE_ADMIN", "ROLE_MANAGER"})
public OrderResponse updateStatus(Long orderId,
UpdateStatusRequest request) {
Order order = orderRepo.findById(orderId).orElseThrow();
order.setStatus(request.status());
return OrderResponse.from(orderRepo.save(order));
}
}
// ββ Comparison ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
//
// Annotation Package Parameters Expressions Recommended
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// @PreAuthorize Spring Security Yes Yes Yes
// @PostAuthorize Spring Security Yes Yes For return checks
// @Secured Spring Security No No Legacy
// @RolesAllowed JSR-250 No No PortabilityRole Hierarchy
A role hierarchy defines that higher roles implicitly include the authorities of lower roles β ADMIN includes MANAGER which includes USER. Configure it as a RoleHierarchy bean. Spring Security applies it automatically in @PreAuthorize expressions and SecurityFilterChain requestMatchers.
Java
// ββ Role hierarchy configuration βββββββββββββββββββββββββββββββββββββ
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// ββ RoleHierarchy bean ββββββββββββββββββββββββββββββββββββββββββββ
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("ADMIN").implies("MANAGER")
.role("MANAGER").implies("EDITOR")
.role("EDITOR").implies("USER")
.build();
// ADMIN β has MANAGER + EDITOR + USER authorities
// MANAGERβ has EDITOR + USER authorities
// EDITOR β has USER authorities
// USER β base level
}
// ββ Wire hierarchy into method security βββββββββββββββββββββββββββ
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
// ββ Wire hierarchy into URL security ββββββββββββββββββββββββββββββ
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
RoleHierarchy roleHierarchy) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// hasRole("MANAGER") also allows ADMIN users because
// of the hierarchy
.requestMatchers("/api/v1/reports/**").hasRole("MANAGER")
.requestMatchers("/api/v1/content/**").hasRole("EDITOR")
.anyRequest().hasRole("USER"))
.build();
}
}
// ββ Service β hierarchy in action ββββββββββββββββββββββββββββββββββββ
@Service
public class ContentService {
// ADMIN, MANAGER, and EDITOR can all call this
// because ADMIN > MANAGER > EDITOR
@PreAuthorize("hasRole('EDITOR')")
public ContentResponse publish(Long contentId) {
// ...
}
// Only ADMIN and MANAGER can call this
@PreAuthorize("hasRole('MANAGER')")
public void approve(Long contentId) {
// ...
}
}Dynamic Permissions from Database
Hard-coded roles in annotations work for simple applications but break when permissions change at runtime. Load permissions from the database and represent them as GrantedAuthority strings. The permission check in @PreAuthorize then resolves against the loaded set without code changes.
Java
// ββ Permission entity βββββββββββββββββββββββββββββββββββββββββββββββββ
@Entity
@Table(name = "permissions")
@Getter @Setter @NoArgsConstructor
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String name; // e.g. "PRODUCT_READ", "ORDER_CANCEL"
}
@Entity
@Table(name = "roles")
@Getter @Setter @NoArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String name; // e.g. "ROLE_ADMIN"
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
}
// ββ UserDetailsService loading roles + permissions ββββββββββββββββββββ
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepo;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = userRepo.findByEmailWithRoles(email)
.orElseThrow(() ->
new UsernameNotFoundException("Invalid credentials"));
Set<GrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(role -> {
// Add the role itself
authorities.add(
new SimpleGrantedAuthority(role.getName()));
// Add each permission from the role
role.getPermissions().forEach(perm ->
authorities.add(
new SimpleGrantedAuthority(perm.getName())));
});
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword())
.authorities(authorities)
.build();
}
}
// ββ Service using fine-grained permission checks ββββββββββββββββββββββ
@Service
public class OrderService {
@PreAuthorize("hasAuthority('ORDER_READ')")
public Page<OrderResponse> findAll(Pageable pageable) { ... }
@PreAuthorize("hasAuthority('ORDER_CANCEL')")
public void cancel(Long orderId) { ... }
@PreAuthorize("hasAuthority('ORDER_REFUND')")
public RefundResponse refund(Long orderId) { ... }
}