Existing English articles keep code blocks verbatim (including Japanese comments and strings), so I’ll follow the rules exactly and translate only the prose.
If you have ever added Spring Security to a project for the first time and been confused when a login screen suddenly appeared, you are not alone.
This article walks developers who are new to Spring Security through the fundamentals of authentication, step by step. We start with the minimal setup, then implement Basic authentication and form-based authentication in turn, explaining what each setting means and where beginners typically get stuck.
What Is Spring Security?
Spring Security is the framework responsible for authentication (who you are) and authorization (what you can do) in Spring applications. The main authentication methods include Basic authentication, form-based authentication, OAuth2, and JWT. This article covers Basic authentication and form-based authentication.
Checking Spring Security’s Default Behavior
Let’s start by looking at the default behavior. Simply adding the following dependency to pom.xml automatically protects every endpoint.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
When you start the application, the console shows a log line like this:
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
This password changes on every startup. You can log in with it combined with the default username user, and when you access the application in a browser, an auto-generated login page is displayed.
Use this default password only during development. If you want to fix it, you can configure it in application.yml as shown below, but it must be disabled in production.
spring:
security:
user:
name: user
password: dev-password
Behind this auto-configuration is a mechanism called SecurityFilterChain, which we look at next.
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 a SecurityFilterChain as a @Bean and configure it using the Lambda DSL style.
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()); // この時点で認証方式を指定
return http.build();
}
}
When you register a SecurityFilterChain as a @Bean in a configuration class annotated with @Configuration, Spring Boot detects it automatically and applies it as the security configuration for the entire application. This is also a practical example of DI in action. The .httpBasic(withDefaults()) part specifies Basic authentication as the authentication method. Note that withDefaults() is functionally identical to httpBasic(); it is the recommended style in Spring Security 6 and later for explicitly enabling a feature with its default settings.
Extending WebSecurityConfigurerAdapter, which you often see in older articles, was deprecated in Spring Security 5.7 and removed in 6.0 (Spring Boot 3.0). Replace the configure(HttpSecurity) override with a SecurityFilterChain @Bean, replace user registration with a UserDetailsService @Bean, and rewrite antMatchers() as requestMatchers().
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 UserDetailsService Bean uses InMemoryUserDetailsManager to create a test user in memory. Note that we use BCryptPasswordEncoder from the start. The User.withDefaultPasswordEncoder() seen in older code examples has been 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
# ヘッダーを直接指定する場合(user:password を Base64 エンコードした値)
curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/hello
When you access the application in a browser, the standard authentication dialog appears. Keep in mind that Base64 is an encoding (a reversible transformation), not encryption. Over plain HTTP, the password can easily be recovered by eavesdropping, so in production always use Basic authentication together with HTTPS (TLS).
Why the Password Encoder Matters
Storing passwords in plain text is a serious problem: if the database leaks, every user’s password is exposed. When you register a PasswordEncoder as a @Bean, Spring Security automatically uses that Bean to verify passwords during authentication. A password encoded with BCryptPasswordEncoder takes a form like $2a$10$..., which contains the version, the salt, and the hash value.
The spring-security-crypto Module and DelegatingPasswordEncoder
Encoders such as BCryptPasswordEncoder live in a standalone module called spring-security-crypto. For batch jobs or admin tools that only need password hashing, you can add just this module as a dependency (no <version> is needed because the version is resolved by the Spring Boot BOM).
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
In practice, it is also recommended to use DelegatingPasswordEncoder rather than BCryptPasswordEncoder directly.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
This encoder stores an encoder ID such as {bcrypt} at the beginning of the hash string (for example, {bcrypt}$2a$10$...) and delegates verification to the encoder matching that ID. Even if you later migrate to a stronger algorithm (such as {argon2}), existing users’ passwords can still be verified. Because the default is BCrypt, swapping it in for the sample in this article does not change the behavior.
The BCrypt computational cost (strength) can be specified in the constructor. The default is 10, and each increment roughly doubles the computation time. Values around 10 to 12 are common in production. Even if BCrypt feels slow, never use NoOpPasswordEncoder, which is deprecated and dangerous.
Moving to Form-Based Authentication
The browser dialog used by Basic authentication is not very user-friendly, so form-based authentication is a better fit for web applications aimed at general users. Change the earlier SecurityFilterChain as follows (the PasswordEncoder and UserDetailsService Beans are the same as in the previous section).
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.defaultSuccessUrl("/home", true)
.permitAll()
);
return http.build();
}
We replaced .httpBasic(withDefaults()) with .formLogin(). defaultSuccessUrl("/home", true) specifies where to redirect after a successful login, and .permitAll() allows access to the login page itself (without it, you get an infinite redirect). The default login page is auto-generated at /login.
Customizing the Login Page
To use your own login page, configure it as follows.
@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();
}
.loginPage("/custom-login") specifies the custom login page, and .requestMatchers() permits access to static resources and the login page. The ** in .requestMatchers("/css/**") is Ant-style pattern matching and means “every path under /css/”.
.logout() configures the logout feature. With logoutSuccessUrl("/custom-login?logout"), the user returns to the login page after logging out, and the ?logout parameter can be used to display a success message.
Here is an example login page in 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>ログイン</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="login-container">
<h1>ログイン</h1>
<div th:if="${param.error}" class="error">
ユーザー名またはパスワードが正しくありません。
</div>
<div th:if="${param.logout}" class="success">
ログアウトしました。
</div>
<form th:action="@{/custom-login}" method="post">
<div>
<label for="username">ユーザー名:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">ログイン</button>
</form>
</div>
</body>
</html>
The key points are: specify the action attribute with th:action, method="post" is required, and the name attributes must be username and password by default. You can also display a login error message using ${param.error}. A POST form using th:action automatically gets a hidden CSRF token field, so there is no need to add one manually. If you place the CSS file at src/main/resources/static/css/style.css, it is served automatically as a static resource.
An Overview of the Main HttpSecurity Settings
To see what HttpSecurity can configure, here are the commonly used options gathered into a single SecurityFilterChain (this is an excerpt of the configuration methods; the imports for HttpMethod and SessionCreationPolicy are omitted).
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. URLごとの認可ルール(上から順に評価される)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.anyRequest().authenticated()
)
// 2. 認証方式(複数指定可)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home", true)
.permitAll()
)
.httpBasic(withDefaults())
// 3. ログアウト
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
// 4. CSRF(REST APIで無効化する場合。Webアプリでは有効のままにする)
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))
// 5. セッション管理
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
)
// 6. 未認証(401/リダイレクト)・権限不足(403)時の応答
.exceptionHandling(ex -> ex
.accessDeniedPage("/access-denied")
)
// 7. セキュリティヘッダー(H2コンソールをiframe表示したい場合など)
.headers(headers -> headers
.frameOptions(frame -> frame.sameOrigin())
);
return http.build();
}
Here is a summary of what each option does.
| Configuration method | Role | Notes |
|---|---|---|
authorizeHttpRequests() | Authorization rules per URL pattern | Evaluated top to bottom, so write narrower patterns first |
formLogin() / httpBasic() | Authentication method | Both can be used together if both are specified |
logout() | Session invalidation and redirect on logout | The default logout URL is POST /logout |
csrf() | CSRF token verification | Enabled by default. Consider disabling only for stateless REST APIs |
sessionManagement() | Session creation policy and concurrent login limit | Use STATELESS for stateless authentication such as JWT |
exceptionHandling() | AuthenticationEntryPoint for unauthenticated requests and AccessDeniedHandler for insufficient permissions | Used in REST APIs to return 401/403 as JSON |
headers() | Security headers such as X-Frame-Options | Safe defaults are applied automatically |
cors() | CORS configuration | Required when the frontend runs on a different origin |
csrf() is covered in detail in Understanding CSRF Protection in Spring Security - Configuration Differences Between REST APIs and Web Apps, and cors() in How to Configure CORS in Spring Boot.
Separating SecurityFilterChains for REST APIs and Web Pages
In real projects, requirements are often split, such as “/api/** uses stateless Basic authentication (or JWT), and everything else uses form-based authentication.” In that case, define multiple SecurityFilterChain @Beans, restrict the URLs each one handles with securityMatcher(), and specify the evaluation order with @Order.
@Configuration
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // このチェーンは /api/** のみ担当
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form.loginPage("/login").permitAll());
return http.build();
}
}
Chains are tried in ascending @Order value, and only the first matching chain is applied. A chain without securityMatcher() matches every request, so always place it last. Getting the order wrong leads to the classic problem of “I accessed /api/** but got redirected to the login screen.”
How the Authentication Flow Works
Both form-based and Basic authentication are processed internally through the following flow.
- An authentication filter (
UsernamePasswordAuthenticationFilter/BasicAuthenticationFilter) extracts the credentials from the request and creates an unauthenticatedAuthentication - The
AuthenticationManager(implemented byProviderManager) delegates to the registeredAuthenticationProviders in order DaoAuthenticationProviderloads the user viaUserDetailsService#loadUserByUsername()and verifies the password withPasswordEncoder#matches()- On success, the authenticated
Authenticationis stored inSecurityContextHolder, after which it can be accessed via@AuthenticationPrincipaland similar - On failure, an
AuthenticationExceptionis thrown, resulting in a 401 for Basic authentication or a redirect to/login?errorfor form-based authentication
The reason authentication worked in this article just by registering UserDetailsService and PasswordEncoder as @Beans is that auto-configuration detects these Beans and wires them into DaoAuthenticationProvider. To load users from a database, you only need to replace this UserDetailsService. The steps are explained in Implementing Database Authentication in Spring Security - UserDetailsService and JdbcUserDetailsManager.
The AuthenticationEntryPoint determines the response when a protected resource is accessed without authentication. With httpBasic(), it returns 401 with a WWW-Authenticate: Basic header (which is what prompts the browser to show the authentication dialog); with formLogin(), it returns a 302 redirect to /login. If you want a REST API to return a JSON error body, configure a custom AuthenticationEntryPoint (import HttpServletResponse from the jakarta.servlet.http package).
http
.httpBasic(basic -> basic.realmName("my-api"))
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"unauthorized\"}");
})
);
Common Configuration Errors for Beginners and How to Fix Them
Here are typical errors you are likely to encounter when getting started.
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 the password.
2. Infinite redirect to the login page
If the login page itself requires authentication, you get an infinite redirect. Call .permitAll() inside .formLogin(), and additionally permit it explicitly with .requestMatchers("/custom-login").permitAll().
3. CSRF token errors
Using Thymeleaf’s th:action embeds the CSRF token automatically. If you write the HTML by hand, add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>.
Enabling debug logging
Debug logging, which outputs detailed internal behavior, is useful for troubleshooting. Add the following to application.yml.
logging:
level:
org.springframework.security: DEBUG
Choosing an Authentication Method
Should you choose Basic authentication or form-based authentication? Basic authentication is a good fit for protecting REST API endpoints, simple admin screens and development tools, and stateless applications accessed mainly from tools such as curl or Postman. On the other hand, for browser-based web applications aimed at general users that need a customized login screen and session management, choose form-based authentication.
Note that httpBasic() and formLogin() can coexist in the same chain: browsers get form-based authentication, while requests with an Authorization: Basic ... header get Basic authentication. However, this can easily cause confusion, so it is safer to separate the chains for the REST API and the web UI as shown earlier.
You can also use Spring Boot Profiles to switch configuration by environment, using Basic authentication in development and form-based authentication in production.
Next Steps
First, try the basics implemented in this article in your own project, then move on to the following articles depending on your requirements.
- For database integration, see Implementing Database Authentication in Spring Security - Persisting Users with UserDetailsService and JdbcUserDetailsManager. For storing sensitive information, you can also use How to Encrypt Sensitive Information in Configuration Files with Jasypt
- For login via external services, see How to Implement Google Login (OAuth2) in Spring Boot. For protecting APIs with tokens, see How to Configure Spring Boot as an OAuth2 Resource Server
- For token-based stateless authentication, see How to Implement Stateless Authentication with Spring Security + JWT
- For role-based access control, see Spring Security Method Security - Implementing RBAC with @PreAuthorize
- For how CSRF protection works and when it is safe to disable it, see Understanding CSRF Protection in Spring Security
- For testing authenticated endpoints, Unit Testing Controllers with MockMvc can be combined with
@WithMockUserand similar annotations - For handling passwords and JWT signing keys safely in production, the Secret resource introduced in How to Deploy a Spring Boot Application to Kubernetes is the standard approach