Role-Based Authorization in Spring Boot using @PreAuthorize and @PostAuthorize

Role-Based Authorization in Spring Boot using @PreAuthorize and @PostAuthorize

 

In modern Spring Boot applications, security is not just about authentication (who you are) but also about authorization (what you can do).

Earlier, we handled authorization at the Security Filter layer. That approach works fine for small systems, but in large-scale applications with hundreds of APIs, managing all roles and permissions in filters becomes complex and hard to maintain.

That’s where Annotation-based Role Authorization comes into the picture.


Why Annotation-Based Authorization?

Instead of controlling access in filters, we can declare security rules directly on controller methods using annotations like:

  • @PreAuthorize
  • @PostAuthorize

This makes the system:

  • More readable
  • More maintainable
  • More scalable

Enabling Method-Level Security

Before using these annotations, we must enable them in our Security Configuration:

@EnableMethodSecurity
public class SecurityConfig {
}

⚠️ Without this, @PreAuthorize and @PostAuthorize will be ignored.


Creating Sample Controllers

Let’s assume we have two controllers:

  • OrderController
  • SalesController

Each controller exposes APIs like:

  • /orders
  • /sales

We also create users dynamically with roles and authorities.

Example user:

Username: user1
Roles: ROLE_USER
Authorities: ORDER_READ

This user:

  • Can access Order APIs
  • Cannot access Sales APIs

Using @PreAuthorize

Example

@PreAuthorize("hasRole('USER') and hasAuthority('ORDER_READ')")
@GetMapping("/orders")
public String fetchOrders() {
    return "Order Details";
}

This means:

  • User must have role ROLE_USER
  • And must have authority ORDER_READ

If both conditions pass → API executes
Else → 403 Forbidden


Using @PostAuthorize

@PostAuthorize runs after method execution but before returning the response.

Example

@PostAuthorize("returnObject.id == authentication.principal.id")
@GetMapping("/users/{id}")
public User fetchUserDetails(@PathVariable Long id) {
    return userService.fetchUserDetails(id);
}

Here:

  • API executes first
  • Then Spring checks if returned user’s ID matches logged-in user
  • If not → response is blocked

Difference Between hasRole and hasAuthority

Spring treats both similarly, with one key difference:

Method Behavior
hasRole("USER") Internally checks ROLE_USER
hasAuthority("ORDER_READ") Checks exact value

So:

hasRole("USER")  → ROLE_USER
hasAuthority("USER") → USER

This logic is implemented in SecurityExpressionRoot.java.


How Does Spring Enforce This Internally?

This works using Interceptors, not Filters.

For @PreAuthorize

Spring uses:

AuthorizationManagerBeforeMethodInterceptor

For @PostAuthorize

Spring uses:

AuthorizationManagerAfterMethodInterceptor

These interceptors run before or after method execution.


High-Level Internal Flow

Image

Image

Image

Image

  1. Reads expression from annotation
    @PreAuthorize("hasRole('USER') and hasAuthority('ORDER_READ')")
    
  2. Parses using:
    SpelExpressionParser
    
  3. Converts expression into:
    AST (Abstract Syntax Tree)
    
  4. Resolves:
    • authentication object
    • roles
    • authorities
  5. Evaluates result:
    • If true → method executes
    • If false → access denied

SpEL (Spring Expression Language)

@PreAuthorize and @PostAuthorize both use SpEL.

Logical Operators

Operator Meaning Example
and Logical AND hasRole('ADMIN') and hasAuthority('READ')
or Logical OR hasRole('ADMIN') or hasRole('USER')
not Logical NOT not hasRole('ADMIN')
! Logical NOT !hasAuthority('DELETE')

Relational Operators

Operator Meaning Example
== Equal #value == 15
!= Not equal #value != 15
< Less than #value < 100
> Greater than #value > 100
<= Less than or equal #value <= 15
>= Greater than or equal #value >= 90

Real-Time Use Case Example

Scenario

Two users:

  • a_user → id = 101
  • b_user → id = 102

API:

@PostAuthorize("#id == authentication.principal.id")
@GetMapping("/users/{id}")
public User fetchUser(@PathVariable Long id) {
    return userService.fetchUser(id);
}

Case 1:

b_user calls /users/101

  • Method returns user 101
  • PostAuthorize checks:
    101 == 102 → false
    
  • ❌ Access denied

Case 2:

a_user calls /users/101

  • Method returns user 101
  • PostAuthorize checks:
    101 == 101 → true
    
  • ✅ Response sent

Filters vs Interceptors (Quick Difference)

Filters Interceptors
Works at servlet layer Works at Spring method level
Executes before controller Executes around method
Good for authentication Perfect for authorization
Hard to scale Highly scalable

Why This Approach Is Best for Large Systems?

In enterprise applications:

  • 100s of APIs
  • 1000s of permissions

Managing this in filters becomes nightmare.

With annotations:

  • Each API declares its own security
  • Business logic and security stay together
  • Easy auditing and maintenance

Interview Questions & Answers

Q1. What is @PreAuthorize?

Answer:
It performs authorization before method execution using SpEL expressions.


Q2. What is @PostAuthorize?

Answer:
It performs authorization after method execution but before returning response.


Q3. What is SpEL?

Answer:
Spring Expression Language used to write dynamic authorization rules.


Q4. Difference between hasRole and hasAuthority?

Answer:
hasRole automatically adds ROLE_ prefix, hasAuthority does not.


Q5. Which class intercepts @PreAuthorize?

Answer:
AuthorizationManagerBeforeMethodInterceptor


Q6. Which class intercepts @PostAuthorize?

Answer:
AuthorizationManagerAfterMethodInterceptor


Q7. Why not use only Filters?

Answer:
Filters are hard to manage in large systems and cannot inspect method return values.


Final Thoughts

Annotation-based authorization is:

  • Cleaner
  • More expressive
  • More powerful
  • Enterprise-ready

If you’re building microservices or large Spring Boot systems, this is the industry standard way to handle authorization.

Leave a Reply