Skip to content
Spring Boot sb security 3 min read

Authorization & Roles

Once a user is authenticated, authorization decides what they may do. In Spring Security a principal carries a set of GrantedAuthority objects; rules in authorizeHttpRequests (and method annotations) check those authorities against each request. The two flavors — roles and authorities — are the same mechanism with one twist: a “role” is just an authority with a ROLE_ prefix.

Roles vs authorities

TermWhat it isExample string
AuthorityA raw permission string, used as-isorders:read, ROLE_ADMIN
RoleA coarse-grained authority, conventionally prefixed ROLE_ROLE_USER, ROLE_ADMIN

Roles are best for broad categories of users (USER, ADMIN, MANAGER); fine-grained authorities suit specific permissions (invoice:approve, report:export). They coexist freely on the same principal.

The ROLE_ prefix

This trips up almost everyone. Spring Security’s role helpers automatically add or expect the ROLE_ prefix:

// When you GRANT a role, store it WITH the prefix:
new SimpleGrantedAuthority("ROLE_ADMIN");
User.builder().roles("ADMIN");           // roles() adds ROLE_ → ROLE_ADMIN

// When you CHECK a role, pass it WITHOUT the prefix:
.requestMatchers("/admin/**").hasRole("ADMIN");   // matches ROLE_ADMIN

Authorities, by contrast, are matched verbatim — no prefix is added on either side:

new SimpleGrantedAuthority("orders:read");
.requestMatchers("/orders/**").hasAuthority("orders:read");  // exact match

Warning: hasRole("ROLE_ADMIN") will never match, because the framework prepends ROLE_ and looks for ROLE_ROLE_ADMIN. Pass hasRole("ADMIN").

Request-level rules

The HTTP DSL maps URL patterns to access expressions. Rules are evaluated in order, first match wins.

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/public/**").permitAll()
        .requestMatchers(HttpMethod.GET,  "/api/products/**").hasRole("USER")
        .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
        .requestMatchers("/api/reports/**").hasAuthority("report:export")
        .requestMatchers("/api/staff/**").hasAnyRole("MANAGER", "ADMIN")
        .anyRequest().authenticated());
    return http.build();
}

The available access methods:

MethodAllows when the user has
hasRole("X")authority ROLE_X
hasAnyRole("X","Y")ROLE_X or ROLE_Y
hasAuthority("p")authority p (verbatim)
hasAnyAuthority("p","q")p or q
authenticated()any authenticated identity
permitAll() / denyAll()always / never

Testing the rules

As a USER hitting an admin-only POST:

curl -u alice:password -X POST http://localhost:8080/api/products \
  -H 'Content-Type: application/json' -d '{"name":"Widget"}'

Output:

HTTP/1.1 403 Forbidden
{"status":403,"error":"Forbidden","path":"/api/products"}

A 403 Forbidden means authenticated but not authorized — distinct from 401 Unauthorized, which means not authenticated.

Role hierarchy

Often a higher role should implicitly include lower ones — an ADMIN should pass any USER check without granting both authorities to every admin. Configure a RoleHierarchy bean:

import org.springframework.security.access.hierarchicalroles.*;

@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.withDefaultRolePrefix()
            .role("ADMIN").implies("MANAGER")
            .role("MANAGER").implies("USER")
            .build();
}

Now an ADMIN automatically satisfies hasRole("MANAGER") and hasRole("USER"). To make method security honor the hierarchy too, expose it through a MethodSecurityExpressionHandler:

@Bean
static MethodSecurityExpressionHandler methodExpressionHandler(RoleHierarchy roleHierarchy) {
    DefaultMethodSecurityExpressionHandler handler =
            new DefaultMethodSecurityExpressionHandler();
    handler.setRoleHierarchy(roleHierarchy);
    return handler;
}

Tip: A role hierarchy keeps authority assignment simple — grant a user the single most specific role they need and let the hierarchy imply the rest.

Reading authorities at runtime

@GetMapping("/whoami")
public Map<String, Object> whoami(Authentication auth) {
    return Map.of(
        "name", auth.getName(),
        "authorities", auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList());
}

Output:

{ "name": "bob", "authorities": ["ROLE_ADMIN", "ROLE_USER"] }

Pitfalls

  • Mixing prefixed and unprefixed roles — keep grants prefixed (ROLE_) and checks unprefixed.
  • Ordering rules wrong — a broad permitAll() placed early opens paths you meant to lock.
  • Expecting hasAuthority("ADMIN") to match a role — it won’t; use hasRole("ADMIN") or grant "ADMIN" as a bare authority.
Last updated June 13, 2026
Was this helpful?