Authorisation with Spring Security — Part 2
In Part 1 of this article we looked into how Spring Security implements authorisation and how to set it up to allow using annotations to secure service methods based on a user’s roles or authorities. What happens though when our authorisation logic depends on more complex rules as is the case for us?
Using @PreAuthorize / @PostAuthorize and SPEL
As we saw before, Spring defines the @PreAuthorize
annotation, which allows us to perform authorisation checks before a method is entered. We already showed an example where we are using hasAuthority
within @PreAuthorize
to verify the user has the required authority, we can however use much more complex expressions if needed. Specifically, Spring allows us to use any expression written in Spring Expression Language (SPEL) as the argument to @PreAuthorize
, while also giving us access to the method arguments.
Remembering that we can define the class that we use for the principal object to contain any other user information we need, we can therefore write something like the following, where the hash prefix (#
) gives us access to the method arguments.
@PreAuthorize("hasAuthority('DATA_VIEW') and authorisation.principal.getCompanyId() == #companyId")
public Data getCompanyData(long companyId) {
...
}
This is of course fine when we have all the information we need to evaluate the user’s access before we enter the method, but this is often not the case. In the cases therefore where we need the data calculated within the method to evaluate user access we can use the @PostAuthorize
annotation, which is evaluated after the method execution and also has access to the method’s return value. We can of course also combine the two annotations as we can see in the following fragment.
@PreAuthorize("hasAuthority('DATA_VIEW'))
@PostAuthorize("returnObject.getCompanyId() == authorisation.principal.getCompanyId()")
public Data getData() {
...
}
Similarly to the @PostAuthorize
annotation we can also use @PostFilter
in case we need to filter a collection returned by a method to only contain the data the user should have access to. In this case we can reference the returned collection using the returnObject
keyword within our SPEL.
Using Beans to simplify authorisation expressions
The annotations offered by Spring Security along with SPEL is a great way to define authorisation rules, as they are very expressive and not intrusive in our code, a problem we faced however was that we often needed to use complex expressions for our authorisation rules. Defining these expressions directly in a @PreAuthorize
or @PostAuthorize
annotation made them hard to read and more important, impossible to reuse or unit tests.
In order to overcome this problem we decided to define a set of authorisation beans which implement our authorisation rules. These beans look something like the following.
@Component
public class AdminAuthorization extends AbstractAuthorization {
public boolean isAdmin() {
return principal.isAdmin();
}
}public abstract class AbstractAuthorization {
protected UserProfile getPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (UserProfile) authentication.getPrincipal();
}
}
We can then reference our authorisation beans within the authorisation annotations, as any Spring bean can be referenced within a SPEL expression using its name with the @
prefix, as follows.
@PreAuthorize("@adminAuthorization.isAdmin")
This way, our authorisation rules are easily reusable and a breeze to unit test (yes, testing is important).
An important detail here is that bean lookup in method security SPEL expressions is not enabled by default, as the expression handler does not have access to the application context. We therefore need this extra bit in our Spring configuration class to inject the application context in the MethodSecurityExpressionHandler
and thus make our beans discoverable in our @PreAuthorize
/ @PostAuthorize
annotations.
@Configuration
public class SecurityConfiguration extends GlobalMethodSecurityConfiguration { @Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
}
Authorisation checks and transaction rollback
And just when you though we are done and all is well in the authorisation world, there is an important detail missing. a @PostAuthorize
annotation is completely fine when needing to secure a method which is only reading data, but what happens with methods which are writing data and which, for whichever reason, cannot be secured with a @PreAuthorize
annotation? In order to secure these methods using @PostAuthorize
we need to make sure that the authorisation exception thrown if the @PostAuthorize
check fails will rollback the transaction resulting in no data being written in our DB.
As it turns out, assuming we have a @PostAuthorize
annotation in a service which is also annotated with @Transactional
, the order in which the authorisation checks will be performed in relation to the transaction being committed depends on the order of the @EnableGlobalMethodSecurity
configuration in relation to the @EnableTransactionManagement
configuration. By default both annotations have the maximum allowed order, which means that all we need to do to make sure that our authorisation annotations are evaluated within the transaction boundaries and therefore roll back the transaction in case of failure is to move @EnableTransactionManagement
a bit higher in the order list.
@Configuration
@EnableTransactionManagement(order = 0)
public class DataSourceConfiguration {
}