Many developers have experienced the confusion of suddenly seeing a login screen after adding Spring Security for the first time. Spring Security is powerful, but its sheer variety of configuration options often leaves people wondering where to even start.
This article walks first-time Spring Security users through the basics of authentication step by step. Starting from a minimal setup, we’ll implement Basic authentication and then form-based authentication in sequence, explaining what each configuration means and where beginners commonly get stuck.
By the end, you’ll understand how Spring Security authentication works and be ready to choose and implement the right authentication method for your own project.
What is Spring Security?
Spring Security is a framework that handles security for Spring applications. It provides authentication (who you are) and authorization (what you can do), but this article focuses on authentication.
Common authentication methods include Basic authentication, form-based authentication, OAuth2, and JWT. In this article, we’ll implement Basic authentication and form-based authentication progressively.
Observing Spring Security’s Default Behavior
First, let’s see what Spring Security does out of the box. Simply adding the following dependency to pom.xml automatically protects all endpoints.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
When you start the application, you’ll see output like this in the console:
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
This password changes on every startup. You can log in using it together with the default username user. Accessing any endpoint in a browser will show an auto-generated login page.
Security note: Use this default password for development only. If you want a fixed password, you can configure it in application.yml as shown below — but you must disable this in production.
spring:
security:
user:
name: user
password: dev-password
Behind this auto-configuration is a mechanism called SecurityFilterChain. Let’s understand this concept in the next section.
SecurityFilterChain Basics
The core of Spring Security is the SecurityFilterChain. It defines security rules such as which URLs to protect and which authentication method to use.
Since Spring Security 6, the standard approach is to register SecurityFilterChain as a @Bean and configure it using the Lambda DSL. The basic pattern looks like this:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(withDefaults()); // Specify the authentication method here
return http.build();
}
}
Here, we create a configuration class with the @Configuration annotation and register SecurityFilterChain as a @Bean. Security rules are configured using the builder pattern on the HttpSecurity object.
This is also a practical example of dependency injection in action. Spring Boot automatically detects this Bean and applies it as the security configuration for the entire application.
The .httpBasic(withDefaults()) line specifies the authentication method. This example uses Basic authentication, which we’ll look at in detail in the next section.
Minimal Basic Authentication Implementation
Let’s implement Basic authentication using an in-memory user.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
The .httpBasic(withDefaults()) method enables Basic authentication. The UserDetailsService Bean uses InMemoryUserDetailsManager to create a test user in memory.
Important: We’re using BCryptPasswordEncoder from the start. Some older code examples use User.withDefaultPasswordEncoder(), but this is fully deprecated as of Spring Security 5.7 and should not be used even in development.
With Basic authentication, the username and password are Base64-encoded and sent in the Authorization header.
curl -u user:password http://localhost:8080/api/hello
You can also specify the header explicitly (the string dXNlcjpwYXNzd29yZA== below is user:password Base64-encoded):
curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/hello
Accessing the endpoint in a browser will display the browser’s native authentication dialog.
Why Password Encoding Matters
We used BCryptPasswordEncoder in the code above, and it is not optional. Storing passwords in plain text means that if your database is ever compromised, every user’s password is immediately exposed.
Registering a PasswordEncoder as a @Bean lets Spring Security automatically use it to verify passwords during authentication. A password encoded with BCryptPasswordEncoder looks like $2a$10$... and includes the version, salt, and hash value.
Migrating to Form-Based Authentication
Basic authentication is simple, but the browser’s authentication dialog is not user-friendly. For typical web applications, form-based authentication is a better fit.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.defaultSuccessUrl("/home", true)
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
We’ve replaced .httpBasic(withDefaults()) with .formLogin(). Using the Lambda DSL, settings are written in the form form -> form.... defaultSuccessUrl("/home", true) specifies where to redirect after a successful login, and .permitAll() allows access to the login page itself — without this, you’ll get an infinite redirect loop.
The default login page is auto-generated at /login. Accessing it in a browser will display the standard login form provided by Spring Security.
Customizing the Login Page
To use a custom login page, configure it as follows:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/custom-login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/custom-login")
.defaultSuccessUrl("/home", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/custom-login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
.loginPage("/custom-login") specifies the custom login page, and .requestMatchers() grants access to static resources and the login page itself.
The ** in .requestMatchers("/css/**") is Ant-style pattern matching, meaning “all paths under /css/”. For example, it matches /css/style.css, /css/admin/layout.css, and so on.
Implementing Logout
The .logout() method configures logout behavior. With logoutSuccessUrl("/custom-login?logout"), the user is redirected to the login page after logging out, and the ?logout parameter can be used to display a logout success message.
Here’s an example login page using Thymeleaf (placed at src/main/resources/templates/custom-login.html):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="login-container">
<h1>Login</h1>
<div th:if="${param.error}" class="error">
Invalid username or password.
</div>
<div th:if="${param.logout}" class="success">
You have been logged out.
</div>
<form th:action="@{/custom-login}" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
Key points:
- Use
th:actionwith Thymeleaf’s URL expression for the form’sactionattribute. method="post"is required.- The
nameattributes must beusernameandpassword(customizable, but these are the defaults). - Use
${param.error}to detect a login failure and display an error message.
About CSRF tokens: When using Thymeleaf, a hidden field like the following is automatically added to POST forms:
<input type="hidden" name="_csrf" value="random token value"/>
Thymeleaf does this automatically when processing th:action — you do not need to add it manually.
Static resource placement: CSS files (e.g., /css/style.css) should be placed at src/main/resources/static/css/style.css. Spring Boot automatically serves files under the static folder as static resources.
Common Configuration Errors for Beginners and How to Fix Them
When you start using Spring Security, you’ll likely run into a few typical errors. Here are the main ones:
1. “There is no PasswordEncoder mapped for the id “null"" Error
This occurs when you try to use a plain-text password without configuring a password encoder. Register a PasswordEncoder as a @Bean and encode your passwords with it.
2. Infinite Redirect Loop to the Login Page
This happens when the login page itself requires authentication. Call .permitAll() inside .formLogin(), and also explicitly allow access with .requestMatchers("/custom-login").permitAll().
3. CSRF Token Error
Using Thymeleaf’s th:action automatically embeds the CSRF token. If you’re writing HTML manually, add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>.
Enabling Debug Logging
Debug logs are extremely helpful for troubleshooting. Add the following to application.yml:
logging:
level:
org.springframework.security: DEBUG
This outputs detailed logs of Spring Security’s internal behavior.
Choosing the Right Authentication Method
So when should you use Basic authentication versus form-based authentication?
When Basic Authentication is a Good Fit
- Protecting REST API endpoints
- Simple admin screens or development tools
- Stateless applications
- When access is primarily from tools like curl or Postman
When Form-Based Authentication is a Good Fit
- Web applications for end users
- When access is primarily from a browser
- When you want to customize the login screen
- When session management is required
In real projects, you can also use Spring Boot Profiles to switch configuration between environments. For example, you can use Basic authentication in development and form-based authentication in production.
Next Steps
In this article, we implemented Spring Security’s basic authentication features step by step. From here, you can move on to:
- Database integration: Implement
UserDetailsServiceto load user information from a database - OAuth2/OpenID Connect: Login via external services like Google or GitHub
- JWT authentication: Build stateless APIs using token-based authentication
- Authorization: Role-based access control using
@PreAuthorizeand similar annotations
These advanced topics will be covered in future articles. For now, make sure you have a solid grasp of the basics covered here, and try applying them in your own project.