
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: January 23, 2024
Simply put, Spring Security supports authorization semantics at the method level.
Typically, we could secure our service layer by, for example, restricting which roles are able to execute a particular method — and test it using dedicated method-level security test support.
In this tutorial, we’re going to review the use of some security annotations. Then we’ll focus on testing our method security with different strategies.
First, to use Spring Method Security, we need to add the spring-security-config dependency:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
We can find its latest version on Maven Central.
If we want to use Spring Boot, we can use the spring-boot-starter-security dependency, which includes spring-security-config:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Again, the latest version can be found on Maven Central.
Next, we need to enable global Method Security:
@Configuration
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class MethodSecurityConfig
extends GlobalMethodSecurityConfiguration {
}
We’ll explore more about these annotations in the next section.
The @Secured annotation is used to specify a list of roles on a method. So, a user only can access that method if she has at least one of the specified roles.
Let’s define a getUsername method:
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
Here the @Secured(“ROLE_VIEWER”) annotation defines that only users who have the role ROLE_VIEWER are able to execute the getUsername method.
Besides, we can define a list of roles in a @Secured annotation:
@Secured({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername(String username) {
return userRoleRepository.isValidUsername(username);
}
In this case, the configuration states that if a user has either ROLE_VIEWER or ROLE_EDITOR, that user can invoke the isValidUsername method.
The @Secured annotation doesn’t support Spring Expression Language (SpEL).
The @RolesAllowed annotation is the JSR-250’s equivalent annotation of the @Secured annotation.
Basically, we can use the @RolesAllowed annotation in a similar way as @Secured.
This way, we could redefine getUsername and isValidUsername methods:
@RolesAllowed("ROLE_VIEWER")
public String getUsername2() {
//...
}
@RolesAllowed({ "ROLE_VIEWER", "ROLE_EDITOR" })
public boolean isValidUsername2(String username) {
//...
}
Similarly, only the user who has role ROLE_VIEWER can execute getUsername2.
Again, a user is able to invoke isValidUsername2 only if she has at least one of the ROLE_VIEWER or ROLER_EDITOR roles.
Both @PreAuthorize and @PostAuthorize annotations provide expression-based access control. So, predicates can be written using SpEL (Spring Expression Language).
The @PreAuthorize annotation checks the given expression before entering the method, whereas the @PostAuthorize annotation verifies it after the execution of the method and could alter the result.
Now let’s declare a getUsernameInUpperCase method as below:
@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
return getUsername().toUpperCase();
}
The @PreAuthorize(“hasRole(‘ROLE_VIEWER’)”) has the same meaning as @Secured(“ROLE_VIEWER”), which we used in the previous section. Feel free to discover more security expressions details in previous articles.
Consequently, the annotation @Secured({“ROLE_VIEWER”,”ROLE_EDITOR”}) can be replaced with @PreAuthorize(“hasRole(‘ROLE_VIEWER’) or hasRole(‘ROLE_EDITOR’)”):
@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
public boolean isValidUsername3(String username) {
//...
}
Moreover, we can actually use the method argument as part of the expression:
@PreAuthorize("#username == authentication.principal.username")
public String getMyRoles(String username) {
//...
}
Here a user can invoke the getMyRoles method only if the value of the argument username is the same as current principal’s username.
It’s worth noting that @PreAuthorize expressions can be replaced by @PostAuthorize ones.
Let’s rewrite getMyRoles:
@PostAuthorize("#username == authentication.principal.username")
public String getMyRoles2(String username) {
//...
}
In the previous example, however, the authorization would get delayed after the execution of the target method.
Additionally, the @PostAuthorize annotation provides the ability to access the method result:
@PostAuthorize
("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
Here the loadUserDetail method would only execute successfully if the username of the returned CustomUser is equal to the current authentication principal’s nickname.
In this section, we mostly use simple Spring expressions. For more complex scenarios, we could create custom security expressions.
Spring Security provides the @PreFilter annotation to filter a collection argument before executing the method:
@PreFilter("filterObject != authentication.principal.username")
public String joinUsernames(List<String> usernames) {
return usernames.stream().collect(Collectors.joining(";"));
}
In this example, we’re joining all usernames except for the one that is authenticated.
Here, in our expression, we use the name filterObject to represent the current object in the collection.
However, if the method has more than one argument that is a collection type, we need to use the filterTarget property to specify which argument we want to filter:
@PreFilter
(value = "filterObject != authentication.principal.username",
filterTarget = "usernames")
public String joinUsernamesAndRoles(
List<String> usernames, List<String> roles) {
return usernames.stream().collect(Collectors.joining(";"))
+ ":" + roles.stream().collect(Collectors.joining(";"));
}
Additionally, we can also filter the returned collection of a method by using the @PostFilter annotation:
@PostFilter("filterObject != authentication.principal.username")
public List<String> getAllUsernamesExceptCurrent() {
return userRoleRepository.getAllUsernames();
}
In this case, the name filterObject refers to the current object in the returned collection.
With that configuration, Spring Security will iterate through the returned list and remove any value matching the principal’s username.
Our Spring Security – @PreFilter and @PostFilter article describes both annotations in greater detail.
We typically find ourselves in a situation where we protect different methods using the same security configuration.
In this case, we can define a security meta-annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('VIEWER')")
public @interface IsViewer {
}
Next, we can directly use the @IsViewer annotation to secure our method:
@IsViewer
public String getUsername4() {
//...
}
Security meta-annotations are a great idea because they add more semantics and decouple our business logic from the security framework.
If we find ourselves using the same security annotation for every method within one class, we can consider putting that annotation at class level:
@Service
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class SystemService {
public String getSystemYear(){
//...
}
public String getSystemDate(){
//...
}
}
In above example, the security rule hasRole(‘ROLE_ADMIN’) will be applied to both getSystemYear and getSystemDate methods.
We can also use multiple security annotations on one method:
@PreAuthorize("#username == authentication.principal.username")
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser securedLoadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
This way, Spring will verify authorization both before and after the execution of the securedLoadUserDetail method.
There are two points we’d like to recall regarding method security:
To test Spring Security with JUnit, we need the spring-security-test dependency:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
</dependency>
We don’t need to specify the dependency version because we’re using the Spring Boot plugin. We can find the latest version of this dependency on Maven Central.
Next, let’s configure a simple Spring Integration test by specifying the runner and the ApplicationContext configuration:
@RunWith(SpringRunner.class)
@ContextConfiguration
public class MethodSecurityIntegrationTest {
// ...
}
Now that our configuration is ready, let’s try to test our getUsername method that we secured with the @Secured(“ROLE_VIEWER”) annotation:
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}
Since we use the @Secured annotation here, it requires a user to be authenticated to invoke the method. Otherwise, we’ll get an AuthenticationCredentialsNotFoundException.
So, we need to provide a user to test our secured method.
To achieve this, we decorate the test method with @WithMockUser and provide a user and roles:
@Test
@WithMockUser(username = "john", roles = { "VIEWER" })
public void givenRoleViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();
assertEquals("john", userName);
}
We’ve provided an authenticated user whose username is john and whose role is ROLE_VIEWER. If we don’t specify the username or role, the default username is user and default role is ROLE_USER.
Note that it isn’t necessary to add the ROLE_ prefix here because Spring Security will add that prefix automatically.
If we don’t want to have that prefix, we can consider using authority instead of role.
For example, let’s declare a getUsernameInLowerCase method:
@PreAuthorize("hasAuthority('SYS_ADMIN')")
public String getUsernameLC(){
return getUsername().toLowerCase();
}
We could test that using authorities:
@Test
@WithMockUser(username = "JOHN", authorities = { "SYS_ADMIN" })
public void givenAuthoritySysAdmin_whenCallGetUsernameLC_thenReturnUsername() {
String username = userRoleService.getUsernameInLowerCase();
assertEquals("john", username);
}
Conveniently, if we want to use the same user for many test cases, we can declare the @WithMockUser annotation at test class:
@RunWith(SpringRunner.class)
@ContextConfiguration
@WithMockUser(username = "john", roles = { "VIEWER" })
public class MockUserAtClassLevelIntegrationTest {
//...
}
If we wanted to run our test as an anonymous user, we could use the @WithAnonymousUser annotation:
@Test(expected = AccessDeniedException.class)
@WithAnonymousUser
public void givenAnomynousUser_whenCallGetUsername_thenAccessDenied() {
userRoleService.getUsername();
}
In the example above, we expect an AccessDeniedException because the anonymous user isn’t granted the role ROLE_VIEWER or the authority SYS_ADMIN.
For most applications, it’s common to use a custom class as authentication principal. In this case, the custom class needs to implement the org.springframework.security.core.userdetails.UserDetails interface.
In this article, we declare a CustomUser class that extends the existing implementation of UserDetails, which is org.springframework.security.core.userdetails.User:
public class CustomUser extends User {
private String nickName;
// getter and setter
}
Let’s look back at the example with the @PostAuthorize annotation in Section 3:
@PostAuthorize("returnObject.username == authentication.principal.nickName")
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}
In this case, the method would only execute successfully if the username of the returned CustomUser is equal to the current authentication principal’s nickname.
If we wanted to test that method, we could provide an implementation of UserDetailsService that could load our CustomUser based on the username:
@Test
@WithUserDetails(
value = "john",
userDetailsServiceBeanName = "userDetailService")
public void whenJohn_callLoadUserDetail_thenOK() {
CustomUser user = userService.loadUserDetail("jane");
assertEquals("jane", user.getNickName());
}
Here the @WithUserDetails annotation states that we’ll use a UserDetailsService to initialize our authenticated user. The service is referred by the userDetailsServiceBeanName property. This UserDetailsService might be a real implementation or a fake for testing purposes.
Additionally, the service will use the value of the property value as the username to load UserDetails.
Conveniently, we can also decorate with a @WithUserDetails annotation at the class level, similarly to what we did with the @WithMockUser annotation.
We often find ourselves reusing the same user/roles over and over again in various tests.
For these situations, it’s convenient to create a meta-annotation.
Looking again at the previous example @WithMockUser(username=”john”, roles={“VIEWER”}), we can declare a meta-annotation:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value = "john", roles = "VIEWER")
public @interface WithMockJohnViewer { }
Then we can simply use @WithMockJohnViewer in our test:
@Test
@WithMockJohnViewer
public void givenMockedJohnViewer_whenCallGetUsername_thenReturnUsername() {
String userName = userRoleService.getUsername();
assertEquals("john", userName);
}
Likewise, we can use meta-annotations to create domain-specific users using @WithUserDetails.
In this article, we’ve explored various options for using Method Security in Spring Security.
We have also gone through a few techniques to easily test method security and learned how to reuse mocked users in different tests.