Spring Security: HTTP Basic and Form-Based Login Authentication
Objective
HTTP Basic and form-based login are Spring Security's two built-in ways to collect a username and password from a client. HTTP Basic is the simplest possible mechanism — credentials ride along on every request as a header — which makes it excellent for demos, proofs of concept, and machine-to-machine calls, but a poor fit for anything with a browser-facing UI. Form-based login trades that simplicity for exactly what a small web application needs: an actual login page, a session that remembers the authenticated user across requests, and a logout flow — all autoconfigured with a single method call, then customizable in layers as real requirements show up.
Use Cases
- Securing a small web application end to end: an unauthenticated visitor is redirected to a login form, and after a successful login, is sent back to the page they originally tried to reach.
- Returning a custom error response (a specific header, a different HTTP status, a request ID for tracing) instead of Spring Security's default behavior when authentication fails, for either HTTP Basic or form login.
- Redirecting different users to different pages after a successful login, based on their granted authorities.
- Supporting both authentication methods on the same application at once — HTTP
Basic for API/tooling clients using
curl, form login for browser users — with a single security configuration.
Deep Dive
HTTP Basic: minimal setup, then a custom realm and entry point
The bare-minimum way to require HTTP Basic authentication is a single call:
java@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
}
}httpBasic() also accepts a Customizer<HttpBasicConfigurer<HttpSecurity>>,
which is how finer details — like the realm name sent back in the
WWW-Authenticate header on a failed request — get set:
java@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic(c -> {
c.realmName("OTHER");
});
http.authorizeRequests().anyRequest().authenticated();
}A curl -v against a protected endpoint with no credentials now shows
WWW-Authenticate: Basic realm="OTHER" — but only on a 401 Unauthorized
response; a successful 200 OK never carries that header at all.
Customizing an authentication failure: AuthenticationEntryPoint
Beyond the realm name, a completely custom response body or header set on
authentication failure needs an AuthenticationEntryPoint. Its commence()
method receives the request, the response, and the AuthenticationException
that triggered the failure:
javapublic class CustomEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e)
throws IOException, ServletException {
httpServletResponse.addHeader("message", "Luke, I am your father!");
httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value());
}
}Registered alongside the realm name:
java@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic(c -> {
c.realmName("OTHER");
c.authenticationEntryPoint(new CustomEntryPoint());
});
http.authorizeRequests().anyRequest().authenticated();
}AuthenticationEntryPoint is invoked by ExceptionTranslationManager, the
component that bridges Java exceptions thrown inside the filter chain
(AuthenticationException, AccessDeniedException) back into HTTP responses
— the name doesn't obviously suggest "runs on authentication failure," which is
worth knowing before hunting for a differently-named interface.
Form login: an autoconfigured login page, with zero HTML written
Switching httpBasic() for formLogin() is enough to get a working login page,
session handling, and a logout endpoint, with no HTML written by the
application:
java@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin();
http.authorizeRequests().anyRequest().authenticated();
}
}Without a registered UserDetailsService, the default user/generated-UUID
credentials (the same ones introduced in earlier chapters) log in against this
form exactly as they would against HTTP Basic. A protected page still needs a
normal @Controller (not @RestController) returning a view name, so the
response is renderable HTML rather than a JSON body:
java@Controller
public class HelloController {
@GetMapping("/home")
public String home() {
return "home.html";
}
}An unauthenticated visit to /home is redirected to the login form first; after
a successful login, Spring Security sends the browser back to /home — the page
originally requested — rather than to a fixed landing page.
Redirecting after login: defaultSuccessUrl and the two handler interfaces
formLogin() returns a FormLoginConfigurer<HttpSecurity>, whose
defaultSuccessUrl() fixes the post-login destination regardless of which page
triggered the login redirect:
java@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.defaultSuccessUrl("/home", true);
http.authorizeRequests().anyRequest().authenticated();
}For logic that depends on who logged in — different redirects per granted
authority, for instance — AuthenticationSuccessHandler gives full control
over the response:
java@Component
public class CustomAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication)
throws IOException {
var authorities = authentication.getAuthorities();
var auth = authorities.stream()
.filter(a -> a.getAuthority().equals("read"))
.findFirst();
if (auth.isPresent()) {
httpServletResponse.sendRedirect("/home");
} else {
httpServletResponse.sendRedirect("/error");
}
}
}Its mirror image, AuthenticationFailureHandler, does the equivalent for a
failed login — here, stamping a timestamp header on every failed attempt:
java@Component
public class CustomAuthenticationFailureHandler
implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) {
httpServletResponse.setHeader("failed", LocalDateTime.now().toString());
}
}Both are registered the same way, on the FormLoginConfigurer:
java@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
http.authorizeRequests().anyRequest().authenticated();
}Running both methods together
Once formLogin() is configured, HTTP Basic credentials on their own stop
working — every unauthenticated request is redirected to the login form instead
(302 Found), even with a valid Authorization header attached. Chaining
.httpBasic() after formLogin() re-enables both at once:
java@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.httpBasic();
http.authorizeRequests().anyRequest().authenticated();
}With both active, a browser gets the login form as before, and a curl -u user:password call authenticates over HTTP Basic on the very same endpoint.
Trade-offs
- HTTP Basic sends credentials on every single request, in a header decoded with nothing more than Base64 — fine over TLS for scripted/API clients that already manage credentials securely, a poor fit for anything a human types into a browser, since there's no session, no logout, and no login page to build trust or add extra factors.
- Form login trades HTTP Basic's statelessness for a server-side session — the book is explicit that this fits a small application, not one that needs horizontal scalability, since a server-side session ties a user to whichever node holds it (or requires a shared session store to fix that). The book points forward to OAuth 2 (its chapters 12-15) as the answer for that case.
AuthenticationEntryPoint's name doesn't describe what it does — it's invoked specifically on authentication failure, viaExceptionTranslationManager, not on every request; reaching for a differently-named interface when hunting for "customize the failed-auth response" is an easy first guess to get wrong.- Combining
formLogin()andhttpBasic()isn't automatic — the presence of one silences the other unless both are explicitly chained. A team assuming "HTTP Basic still works because I never removed it" after addingformLogin()will get a302redirect instead of the401/success they expect, until.httpBasic()is added back alongside it. - Book vs. today: the
.and()-chained fluent style this section relies on throughout (formLogin()...and().httpBasic()) is scheduled for removal in Spring Security 7, in favor of the Lambda DSL exclusively — confirmed via the current Spring Security migration documentation. The equivalent configuration today reads:
Spring Security 7 also drops the no-argumentjavahttp .formLogin(form -> form .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler) ) .httpBasic(Customizer.withDefaults());httpBasic()/formLogin()calls this section uses for the minimal-configuration examples — aCustomizerargument becomes mandatory, withCustomizer.withDefaults()as the explicit stand-in for "use the defaults." The underlying mechanics (realm name,AuthenticationEntryPoint,defaultSuccessUrl, success/failure handlers) are unchanged — only the configuration syntax wrapping them is affected, the sameWebSecurityConfigurerAdapter-to-SecurityFilterChain-bean migration already noted elsewhere in this workflow.
Documentation Links
- Laurențiu Spilcă, "Spring Security in Action" (Manning, 2020) — Chapter 5, "Implementing authentication", section 5.3, p. 125-133
- Spring Security Reference — Basic Authentication
- Spring Security Reference — Form Login
- Spring Security Reference — Configuration Migrations (Spring Security 7, .and() removal, mandatory Customizer)