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 authenticationpermitAll
- adddress is available for everyonehasRole
- 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 toantMatchers
, 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 pathsformLogin
- allows you to log in via the login form (via the default page or defined byFormLoginConfigurer
)logout
- adds logout supportcsrf
- allows to configure the mechanism CSRFheaders
- 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
- perform password hashing using the
- 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 theuserDetailsService
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 thegetAuthorities
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 resourcehasAnyRole
- defines the necessity to have at least one of the listed roles to get to the resourcehasAuthority
- defines the permission required for the resourcehasAnyAuthority
- 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 roleADMIN
(using methodauthorities
)user2
with roleADMIN
(using methodroles
)user3
with roleCREATE
And two endpoints:
GET /orders/{name}
, which requires the roleADMIN
POST /orders
, which requires the roleCREATE
orADMIN
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
oruser2
- 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.