Spring Security: Applying the Basics.

Your Image

Lars Martens

14 Maart, 2025

In the previous part we discussed the basic building blocks of Spring Security:
- The Spring Security dependency.
- A configuration class annotated with @Configuration and @EnableWebSecurity.
- A SecurityFilterChain.
- A UserDetailsService.
- A PasswordEncoder.

Create a web application.

In this article, we are going to get hands-on and use these building blocks to secure an application. So, let's start by creating our web application.

Go to the Spring Initializr page at: https://start.spring.io/ and use the initializer to generate a Spring project. I chose Java and Maven, but you can select whatever you prefer. The only dependency we will add for now is the Spring Web dependency, which is all we need to create a basic web application.

Screenshot of Spring Initializr

Save the zip file, extract it to a folder of your choice, and import the project into your preferred IDE. Now, let's create a controller class in our application. I created a new package named 'controller' inside the main package of the application. Within this controller package, I created a new class called UserController. However, you can choose a different name or structure if you prefer.

Screenshot of project structure

Give the class the @RestController annotation and create three endpoints, as shown in the code example. Since we haven't added security to our application yet, these endpoints will be accessible to anyone.
Don't worry, we will secure them soon.
Here is the code for the class:


package com.spring.basics.SpringBasics.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
	
	@GetMapping("/no-security")
	public String openToAll() {
		return "Welcome to all";
	}
	
	@GetMapping("/some-security")
	public String openToSome() {
		return "Welcome to some";
	}
	
	@GetMapping("/full-security")
	public String openToOne() {
		return "Welcome to only one";
	}
}

The Spring Initializer has generated an application class for you. Since I named my project SpringBasics, the generated application class in my project is called SpringBasicsApplication. Yours might have a different name.

Locate your application class and run it to start your Spring application. Your web application also includes an embedded web server, which starts automatically when you run the application. This server will host your application at the default address: http://localhost:8080.

With your application running, open your web browser and navigate to the first endpoint we created at: http://localhost:8080/no-security. You should see a screen like this:

Screenshot of no security endpoint

Our application allows access to the /no-security endpoint, as confirmed by the fact that we can open this page and see the "Welcome to all" message. You can also visit the other two endpoints to verify that they are accessible. Simply modify the URL by replacing /no-security with /some-security or /full-security.

We have a web application running on an embedded server. This application includes three endpoints, all of which are accessible to anyone, including unauthenticated users. At this point, we have zero security in place. Let's change that.

Add the Security dependency.

The first and most important step in securing our application is adding the Spring Security dependency. In a Maven project (which is what I’m using), we can do this by adding the following code to the pom.xml file:


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

Now that we have added the security dependency, stop your application. In Eclipse you can do that by pressing the stop button in the console window:

Screenshot of eclipse stop button

Restart the application to apply the changes we made. Then, try accessing one of the three endpoints again. For example, type http://localhost:8080/no-security into the address bar of your web browser and press Enter. You should now see a screen like this:

Screenshot of the login screen

What happened? We tried to visit http://localhost:8080/no-security, but instead of seeing that page, we were redirected to a login page at http://localhost:8080/login?

By adding the Spring Security dependency, we have enabled security in our application. When security is enabled, Spring restricts access to all endpoints for unauthenticated users. Anytime an unauthenticated user tries to visit our endpoints, they are automatically redirected to the default login page at /login.

Restricting access for unauthenticated users and redirecting them to an HTML login form at /login are a few of Spring Security’s default features.

But there’s more: try submitting the login form without entering a username or password and Spring will show a validation message asking you to fill out the fields. This is Spring providing you with input validation out of the box.
And if you try logging in with an incorrect username or password, Spring will redirect you to an empty login form and display the error message 'Bad credentials.'

These built-in security behaviors are useful, but there's a problem: our previously accessible web application is now completely locked down! Even we, the developers, are locked out of the application.

But don't worry, Spring has us covered. Each time we start our application Spring creates a default user with credentials we can use to log into the application. The default username is 'user' and the default password is being displayed in the console at startup. If we restart our application, Spring will generate a new password:

Screenshot of default credentials

The password displayed in the console is encoded using the BCrypt algorithm, which Spring Security applies by default.

Copy the generated password from the console and use 'user' as the username to log in. Once authenticated, Spring will redirect you back to the page you originally tried to visit. So, if you were unauthenticated and tried to visit http://localhost:8080/some-security, Spring would have redirected you to the login page and upon successful authentication would have shown you that http://localhost:8080/some-security page that you were trying to visit. If you tried to visit a different page, then that's the page that Spring will show you upon successful authentication. So, Spring remembers what we were trying to visit, and that's what it will show to us once we are authenticated.

Right now, we have security enabled and a default user, but that’s not very useful. Our goal is to create an application that supports multiple users, not just one default user with a default password. It's time to configure our application. Let's create a Configuration Class.

Add a configuration class.

I created a new package named "config" inside the main package of the application. Inside this config package, I've added a new class called ProjectConfig. Of course, you can structure your application differently if you prefer.

Screenshot of project with config class

As explained it the previous part, we are going to give this class the @Configuration and @EnableWebSecurity annotations. It's inside this class that we are going to be handling most of the security aspects of our application.

Add a SecurityFilterChain.

To handle authorization, we need a SecurityFilterChain. Once we add a SecurityFilterChain to our application, Spring assumes that we are handling authorization ourselves. This means that Spring will no longer present users with a login form or automatically secure our endpoints for us. Instead, handling both login and security will become our responsibility.

Similarly, when Spring detects that we are managing authorization ourselves, such as by declaring a UserDetailsService bean, it will no longer provide a default user and password for the application.

Let's add a SecurityFilterChain in our ProjectConfig class:

Screenshot of filterChain

By adding this SecurityFilterChain we have locked all our endpoints except the /no-security endpoint that is open to all. Users will be able to visit the /no-security endpoint, but when they try to go to one of our other endpoints, they will see the following 403 error message:

Screenshot of 403 error message

Remember to stop and restart your application every time you make changes. Otherwise, those changes will not take effect.

As mentioned earlier, Spring no longer automatically presents users with the default login form when they try to access a protected endpoint while unauthenticated. We now need to handle this ourselves. To add form login capabilities to our application, adjust the SecurityFilterChain so it includes .formLogin(). Here's the code:

Screenshot of SecurityFilterChain

Adding .formLogin() to our SecurityFilterChain is functionally the same as adding .formLogin(Customizer.withDefaults()). Both methods enable form-based authentication with the default settings. The .formLogin() method is simply a shorthand that internally calls .formLogin(Customizer.withDefaults()).

Add a UserDetailsService.

So far, we've added the security dependency, created a configuration class, and defined our SecurityFilterChain. This means we’ve already worked with three of the core building blocks of Spring Security.

Now, let’s move on to the fourth building block, the UserDetailsService. By creating a UserDetailsService, we can experiment with roles and authorities.

Spring uses the UserDetailsService to manage authentication and authorization. Remember from the previous article that it's the UserDetailsService that provides Spring with a UserDetails object. And it's this UserDetails object that Spring uses to verify the credentials of a user and to know what roles and authorities the user has.

While roles and authorities are often used interchangeably, they are not the same thing.
- Roles in Spring Security are high-level groupings of permissions.
- Authorities, on the other hand, represent specific permissions.
For example, an administrator could have the role 'ADMIN', which may group the authorities read, write, delete, and update.
While a regular user might have the role 'USER' and only the authorities 'read' and 'write'.

As an example, I have created three users, using the names of colors to define their roles. I used colors to demonstrate that roles and authorities are not fixed concepts in Spring Security. We can use any String we want to name them and we have the freedom to define them as we see fit.
We will use the SecurityFilterChain to control the level of access associated with a specific role.
- The user with the least access is named 'visitor' and has been assigned the role 'RED'.
- The next user will be given more access, had the role 'ORANGE' and the username 'user'.
- The final user, 'admin', has the most access and receives the role 'GREEN'.

To make it easier to test these users, I’ve given them passwords that are the same as their usernames (e.g., the password for the 'visitor' is 'visitor').

In order for Spring to access a repository of these users, I’ve created a UserDetailsService bean. Notice that this UserDetailsService returns a InMemoryUserDetailsManager. This is a class that implements the UserDetailsService interface, so Spring can use it to verify the credentials and see what roles a user has.
Like you could have guessed from its name, the InMemoryUserDetailsManager keeps the information about the user in memory. In a real production-ready application, we would use a database that can persist data and not a InMemoryUserDetailsManager.

Here is the code:

Screenshot of UserDetailsService

In our application, we will only use roles to regulated the level of access a user has. We will not use any authorities. For our relatively simple application with only three endpoints, using authorities would be overkill.

For more complex applications, it might be necessary to use both roles and authorities. It all depends on the level of fine-grained control you need in your application.
In a complex application, authorities allow us to handle what a user is allowed to do or access in greater detail. While roles make it easier to assign the authorities to individual users by grouping them together. It's for instance much easier to assign the 'ADMIN' role to each administrator, than to manually grant every required authority to each new administrator that has to be added to the system.

Let's rewrite our SecurityFilterChain so that it handles the different roles we’ve created. We want to ensure that none of our endpoints are accessible without authentication. Here's the breakdown of how access should be handled:
- The user with the least access (role 'RED') should only be able to access the /no-security endpoint.
- The user with a bit more access (role 'ORANGE') should be able to access /no-security and /some-security, but should not have access to the /full-security endpoint.
- The 'admin' user (role 'GREEN') should have access to all three endpoints.

Here’s how the SecurityFilterChain would look after this rewrite:

Screenshot of SecurityFilterChain

Don't forget to stop and restart your application.

Now that we have a UserDetailsService and a SecurityFilterChain set up, you can go ahead and experiment with the different user accounts. Since we used .anyRequest().authenticated() to lock everything down, none of the endpoints should be accessible without authentication. If you try to access an endpoint without being logged in, it will fail, and you will be redirected to the login screen.

Try logging in with the username 'visitor' and the password 'visitor'. Once logged in, you should only have access to the /no-security endpoint. If you try to access an endpoint that you don't have the appropriate role for, such as /some-security or /full-security, you will see a whitelabel error page. This is a 403 Forbidden error, which means our security is working as intended. Forbidden is exactly what the page is when a user doesn't have the necessary permissions.

While we could customize this error page to make it look more professional, for the purpose of this article this default page is sufficient.

Screenshot of whitelabel error

At the moment, if you want to switch users, you will need to stop and restart the application. This is because there's no logout functionality implemented yet. Let's fix this by adding logout capabilities to our SecurityFilterChain. We simply need to include .logout() in the SecurityFilterChain This will enable Spring Security's default logout mechanism.

Screenshot of SecurityFilterChain

Now that we’ve enabled the default logout behavior of Spring Security, we can visit the URL http://localhost:8080/logout. There we will be presented with a button to log us out of the current account.

Screenshot of logout screen

Once we press the logout button, Spring will automatically redirect us to the login page. From there, we can log in with a different user account to continue testing and experimenting with different user roles and permissions.

Add a PasswordEncoder.

We have now played around with four out of the five Security building blocks: the Spring Security dependency, a configuration class annotated with @Configuration and @EnableWebSecurity, a SecurityFilterChain and a UserDetailsService. The only element we haven't used yet is a PasswordEncoder.

Let's explore what happens when we don't provide Spring Security with the necessary information to handle passwords. Up until now, we’ve actually provided Spring with password handling instructions, even though it may not seem so. This was done by specifying that we would handle passwords without any encoding. We provided this in lines like this one: .password("{noop}user"), where the {noop} stands for "No Operation" password encoding, meaning the password is stored in plain text. If we remove these lines from the UserDetailsService, so that it no longer includes the {noop} prefix or any password encoding mechanism, then users will no longer be able to log in.

Screenshot of UserDetailsService with no password information

User won't be able to log in, even when they use correct credentials. Simply because Spring lacks the necessary information to be able to verify if the credentials are correct or not. The user will see a server error with error code 500. And in the console there will be an error message, informing us that there's no PasswordEncoder mapped:

Screenshot of 500 error Screenshot of no passwordEncoder error

Let's fix these errors by telling Spring that we'll use BCrypt encoding to hash our passwords. It's the same encoding Spring uses for the default user’s password. To do this, we need to provide Spring with a PasswordEncoder bean. Simply add the following code to our configuration class:

Screenshot of BCrypt Encoder

Stop and restart your application and try to login by using the correct credentials of one of our three users, for instance the credentials user/user. You will notice that although you are using correct credentials, you are not being authenticated. Spring informs you that your credentials are bad with the following error message:

Screenshot of bad credentials error

This situation happens because Spring always saves passwords in their encrypted form. And also expects us to provide them in encrypted form. But in our UserDetailsService we have saved the passwords unencrypted. So, when we try to login, Spring takes the password that we use in the login form, encrypts it and then compares that encrypted password to the unencrypted one it receives from our UserDetailsService. Those two will, of course, never match, hence the 'Bad credentials' error.

Change the UserDetailsService so it takes a PasswordEncoder as a parameter. Spring will make sure that the PasswordEncoder bean we have defined earlier, is the PasswordEncoder that will be injected into the UserDetailsService. Also add code so the encoder hashes the passwords. Like this .password(passwordEncoder.encode("admin")). Don't forget to stop and restart your application after you made the changes:

Screenshot of UserDetailsService + PasswordEncoder

If at any point you successfully log in, but are redirected to a 'Whitelabel Error Page' with a 'Not Found status=404' error (as happened to me), this occurs because you didn't first attempt to visit one of the endpoints in the application. Let me explain.

When you navigate directly to the login page, Spring won't redirect you to the endpoint you were trying to visit before logging in (because there is none, you accessed the login page directly). In this case, it's default behavior to redirect you to the home page, which is the / endpoint.
Since we did not configure this endpoint in our application, Spring is trying to redirect us to a non-existent page, causing that '404 not found' error page.

We can fix this situation by creating a new mapping for this / endpoint in our controller class or by configuring a .defaultSuccessUrl() method in our SecurityFilterChain. The .defaultSuccessUrl() configures where Spring should redirect users when they have succesfully authenticated.

We've now covered the basics of Spring Security. With these components, we've successfully managed authentication and authorization for different users in our web application. I hope this has provided you with a clearer understanding of the fundamentals of Spring Security, demonstrating that it's not as intimidating as it may have seemed and that you’ve enjoyed the process and had some fun along the way.



Part Two: Understanding the Basics