Enhancements of Authorization Lab
Last updated
Last updated
Learn about the latest innovations in Authorization features of Spring Security. Details on Spring Security Authorization can be found in the .
The provided application is a simple Spring Boot application that demonstrates the latest Authorization features of Spring Security. The use case is a simple online banking application with bank accounts owned by different users.
The application uses a simple in-memory database (H2) to store the bank accounts.
The BankAccount
entity class extends the AbstractPersistable
class, which provides a simple way to manage the entity's ID and version.
💡 if Spring Security 6.3 is used: As we will use Jackson (for JSON), we need to set the annotation
@JsonSerialize(as = BankAccount.class)
on top of the class. This is due to how Jackson works with CGLIB proxies (Spring Security needs proxies to make enhanced authorizations work). Otherwise, this may result in a serialization error like the following:com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Direct self-reference leading to cycle
The application provides a simple REST API that allows users to create, view and update their bank accounts.
GET /api/accounts
Administrative Endpoint to get all existing accounts
GET /api/accounts/{id}
Retrieve a single account by its unique identifier
POST /api/accounts
Create a new account for currently authenticated user
POST /api/accounts/{id}
Increase the account balance by given amount
Here is the code for the REST API:
The Rest API delegates all calls to the BankAccountService
class, which is responsible for the business logic and bridge to data access.
The BankAccountService
class is annotated with @Transactional(readOnly = true)
to indicate that all methods are read-only by default. The save
and update
methods are annotated with @Transactional
to indicate that they are write operations.
Finally, we approach the data access layer.
The BankAccountRepository
class is a simple Spring Data JPA repository that provides CRUD operations for the BankAccount
entity.
Please notice the updateBankAccount
method. It contains a custom query that updates the balance of a bank account only if the owner of the account matches the currently authenticated user.
This is basically done by using the ?#{principal?.username}
expression in the query.
This expression extension is enabled through the corresponding Spring Data Security extension defined in class WebSecurityConfiguration
.
And it also requires an additional dependency in the pom.xml
that is already included in the provided application:
Let's start the application with:
and test the API with the following credentials:
user
secret
USER
admin
secret
USER, ADMIN
accountant
secret
USER, ACCOUNTANT
To test the application, you can use one of the provided HTTP client files in the requests
folder of the module:
api-call.http
: The IntelliJ Http client
What do you think about the responses of the different API calls? It looks like there are issues with the authorization of the API calls. So there is work to do!
But first let's have a look at the (new and enhanced) method authorization features in Spring Security.
An application can use Spring Security to secure the REST API and restrict access to the method (service) layer.
This is basically done by using annotations like @PreAuthorize
, @PostAuthorize
and @AuthorizeReturnObject
on the service methods or domain objects.
Summary of Annotations
@PreAuthorize
Before method call
Guard method based on roles or parameters
@PostAuthorize
After method call
Guard method based on return value
@PreFilter
Before method call
Filter input collections
@PostFilter
After method call
Filter returned collections
@AuthorizeReturnObject
After return (6.3+)
Object-level security on returned object
@HandleAuthorizationDenied
Interception (6.3+)
Custom handling of authorization failures
✅ @PreAuthorize
Runs before the method and prevents execution if the expression is false.
🧠Common use cases:
Role-based access
Ownership checks: @PreAuthorize("#userId == authentication.name")
✅ @PostAuthorize
Runs after the method and prevents return if the expression is false. This allows checking the returned object.
🧠Use this when you need to inspect the returned domain object to enforce access control.
✅ @PreFilter
Filters collection input parameters before the method executes.
🧠filterObject is a special variable that refers to each item in the collection.
✅ @PostFilter
Filters the collection result after method execution.
🧠This is useful when returning a list of domain objects, and you want to hide some from the caller.
✅ @AuthorizeReturnObject (Spring Security 6.3+)
A new alternative to @PostAuthorize, more structured and composable
This can be combined with @PreAuthorize on parts of the domain object to enforce a bit more fine-grained object security.
✅ @HandleAuthorizationDenied (Spring Security 6.3+)
This allows you to intercept and customize the behavior when an authorization check fails at a method level
(e.g., in @PreAuthorize
, @AuthorizeReturnObject
, etc.).
🧠This is useful when you want to mask or log the failure instead of throwing an exception.
In the next step we will now implement the new and enhanced method authorization features in the provided application.
Now it is the time to add the missing authorization to the application.
To make method-level authorization generally work, we need to add the @EnableMethodSecurity
annotation to the WebSecurityConfiguration
class. If you want to use Annotation Parameters, you have to opt in Templating Meta-Annotation Expressions, a new feature introduced in Spring Security 6.3. For this we have to publish an AnnotationTemplateExpressionDefaults
bean.
The PreGetBankAccounts
and PreWriteBankAccount
annotations are custom security annotations that are used to restrict access to the methods based on the user's role and ownership of the bank account. Please create these two classes in the security
package.
PreGetBankAccounts:
PreWriteBankAccount:
Let us add these annotations to the BankAccountService
class:
✅ Explanation:
@PreGetBankAccounts(role = "ADMIN") is a custom-composed parameterized annotation built on top of @PreAuthorize
When findAll()
is called, Spring Security will:
Resolve the annotation parameter role = "ADMIN"
Evaluate the SpEL expression hasRole() → becomes hasRole('ADMIN')
Check if the currently authenticated user has the authority ROLE_ADMIN
If the check passes, proceed to call findAll()
Otherwise, throw an AccessDeniedException
@PreWriteBankAccount is a custom-composed parameterized annotation built on top of @PreAuthorize
Spring sees @PreWriteBankAccount("#toSave")
The expression #toSave is passed to the account attribute of the annotation
Inside the annotation, {account} is replaced with #toSave
The resulting @PreAuthorize becomes:@PreAuthorize("#toSave?.owner == authentication?.name")
At runtime, before executing save()
, Spring evaluates whether: toSave.owner == currentlyAuthenticatedUser.name
Restart the application and test the API again (with the previously introduced credentials above)
Now most Authorizations should now work as expected.
Since Spring Security 6.3+ it's also supported to implement a handler to customize authorization errors.
Let's see how this works by defining a new class called MaskMethodAuthorizationDeniedHandler
, create this in the security
package:
This class implements the MethodAuthorizationDeniedHandler
interface and provides a custom implementation for handling authorization failures. In this case, it simply returns a masked value (*****). We will later add this to the BankAccount
entity class.
Since Spring Security 6.3+ also supports wrapping any object that is annotated its method security annotations. The simplest way to achieve this is to mark any method that returns the object you wish to authorize with the @AuthorizeReturnObject
annotation.
The @AuthorizeReturnObject
annotation instructs Spring Security to check the returned object against the security expression. This is a new feature introduced in Spring Security 6.3 as well and allows you to restrict access to the returned domain object based on the user's role and the returned object's properties.
We want to try this now on our domain entity.
Here is what the BankAccount
entity class looks like after adding the @PreAuthorize
and @HandleAuthorizationDenied
annotation:
@HandleAuthorizationDenied
now also references the previously created MaskMethodAuthorizationDeniedHandler
class, which is a custom authorization-denied handler that is used to mask the account number if the user is not authorized to access it.
Finally, we need to activate the validation of authorization checks on our domain object. We achieve this by adding another customized annotation.
The PostReadBankAccount
annotation is a custom security annotation that is used to restrict access to the method based on the user's role and the returned object. Create a new class called PostReadBankAccount
in the security
package:
We also need to add this annotation to the findById
method in the BankAccountService
class:
Restart the application by running the BankAccountApplication
class.
You can do this by right-clicking on the class and selecting Run or by using the command line:
Re-test the API.
All authorizations should work now as expected.
If the user has restricted access rights, the account number is being masked out.
✅ That's it! You have successfully implemented the really important authorization part in your Spring Boot application.
Let's now move on to the next section, where we will look into the OAuth2 Token Exchange.
This implements a well-known security principle called
This is the BankAccount
entity class that represents a bank account in the application (see if you are not familiar with JPA and/or Spring Data JPA):
api-call.httpie
: The command line client
api-call.curl
: The command line client
At first let's create some custom Authorization annotations to show the feature introduced in Spring Security 6.3. Later we will add these to the BankAccountService
class.