Authorisation with Spring Security — Part 1
One of our most important concerns in Harbor Lab is protecting the data of our users from unauthorised access. As such, a proper authorisation mechanism that is easily configurable and maintainable is a very important part of our platform. Since our back end services are built with Java and Spring Boot the logical choice for our authorisation is of course Spring Security.
Authorisation in Spring Security
Spring Security defines the notion of a principal, which is the currently logged in user. When a user authenticates successfully, the principal is stored in Spring’s security context, which is thread-bound, thus making it available to the rest of our service. An important nuance here is that the security context does not propagate by default in child threads, although this is not something that needs to worry us for now.
Providing the principal to Spring falls under the authentication umbrella and is perhaps another post for another day. What we care about in terms of authorisation though, is that the principal is an implementation of the UserDetails interface, meaning that it has, among others, the following method.
Collection<? extends GrantedAuthority> getAuthorities();
The authorities, which each application can define differently based on its business needs, are a set of privileges or access rights a user has. For us, authorities give users access to specific parts of the application. It is also important to note here that we can add any other attributes or methods we need to our principal object, which we will be able to use in our authorisation checks as we will see below.
In order to provide authorisation functionality, Spring Security uses the AccessDecisionManager
, which is responsible for delegating authorisation decisions to one or more AccessDecisionVoter
instances. Each voter provides a decision on whether the principal is authorised or not, based on different rules or configurations, and the manager is responsible for combining the decisions of the voters in order to reach the final consensus. Spring’s default AccessDecisionManager
is affirmative based, meaning that it returns true
if at least one voter returns true
. More details on Spring Security’s architecture are of course available in the official Spring documentation.
Configuration
Authorisation is usually part of a service’s business logic, meaning that it makes sense to apply it on the service layer of the application, ie. within our Spring service. Ideally, we need an easy and maintainable way to define the authorisation rules that apply to each service method, which is where an annotation could come in very handy.
Enter Spring global method security. Global method security allows us to use annotations in order to apply authorisation rules on methods in any layer of a Spring application. All it needs to be enabled is adding the @EnableGlobalMethodSecurity
annotation on our security configuration class. This annotation also has a few interesting attributes we can use:
- prePostEnabled: Set to true in order to enable
@PreAuthorize
and@PostAuthorize
annotations, which we will see below in more detail. - securedEnabled: Set to true in order to enable Spring’s
@Secured
annotation, which can be used to specify a list of user roles that should have access to the annotated method. - jsr250Enabled: Set to true in order to enable the JSR-250
@RolesAllowed
annotation, which can be used instead of Spring’s@Secured
.
We can therefore have something like the following on our security configuration class.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration
Securing service methods
The first thing we want to do is limit the access to our service methods to users having specific roles. Let’s consider as an example the following code fragment:
@Service
public class ContentService {
public Article retrieve(long articleId) {
// Returns an article
}
public Article create(String title, String content) {
// Creates a new article
}
}
Assuming we have two types of users, those with the READER
role and those with the WRITER
role, we want to restrict access to the create
method only to writers. This can easily be done as follows.
@Service
public class ContentService {
@Secured({"READER", "WRITER"})
public Article retrieve(long articleId) {
// Returns an article
}
@Secured("ROLE_WRITER")
public Article create(String title, String content) {
// Creates a new article
}
}
With these annotations Spring will check the user’s roles before calling the method and only proceed with the method call if the user has one of the specified roles, otherwise an exception will be thrown.
Roles vs Authorities
So far we conveniently left out how roles are defined and where Spring finds them in order to check them against those included in the @Secured
annotation. Spring Security expects the principal we mentioned above to have a list of granted authorities. These authorities are then used by the RoleVoter
, which gets all authorities starting with the prefix ROLE_
and checks them against the roles defined in the @Secured
annotation. This means that in the example above Spring will check for the authorities ROLE_WRITER
and ROLE_READER
.
This prefix can change if needed by modifying the Spring configuration as follows.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("PREFIX_");
}
}
Authorisation based on authorities
In our case, we have decided to use authorities instead of roles since they are a better fit for our authorisation model. This means that we cannot use the @Secured
annotation to check for them, but we can instead use a @PreAuthorize
annotation along with the hasAuthority
method as follows.
@PreAuthorize("hasAuthority('ARTICLE_VIEW')")
public Article retrieve(long articleId) {
// Returns an article
}
Similarly to how @Secured
works, @PreAuthorize
runs before the method is entered and allows access to the method only if the result of the evaluation is true
. This means that in the case above only users who have the specified authority will be allowed to access the business functionality implemented by this method.
Come back for Part 2 of this article, where we will explore more complex authorisation rules, using SPEL and @PreAuthorize
/@PostAuthorize
annotations, as well as how to make sure that our transactions roll back when authorisation checks fail.