When developers first introduce Spring Security, many are caught off guard by a sudden login screen. Spring Security is powerful, but its wide range of configuration options often leaves developers wondering where to begin.
This article walks developers who are new to Spring Security through the basics of authentication, step by step. Starting from a minimal configuration and progressing through Basic authentication and form-based authentication, we will carefully explain the meaning of each setting and the points where beginners commonly stumble.
By the time you finish reading, the goal is for you to understand how Spring Security’s authentication works and be able to choose and implement the authentication method that fits 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.
The main authentication methods include Basic authentication, form-based authentication, OAuth2, and JWT. In this article, we will implement Basic authentication and form-based authentication step by step.
Verifying Spring Security’s default behavior
First, let’s check the default behavior when Spring Security is introduced. By simply adding the following dependency to pom.xml, all endpoints are automatically protected.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
When you start the application, you will see a log line like the following in the console.
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
This password changes every time the application starts. You can log in by combining it with the default username user. When you access any endpoint in a browser, an automatically generated login page is displayed.
Security note: This default password should only be used during development. If you want to fix it, you can configure it in application.yml as follows, but it must be disabled in production environments.
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.
The basics of SecurityFilterChain
At the core of Spring Security is the SecurityFilterChain. It defines security rules such as which URLs to protect and which authentication method to use.
In Spring Security 6 and later, the standard approach is to register SecurityFilterChain as a @Bean and configure it using the Lambda DSL notation. The basic pattern is 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.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 use the @Configuration annotation to create a configuration class and register SecurityFilterChain as a @Bean. We use the builder pattern with the HttpSecurity object to configure the security rules.
This mechanism is also a practical application of DI. Spring Boot automatically detects this Bean and applies it as the security configuration for the entire application.
The .httpBasic(withDefaults()) part specifies the authentication method. In this example, we use Basic authentication, but let’s look at it in more detail in the next section.
A minimal Basic authentication implementation
Now 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. In the UserDetailsService Bean, we use InMemoryUserDetailsManager to create a test user in memory.
Important: We are using BCryptPasswordEncoder from the very start. Older code examples sometimes use User.withDefaultPasswordEncoder(), but this has been completely deprecated since Spring Security 5.7 and should not be used even in development environments.
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 explicitly specify the header (the dXNlcjpwYXNzd29yZA== below is the Base64-encoded form of user:password).
curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/hello
When accessed from a browser, the browser’s standard authentication dialog is displayed.
The importance of a password encoder
In the code above we use BCryptPasswordEncoder, and this is essential. If you store passwords in plain text, then if the database is compromised, every user’s password will be exposed.
When PasswordEncoder is registered as a @Bean, Spring Security automatically uses this Bean to verify passwords during authentication. A password encoded with BCryptPasswordEncoder has a format like $2a$10$..., which contains the version, salt, and hash value.
Migrating to form-based authentication
Basic authentication is simple, but the browser’s authentication dialog is not very user-friendly. For typical web applications, form-based authentication is more suitable.
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 replaced .httpBasic(withDefaults()) with .formLogin(). Using the Lambda DSL notation, the configuration is written in the form form -> form.... defaultSuccessUrl("/home", true) specifies the redirect destination after successful login, and .permitAll() allows access to the login page itself (without this, an infinite redirect occurs).
The default login page is automatically generated at /login. When accessed from a browser, a standard login form provided by Spring Security is displayed.
Customizing the login page
To use your own 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() allows access to static resources and the login page.
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 functionality
The .logout() method configures the logout functionality. With logoutSuccessUrl("/custom-login?logout"), the user is redirected to the login page after logout, and the ?logout parameter can be used to display a logout success message.
A login page example using Thymeleaf is shown below (place it in 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">
The username or password is incorrect.
</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>
Important points:
- Specify the form’s
actionattribute usingth:actionwith a Thymeleaf URL expression. method="post"is required.- The
nameattributes must beusernameandpassword(these can be customized, but these are the defaults). - You can detect login errors with
${param.error}and display an error message.
About the CSRF token: When using Thymeleaf, a hidden field like the following is automatically added to POST forms.
<input type="hidden" name="_csrf" value="random token value"/>
This is automatically done by Thymeleaf when processing th:action, so you do not need to add it manually.
Static resource placement: Place CSS files (/css/style.css) at src/main/resources/static/css/style.css. Spring Boot automatically exposes files under the static folder as static resources.
Common configuration errors beginners encounter and how to solve them
When you start using Spring Security, you will encounter several 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 PasswordEncoder as a @Bean and encode the password.
2. Infinite redirect to the login page
If the login page itself requires authentication, an infinite redirect occurs. Call .permitAll() inside .formLogin(), and additionally explicitly allow it with .requestMatchers("/custom-login").permitAll().
3. CSRF token errors
If you use Thymeleaf’s th:action, the CSRF token is automatically embedded. If you write the HTML manually, add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>.
Enabling debug logging
For troubleshooting, debug logs are extremely helpful. Add the following to application.yml.
logging:
level:
org.springframework.security: DEBUG
This will produce detailed log output of Spring Security’s internal behavior.
Criteria for choosing an authentication method
Frequently Asked Questions (Spring Boot × Spring Security Authentication Tutorial FAQ)
Q. Can httpBasic() and formLogin() be enabled at the same time?
Yes, you can call both within the same SecurityFilterChain. Form-based authentication responds to browser access, while Basic authentication responds to Authorization: Basic ... headers from curl or API clients. However, this can be confusing to operate, so it is preferable to separate SecurityFilterChain @Beans for REST APIs and the web UI.
Q. What is the difference between httpBasic(withDefaults()) and httpBasic()?
Functionally they are the same. withDefaults() is a utility that takes a Customizer and is used to make the abbreviated form explicit in the Lambda DSL notation; it is the recommended style in Spring Security 6 and later. The sample code in this article is consistent with this style.
Q. If I want to start with the simplest possible Spring Boot authentication tutorial, which one should I implement?
The minimum is the configuration shown at the beginning of this article in “The basics of SecurityFilterChain”, which uses only httpBasic(withDefaults()). You only need to add spring-boot-starter-security to your dependencies and provide UserDetailsService and PasswordEncoder as @Beans to get it working. From there, we recommend gradually expanding to form-based authentication → custom login page → database integration.
Q. Why is Basic authentication’s password sent in Base64 but not considered “encryption”?
Because Base64 is encoding (a reversible representation), not encryption. If you use HTTPS (TLS), the communication channel is protected, but if you use Basic authentication over HTTP, the password can easily be recovered by eavesdropping. In production, always use it together with HTTPS.
Which should you choose, Basic authentication or form-based authentication?
When Basic authentication is suitable
- Protecting REST API endpoints
- Simple admin screens or development tools
- Stateless applications
- Cases where access from tools like curl or Postman is the primary use
When form-based authentication is suitable
- Web applications for general users
- Cases where access is primarily from browsers
- When you want to customize the login screen
- When session management is needed
In real projects, you can also switch configurations by environment using Spring Boot Profiles. For example, you can use Basic authentication in development and form-based authentication in production.
Next steps
In this article, we incrementally implemented Spring Security’s basic authentication features. From here, you can proceed to steps like the following.
- Database integration: Implement
UserDetailsServiceto retrieve user information from a database. If you want to securely store DB connection information and passwords, also consider how to encrypt sensitive configuration with Jasypt. - OAuth2/OpenID Connect: Login integration with external services such as Google and GitHub
- JWT authentication: Building stateless APIs with token-based authentication
- Authorization: Role-based access control (
@PreAuthorize, etc.) - Testing authenticated endpoints: Controllers that require authentication can be verified by combining
@WithMockUserand similar with unit testing Controllers with MockMvc. - Production deployment: To safely handle passwords and JWT signing keys in production, using Secret resources as introduced in how to deploy Spring Boot applications to Kubernetes is the standard approach.
These advanced topics will be covered in future articles. First, make sure to firmly understand the basics learned in this article and try them out in your own project.