Spring Security Expressions provides a powerful expression language to define fine-grained access control rules based on various factors such as user roles, authorities, authentication details, and more. In this article, we’ll explore practical examples of using Spring Security expressions, configurations, and working code examples.

NOTE: WebSecurityConfigurerAdapter is not available in Spring Security 6.x. Read the article: Spring Security without the WebSecurityConfigurerAdapter

1. Setting up Spring Security Dependencies

Before we dive into the examples, let’s set up the required dependencies for Spring Security. If you’re using Spring Boot, you can simply add the spring-boot-starter-security dependency to your pom.xml file:

XML
...
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
...

If you’re using the Spring Security Web module directly, you’ll need to include the spring-security-web dependency:

XML
...
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>6.2.4</version> <!-- latest version -->
</dependency>
...

2. Configuring Spring Security

Next, let’s configure Spring Security in our application. Here’s an example SecurityConfig class:

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
@EnableWebSecurity //if required
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();

        UserDetails admin = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

In this configuration, we’re defining two in-memory users (user and admin) with different roles. We’re also enabling method-level security with @EnableMethodSecurity(prePostEnabled = true), which allows us to use Spring Security expressions in our method annotations.

XML configuration: if your project is still using XML based Configurations

XML
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/security
                                 http://www.springframework.org/schema/security/spring-security.xsd">

    <global-method-security pre-post-annotations="enabled" />
    <!-- Other security configurations -->

</beans:beans>

3. Web Security Expressions

Spring Security provides a set of built-in expressions that you can use to define access control rules in your web application. Here are some practical examples of Spring Security Expressions:

3.1. Role-based Access Control – hasRole and hasAnyRole

You can use the hasRole and hasAnyRole expressions to control access based on user roles.

Role-based access control is a widely used approach in which permissions and access rights are assigned to roles, and users are assigned one or more roles. The roles determine the set of actions and resources a user is allowed to access or perform within the application.

In Spring Security, roles are typically prefixed with the string “ROLE_” (e.g., “ROLE_ADMIN”, “ROLE_USER”). This is a convention, not a requirement, but it helps distinguish roles from other types of authorities.

Java
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeRequests()
                .requestMatchers("/auth/admin/**").hasRole("ADMIN")
                .requestMatchers("/auth/**").hasAnyRole("ADMIN", "USER")
            .and()
                .formLogin(Customizer.withDefaults())
            .build();
    }

In the example above:

  • URLs starting with /auth/admin/ require the ADMIN role.
  • URLs starting with /auth/ are accessible to users with either the ADMIN or USER role.

XML Config: The equivalent XML configuration:

XML
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans
                                 http://www.springframework.org/schema/beans/spring-beans.xsd
                                 http://www.springframework.org/schema/security
                                 http://www.springframework.org/schema/security/spring-security.xsd">

    <!-- Define security rules -->
    <http>
        <intercept-url pattern="/auth/admin/**" access="hasRole('ADMIN')" />
        <intercept-url pattern="/auth/**" access="hasAnyRole('ADMIN','USER')" />
        <!-- Other URL patterns and access rules -->
    </http>

    <!-- Authentication configuration -->
    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="Namo" password="namo123" authorities="ROLE_ADMIN" />
                <user name="Amit" password="amit123" authorities="ROLE_USER" />
                <!-- Other users and roles -->
            </user-service>
        </authentication-provider>
    </authentication-manager>

</beans:beans>

In the above XML configuration:

  • The <http> section defines URL patterns and their corresponding access rules.
    • URLs starting with /auth/admin/ require the ADMIN role.
    • URLs starting with /auth/ are accessible to users with either the ADMIN or USER role.
  • The <authentication-manager> section configures user authentication.
    • We define two users (Namo and Amit) with their passwords and associated roles.

3.2. Authority-based Access Control – hasAuthority and hasAnyAuthority

Similar to roles, you can use the hasAuthority and hasAnyAuthority expressions to control access based on user authorities.

Authority-based access control is a more granular approach where permissions and access rights are assigned directly to authorities, rather than roles. Authorities represent specific privileges or capabilities within the application, such as “READ_PERMISSION”, “WRITE_PERMISSION”, or “DELETE_PERMISSION”.

You do not have to use ROLE_ when using Authority based Access Control.

Java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
 //extra stuffs

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers("/read/**").hasAuthority("READ_PERMISSION")
                .requestMatchers("/write/**").hasAnyAuthority("WRITE_PERMISSION", "ADMIN")
                .anyRequest().authenticated()
            .and()
                .formLogin(form -> form.loginPage("/login").permitAll())
                .logout(logout -> logout.logoutSuccessUrl("/my/success/endpoint").permitAll());

        return http.build();
    }
}

In this example, the /read/** URLs are accessible to users with the READ_PERMISSION authority, while the /write/** URLs are accessible to users with either the WRITE_PERMISSION or ADMIN authority.

XML based config:

XML
<beans:beans xmlns="http://www.springframework.org/schema/security"
             ......>

    <http>
        <intercept-url pattern="/read/**" access="hasAuthority('READ_PERMISSION')" />
        <intercept-url pattern="/write/**" access="hasAnyAuthority('WRITE_PERMISSION', 'ADMIN')" />

        <!-- Other URL patterns and access rules -->
        <form-login login-page="/login" />
        <logout />
    </http>

    <!-- Other security configurations (authentication, user details, etc.) -->

</beans:beans>

3.3. Authentication-based Access Control

Spring Security also provides expressions to control access based on the user’s authentication state .Authentication-based access control in Spring Security is used to control access based on the user’s authentication state. This type of access control is often used in combination with role-based or authority-based access control to provide an additional layer of security.

Java
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/anonymous/**").anonymous()
                .requestMatchers("/authenticated/**").authenticated()
                .requestMatchers("/remember-me/**").rememberMe()
            .anyRequest().fullyAuthenticated();

        return http.build();
    }

In this example:

  • Public resource: The /public/** URLs are accessible to everyone (both authenticated and anonymous users)
  • Annonymus access: The /anonymous/** URLs are accessible only to anonymous users
  • Authenticated: The /authenticated/** URLs are accessible to both authenticated and fully authenticated users
  • Remember me: The /remember-me/**isRememberMe expression allows access to resources for users who are authenticated through the “remember-me” feature.
  • All other URLs require full authentication (e.g., not remember-me authentication)

3.4. Principal and AuthenticationPrincipal Expressions

Spring Security also provides expressions to access the current Principal and Authentication objects.

Java
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RequestMapping("/user")
@RestController
public class UserController {
    @GetMapping("/me")
    public String getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
        return "Hello, " + userDetails.getUsername();
    }

    @GetMapping(value = "/username")
    public String currentUserName(Principal principal) {
        return principal.getName();
    }
}

In this example, we’re using the @AuthenticationPrincipal annotation to inject the current UserDetails object into the controller method. We can then access the username and other user details directly. For all posible ways to inject and access the User information in Spring, please refer the related article provided below.

Reference: Retrieving User Information in Spring Security

3.5. PermitAll and denyAll expressions

PermitAll: The permitAll expression allows unrestricted access to specific endpoints or resources without requiring authentication or authorization. It is typically used for defining public endpoints that should be accessible to all users, including those who are not authenticated. Suppose we want to allow unrestricted access to the homepage (/) and a public API endpoint (/api/public). We can configure it as follows:

DenyAll: The denyAll expression denies access to specific endpoints or resources for all users, regardless of their authentication status. It is rarely used but can be helpful in specific scenarios. Suppose we want to deny access to a sensitive admin panel (/admin). We can configure it as follows:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain configure(HttpSecurity http) throws Exception {
      http
          .authorizeRequests()
          .requestMatchers("/", "/api/public").permitAll()
          .requestMatchers("/admin").denyAll()
          .anyRequest().authenticated();
          //add login and logout from above
      return http.build();
  }
}
XML
<http>
    <intercept-url pattern="/" access="permitAll" />
    <intercept-url pattern="/api/public" access="permitAll" />
    <intercept-url pattern="/admin" access="denyAll" />
    <!-- Other URL patterns and access rules -->
    <form-login />
    <logout />
</http>

3.6. isAnonymous, isRememberMe, isAuthenticated, isFullyAuthenticated

1. isAnonymous Expression: The isAnonymous() expression checks whether the current principal is an anonymous user (i.e., not authenticated). It’s useful for allowing unrestricted access to certain endpoints or resources.

2. isRememberMe Expression: The isRememberMe() expression checks whether the current principal is a remember-me user. Remember-me authentication allows users to be automatically logged in based on a persistent token (e.g., a cookie).

3. isAuthenticated and isFullyAuthenticated Expressions: These expressions check the login status of the user:

  • isAuthenticated() returns true if the user is not anonymous (logged in).
  • isFullyAuthenticated() returns true if the user is not anonymous or a remember-me user.
Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers("/public/**").anonymous()
                .requestMatchers("/dashboard/**").rememberMe()
                .requestMatchers("/user/**").authenticated()
                .requestMatchers("/admin/**").fullyAuthenticated()
            .anyRequest().authenticated()
            .and()
                .formLogin(form -> form.loginPage("/login").permitAll())
                .logout(logout -> logout.logoutSuccessUrl("/my/success/endpoint").permitAll());
        return http.build();
    }
}
  • The antMatchers("/public/**").anonymous() line allows access to URLs starting with /public/ for anonymous users.
  • The antMatchers("/dashboard/**").rememberMe() line allows access to URLs starting with /dashboard/ for remember-me users.
  • The antMatchers("/user/**").authenticated() line allows access to URLs starting with /user/ for authenticated users (not anonymous).
  • The antMatchers("/admin/**").fullyAuthenticated() line requires fully authenticated users (not anonymous or remember-me) for URLs starting with /admin/.
  • All other requests require authentication.

Alternatively, use can also use Java based config as shown below.

XML
<http>
    <intercept-url pattern="/public/**" access="isAnonymous()" />
    <intercept-url pattern="/dashboard/**" access="isRememberMe()" />
    <intercept-url pattern="/user/**" access="isAuthenticated()" />
    <intercept-url pattern="/admin/**" access="isFullyAuthenticated()" />
    
    <!-- Other URL patterns and access rules -->
    <form-login />
    <logout />
</http>

4. hasPermission Expression in Spring Security

In Spring Security, the hasPermission expression is a powerful mechanism that allows for fine-grained access control based on permissions associated with domain objects. This expression is commonly used in conjunction with Access Control Lists (ACLs) to enforce access policies at the object level.

Working Principle:

The hasPermission expression evaluates whether the authenticated user has a specific permission on a given domain object. It takes two arguments:

  1. Target Domain Object: The object for which permission is being checked.
  2. Permission Expression: A string representing the permission being checked.

Usage:

The hasPermission expression is typically used within method-level security annotations, such as @PreAuthorize or @PostAuthorize, to restrict access to methods based on the permissions associated with domain objects. Here’s an example:

Java
@PreAuthorize("hasPermission(#book, 'read')")
public BookDto getBookDetails(Book book) {
    // Method implementation
}

In this example, the getBookDetails method is annotated with @PreAuthorize, and the hasPermission expression is used to ensure that the authenticated user has the ‘read’ permission on the specified Book object. If the user doesn’t have the required permission, access to the method will be denied.

Configuration:

To use the hasPermission expression with ACLs, you need to configure Spring Security to integrate with your data source and define the necessary permissions for your domain objects. This typically involves setting up a PermissionEvaluator bean that evaluates permissions based on your application’s business logic.

Java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Autowired
    private PermissionEvaluator permissionEvaluator;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler =
                new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator);
        return expressionHandler;
    }
}

Custom Permission Evaluators:

You can also implement custom PermissionEvaluator classes to define how permissions are evaluated for specific domain objects and permissions. This allows you to customize the access control logic based on your application’s requirements.

Java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        // Custom permission evaluation logic
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // Custom permission evaluation logic
    }
}

The hasPermission expression in Spring Security provides a flexible and extensible mechanism for implementing object-level access control based on permissions associated with domain objects. By using hasPermission, you can enforce fine-grained access policies in your application, ensuring that users have appropriate permissions to perform specific actions on specific objects.

Conclusion

Spring Security expressions provide a powerful and flexible way to define access control rules in your application. With role-based access control, authority-based access control, authentication-based access control, and Access Control Lists, you can implement fine-grained authorization logic based on various factors, such as user roles, authorities, authentication details, and domain objects.

Remember to always follow security best practices and regularly review and update your security configurations to ensure the ongoing protection of your application and its data.

By |Last Updated: May 10th, 2024|Categories: Spring Security|

Table of Contents