Skip to content

Spring Security

Securing web applications is a incredibly important topic and companies are investing more and more (and increasingly) to make sure that user data is secure and that authentication is quick and easy. Spring enables integration with most market standards (eg SAML, OAuth or OpenID Connect). In this section we will describe some of the launchpad functionalities:

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

The main task is to add a security layer to the application. One of its most important advantages is the fact that it does so in a non-invasive manner, i.e. it can be integrated at any time during application development, without the need to modify existing functionalities.

Autoconfiguration

The spring-boot-starter-security starter automatically configures several application security mechanisms. These include:

  • automatically generated user with password
  • support for Basic Authentication for REST API
  • an automatically generated page that allows you to log in and out
  • protection against attacks CSRF (so-called Cross-site request forgery)
  • anti-attack protection enabled Session Fixation

Default user

After adding the spring-boot-starter-security starter to the dependency, all endpoints and pages are secured and require authentication. Access to resources has a user named user, and the password is randomly generated each time the application is started. This password should be found in the logs, for example:

Using generated security password: d14a5f60-6789-4e0b-870f-26ad01d6628e

The username and password can be changed using the following configuration keys:

spring.security.user.name
spring.security.user.password

After setting the password, it will not be logged to the screen.

Basic Authentication

Trying to use REST API such as, for example below:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
  @GetMapping("/hi")
  public String sayHello() {
    return "hello";
  }
}

by sending the request:

curl --location --request GET 'http://localhost:8080/hi'

We will get a response with the status 401 (ie Unauthorized).

By default, the authentication method is configured to use the so-called Basic Authentication. For this purpose, we need to send the Authorization header in our request with the value in the format:

Basic user:pasword

where user:password is encoded with the Base64 algorithm, e.g.:

curl --location --request GET 'http://localhost:8080/hi' --header 'Authorization: Basic dXNlcjpwYXNzd29yZA=='

NOTE: The Base64 algorithm is not a way to hash a password. It is used to represent the string in ASCII characters to not lose the data while it is being transferred.

Login page

When we try to visit any website for the first time, e.g. http://localhost:8080:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/index")
public class IndexPage {

  @GetMapping
  public String hi() {
    return "index";
  }
}

instead of its content, we should see the default login page (which we can redefine and freely change). After entering the password correctly, we should see the content of the page. If we want to log out, we can do it using the address http://localhost:8080/logout.

CSRF

Spring Security offers the ability to defend against CSFR attacks. This attack is an attempt to trick the user into typing their username and password on a page that, for example, resembles a landing page.

When enabling CSRF, the server expects, in addition to the data sent by the user, a special, unique token that is visible only on the "real" side (on the client side, ie on the browser side). This token is usually part of form, in a hidden field. In the event that the server does not receive an appropriate token from the user (despite, for example, a correct password), such request will be rejected.

When CSRF functionality is enabled, a token is generated for each of the form that uses the HTTP POST method. This can be seen on the automatically generated login page by looking at the source of the page:

<form class="form-signin" method="post" action="/login">
    <h2 class="form-signin-heading">Please sign in</h2>
    <p>
      <label for="username" class="sr-only">Username</label>
      <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
    </p>
    <p>
      <label for="password" class="sr-only">Password</label>
      <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
    </p>
    <!--ukryty token csrf-->
    <input name="_csrf" type="hidden" value="f72715d4-86e1-400d-808b-8b7ba8cc4d8c" />
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>

NOTE: Enabled CSRF prevents sending the request with some methods (eg POST) using the REST API.

Session Fixation

The Session Fixation attack is an attempt by the attacker to extract the user's session ID, thanks to which the attacker can take over the identity of another user. By default, a simple mechanism against this type of attack is enabled by default - new session is created when authentication is attempted.

Basic configuration

Due to the fact that Spring Security offers rich auto-configuration, we often want to overwrite only part of the default configuration. We do this by extending the WebSecurityConfigurerAdapter class and overwriting only selected methods.

Configuration of access to paths

By default, all paths require authentication. If you want to change this behavior, you should override the configure (HttpSecurity) method. This method takes as an argument an HttpSecurity object, which allows you to configure:

  • which paths require authentication (and what roles)
  • what methods of logging in the application offers
  • enable or disable CSRF

We configure all these elements using the available fluent builder. We can use, for example, the following methods:

  • antMatchers - this method expects a path (or additionally an HTTP method), and then one of the methods defining the access level, e.g .:

    • authenticated - address always require authentication
    • permitAll - adddress is available for everyone
    • hasRole - address requires authentication and role

    In addition, antMatchers allows you to define special characters that can match a group of paths. These signs are:

    • $ - replaces one, any character
    • * - replaces one or more characters, up to the next / character
    • ** - replaces one or more characters
  • mvcMatchers - an alternative to antMatchers, which allows you to define paths in the Spring MVC style (i.e. using additional braces)

  • anyRequest - defines the required access level for the remaining, undefined paths
  • formLogin - allows you to log in via the login form (via the default page or defined by FormLoginConfigurer)
  • logout - adds logout support
  • csrf - allows to configure the mechanism CSRF
  • headers - allows you to configure the headers associated with the security layer

All the separate configuration elements in this method are connected using the and method.

The following example overrides the configure (HttpSecurity) method as follows: * requires authentication on a path starting with */ users /* * requires authentication on/api/ordersbut only when using the POSTmethod * allows you to get to/ h2` and other paths not configured in this method * configures the ability to log in using the form and Basic Authentication * allows you to log out of the application * disables the CSRF mechanism

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/users/**").authenticated()
        .antMatchers("/h2").permitAll()
        .antMatchers(HttpMethod.POST, "/api/orders").authenticated()
        .anyRequest().permitAll()
        .and()
        .formLogin()
        .and()
        .httpBasic()
        .and()
        .logout()
        .and()
        .headers().frameOptions().disable()
        .and()
        .csrf().disable();
  }
}

NOTE: The above example contains a line of code headers().frameOptions().disable() which will disable some important headers (X-Frame-Options) related to the security layer. This allows the H2 consoles to function properly. We recommend this configuration only in development profile.

User configuration

The default auto-configuration gives us access only to a single user. This is not always a sufficient configuration. You can add new users and give them the so-called roles. We can do this by overriding the configure (AuthenticationManagerBuilder) method. This method offers the possibility to define the source of the user list. We will describe two ways:

  • users defined at the start of the application
  • users (and their hashed passwords) defined in the database

Users in memory

The AuthenticationManagerBuilder object allows you to define a list of users whose data (username, password, and roles) are stored in memory. We do this with the inMemoryAuthentication method, which then allows:

  • defining a username using the withUser method
  • setting the user's password using the password method
  • define user roles using the roles method
  • define another user using the and method

The next example shows how to define users:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user1").password("Admin_1").roles("ADMIN")
        .and()
        .withUser("user2").password("Admin_2").roles("SUPER_ADMIN");
  }
}

However, attempting to log in with the login user and password Admin_1 will fail with the following exception:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

This is due to the fact that when defining users and their passwords, we must indicate how we should hash their passwords. Most often we do this by defining a PasswordEncoder object.

NOTE: The hash operation is not reversible. The password validation is performed by hashing on the entered password and then comparing it with the expected hash.

NOTE: Hash algorithms use a certain disturbance string (the so-called salt), i.e. random data added to the password when calculating the hash value so that two identical passwords do not have the same value of the hashed password.

Password encoder

Spring offers several ways to define information about how passwords are hashed. These are, among others:

  • defining bean type PasswordEncoder, which allows to:
    • perform password hashing using the encode method
    • compare the password with the hash using the matches method
  • indication of the encoder by its identifier, in front of the password, inside the clamps

By default, we can choose between built-in encoders using the following keys:

key encoder class
bcrypt BCryptPasswordEncoder
ldap LdapShaPasswordEncoder
MD5 MessageDigestPasswordEncoder("MD5")
noop NoOpPasswordEncoder
pbkdf2 Pbkdf2PasswordEncoder
scrypt SCryptPasswordEncoder
SHA-1 MessageDigestPasswordEncoder("SHA-1")
SHA-256 MessageDigestPasswordEncoder("SHA-256")
sha256 StandardPasswordEncoder
argon2 Argon2PasswordEncoder

The next example corrects the bug that occurred in previous. This time the password Admin_1 uses the bcrypt algorithm, while the password Admin_2 uses the MD5 algorithm:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user1").password("{bcrypt}$2a$10$w6AoSvH7ezHwQomG19OjOev3AZuu/UWkROR5CjW1fXZjvP/dy9mEq").roles("ADMIN")
        .and()
        .withUser("user2").password("{MD5}{WtFrETqej9qZsjAWIlXhM03PxD5xKvdcpr3T9R/BHl8=}2aff96e65139a682af29072ffcef19d1").roles("SUPER_ADMIN");
  }
}

After starting the application, we should be able to log in correctly by entering the login user and password Admin_1.

Choice of algorithm

Having a large number of encoders at our disposal, we should choose one whose hashing process is not fast. This keeps the attacker unable to quickly make multiple password-guesses. Currently, frequently used encoders are those using the bcrypt or scrypt algorithms. However, support for algorithms such as, for example, MD5 (which we should not use) exists to support older applications that may still use such an algorithm.

When creating the software, e.g. using profile the developer can also use the noop encoder, which allows you to store passwords as a so-called plain text (not hashed).

Some encoders will not work properly without additional dependencies. If you want to use the SCryptPasswordEncoder encoder, add to thedependencies section in the pom.xml file:

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <version>1.66</version> <!--może być dostępna nowsza wersja-->
</dependency>

PasswordEncoder as bean

If all user passwords are hashed with the same algorithm, it is better to define it as bean with the name passwordEncoder instead of entering the encoder key, e.g.:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication() // oba hasła wykorzystują algorytm bcrypt i są to odpowienio Admin_1 i Admin_2
        .withUser("user1").password("$2a$10$w6AoSvH7ezHwQomG19OjOev3AZuu/UWkROR5CjW1fXZjvP/dy9mEq").roles("ADMIN")
        .and()
        .withUser("user2").password("$2a$10$V/kkkDd5jXR1Y/mbmrneIePEsgHcXteNr41IwoFWzKQxUeeV1a.q2").roles("SUPER_ADMIN");
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}

Users in the database

The Spring Security configuration also allows us to define our own user source (for example a database). For this we must:

  • create an implementation of the UserDetailsService interface
  • in the WebSecurityConfigurerAdapter extension configuration class, indicate the implementation ofUserDetailsService in the userDetailsService method

The UserDetailsService interface defines how to retrieve user data based on their username. We represent the user through the UserDetails interface, which, in addition to indicating the username and password (which should already be hashed), also allows you to block the login by marking:

  • accounts as such that have expired (method isAccountNonExpired)
  • accounts as locked (method isAccountNonLocked)
  • accounts as disabled (method isEnabled)
  • passwords as such that have expired (method isCredentialsNonExpired)

If we do not want to implement any of the above functionalities, the appropriate method should return the value true.

Most often, the user in the application is represented by a class other than UserDetails. This class can be adapted to this interface, eg using adapter. The following example shows how to define a table as a user data source:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "users")
public class User {

  @Id
  private String username;

  @Column(name = "password")
  private String password;

  @Column(name = "role")
  private String role;
}
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {
}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class UserDetailsAdapter implements UserDetails {

  private final User user;

  public UserDetailsAdapter(final User user) {
    this.user = user;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority(user.getRole()));
  }

  @Override
  public String getPassword() {
    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  private final UserDetailsService userDetailsService;

  public SecurityConfig(@Qualifier("customUserDetailsService") final UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
  }

  @Override
  protected UserDetailsService userDetailsService() {
    return userDetailsService;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}

Thanks to the class below, the application allows you to log in with the username user1 and the passwordAdmin_1.

import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DbInitializer implements CommandLineRunner {

  private final UserRepository userRepository;
  private final PasswordEncoder passwordEncoder;

  public DbInitializer(final UserRepository userRepository, final PasswordEncoder passwordEncoder) {
    this.userRepository = userRepository;
    this.passwordEncoder = passwordEncoder;
  }

  @Override
  public void run(final String... args) throws Exception {
    userRepository.save(new User("user1", passwordEncoder.encode("Admin_1"), "ADMIN"));
  }
}

Roles

When defining users in-memory or in database, we are forced to define them a role list. The main purpose of the roles is to limit access to certain resources for some logged in users, i.e. the roles allow you to define which subgroup of users can get access, e.g. to a given URL.

Role a Authority

Spring Security allows you to define user rights and uses two definitions:

  • roles
  • authority

Both are any sequence of characters, but the role has an additional prefix ROLE_ (which we do not have to provide), i.e. when talking about roles, we can interchangeably talk about authorities (permits), whose name starts with ROLE_ (and this part must be added most often).

Defining roles

Depending on the configuration, we define user roles in slightly different ways:

  • if the default user is used, the role is defined by the following entry in the application.properties file: spring.security.user.roles
  • if we define users in-memory, we define their roles using the roles orauthorities method
  • if we define users using the UserDetailsService andUserDetails interface, roles are defined in the getAuthorities

The role object is most often represented by the GrantedAuthority interface and one of its implementations (eg,SimpleGrantedAuthority).

The example below defines the ADMIN role for both user1 and user2:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user1").password("password").authorities("ROLE_ADMIN")
        .and()
        .withUser("user2").password("password").roles("ADMIN");
  }
}

The next example unfolds one of the previous, defining two user roles - one that is part of the object definition of User and the other - SIMPLE_USER.:

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class UserDetailsAdapter implements UserDetails {

  private final User user;

  public UserDetailsAdapter(final User user) {
    this.user = user;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(new SimpleGrantedAuthority(user.getRole()), new SimpleGrantedAuthority("ROLE_SIMPLE_USER"));
  }

  @Override
  public String getPassword() {
    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

Blocking resources

Having defined users and their roles, we can configure those parts of the application that require them (one or more). It can do this in many ways. We will describe two of them:

  • define the required roles in the configuration class that extends the WebSecurityConfigurerAdapter class in theconfigure (HttpSecurity)method
  • defining the required roles via annotations directly in the classes that offer resources

WebSecurityConfigurerAdapter a role

The roles in the configure (HttpSecurity) method in the WebSecurityConfigurerAdapter class can be defined using fluent builder as offered by the input object. When we define which paths need to be authenticated, instead of the authenticated method, we can use:

  • hasRole - defines the required role for the resource
  • hasAnyRole - defines the necessity to have at least one of the listed roles to get to the resource
  • hasAuthority - defines the permission required for the resource
  • hasAnyAuthority - defines the need to have at least one of the permissions listed to access the resource

The following example shows the use of these methods:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/orders").hasRole("ADMIN")
        .antMatchers("/groups").hasAnyRole("ADMIN", "GROUP_ADMIN")
        .antMatchers("/products/**").hasAnyAuthority("ROLE_ADMIN", "ROLE_PRODUCTS_MANAGER");
  }
}

NOTE: If we are logged in, but we do not have the appropriate role for the resource, REST API should return the status 403 (Forbidden).

@Secured annotation

Spring Security allows you to define the requirements for accessing a resource with certain annotations. These include:

  • @RolesAllowed
  • @PreAuthorize
  • @Secured

Each of the above performs a similar task. The @Secured annotation, which we will focus on in the next example, allows you to define the permission (so we must enter the ROLE_ prefix manually if necessary) required to access the resource. This annotation can be placed both on methods and classes (most often controllers). By default, however, it is ignored. In order for it to be included, we need to add the annotation @EnableGlobalMethodSecurity (securedEnabled = true) on the configuration class.

The next example defines the following users:

  • user1 with role ADMIN (using method authorities)
  • user2 with role ADMIN (using method roles)
  • user3 with role CREATE

And two endpoints:

  • GET /orders/{name}, which requires the role ADMIN
  • POST /orders, which requires the role CREATE or ADMIN
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final HttpSecurity http) throws Exception {
    http.httpBasic()
        .and()
        .csrf().disable();
  }

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("user1").password("password").authorities("ROLE_ADMIN")
        .and()
        .withUser("user2").password("password").roles("ADMIN")
        .and()
        .withUser("user3").password("password").roles("CREATE");
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;

import java.util.Random;

@RestController
@RequestMapping("/orders")
public class OrdersController {

  @Secured("ROLE_ADMIN")
  @GetMapping("/{name}")
  public Order getOrder(@PathVariable String name) {
    return new Order(name, new Random().nextInt());
  }

  @Secured({"ROLE_CREATE", "ROLE_ADMIN"})
  @PostMapping
  public Order createRandomOrder() {
    return new Order("randomOrder", new Random().nextInt());
  }
}

Trying to get to the resource GET /orders/{name}:

  • as user1 or user2 - we get the status 200
  • as user3 we get the status 403

When trying to access the POST/orders resource, we will get the status 200 if we use any of the defined users.