Spring BootRole-Based Access
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           Portability

Role 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) { ... }
}