Spring Security Fundamentals

Form Login & HTTP Basic

18 min Lesson 7 of 13

Form Login & HTTP Basic

Spring Security ships two ready-made authentication mechanisms that cover the most common web application patterns: Form Login for browser-driven user sessions, and HTTP Basic for machine-to-machine or API clients. In the previous lessons you configured the SecurityFilterChain and set up a UserDetailsService. Now you will wire those pieces to the actual login flows.

How Authentication Mechanisms Fit Into the Filter Chain

Every mechanism in Spring Security is implemented as a servlet filter sitting inside the SecurityFilterChain. When a request arrives, the chain walks through its filters in order. Two filters are relevant here:

  • UsernamePasswordAuthenticationFilter — intercepts POST /login, extracts credentials from the request body, and delegates to the AuthenticationManager.
  • BasicAuthenticationFilter — reads the Authorization: Basic <base64> header on every request and attempts authentication before the request reaches your controllers.

You do not instantiate these filters directly. Spring Security builds them when you call .formLogin() or .httpBasic() on the HttpSecurity DSL.

Configuring Form Login

The minimal configuration enables the built-in login page and wires all the defaults:

@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); // built-in /login page + POST handler return http.build(); }

With this alone, any unauthenticated request to a protected URL is redirected to /login, Spring renders its own login form, and on a successful POST to /login the user is redirected to the original URL (or /). On failure they go back to /login?error.

Customising the Form Login Flow

In real projects you almost always want your own login page, a custom success URL, and a specific failure handler:

.formLogin(form -> form .loginPage("/auth/login") // GET — your Thymeleaf/MVC view .loginProcessingUrl("/auth/login") // POST — Spring handles this .usernameParameter("email") // override default "username" .passwordParameter("pass") // override default "password" .defaultSuccessUrl("/dashboard", true) // always go here after login .failureUrl("/auth/login?error=true") // redirect on bad credentials .permitAll() // the login page itself is public )

The key distinction: loginPage() is a GET endpoint your MVC controller serves; loginProcessingUrl() is the POST endpoint Spring Security's filter intercepts — you write no controller method for it.

loginPage vs loginProcessingUrl: These can be the same path (as shown above) because HTTP method distinguishes them — GET renders the form, POST submits credentials. Spring Security registers the POST handler; you register the GET handler in your @Controller.

Logout Configuration

Form Login pairs naturally with a logout handler. Spring Security registers POST /logout by default, which invalidates the session and clears the SecurityContext. Customise it like this:

.logout(logout -> logout .logoutUrl("/auth/logout") .logoutSuccessUrl("/auth/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") )
Always use POST for logout. A GET-based logout endpoint is vulnerable to Cross-Site Request Forgery (CSRF): an attacker can embed an invisible <img src="/logout"> tag on any page and force a victim to log out. Spring Security enforces POST by default — do not change it to GET unless you also disable CSRF protection for that URL.

Configuring HTTP Basic

HTTP Basic is enabled with a single call:

@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .httpBasic(basic -> basic .realmName("My API") // shown in browser credential dialog ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // no server-side session ); return http.build(); }

The client encodes credentials as Base64(username:password) and sends them on every request inside the Authorization header. Spring Security decodes and verifies them against the UserDetailsService on each call.

HTTP Basic credentials are only Base64-encoded, not encrypted. Base64 is trivially reversible. You must use HTTPS in production to prevent credentials being captured in transit. Never deploy HTTP Basic over plain HTTP.

HTTP Basic in Distributed Systems

In microservices architectures, HTTP Basic is sometimes used for service-to-service authentication when the services are already inside a private network with mutual TLS, or when a dedicated secrets manager rotates credentials automatically. Its advantage is simplicity — no token exchange, no expiry, no refresh. Its disadvantage is exactly that: credentials are long-lived and cannot be revoked without redeploying all consumers. For anything user-facing or across a network boundary, prefer JWT Bearer tokens (covered in the next tutorial).

Combining Both Mechanisms

A common pattern is to serve a browser-facing UI with Form Login and a REST API with HTTP Basic (or JWT) from the same application, using two separate SecurityFilterChain beans with different securityMatcher patterns:

// Chain 1: REST API — stateless, HTTP Basic @Bean @Order(1) public SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .csrf(csrf -> csrf.disable()); // REST clients don't use cookies return http.build(); } // Chain 2: Web UI — stateful, Form Login @Bean @Order(2) public SecurityFilterChain webChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .defaultSuccessUrl("/dashboard", true) .permitAll() ) .logout(Customizer.withDefaults()); return http.build(); }

The @Order annotation is critical: Spring evaluates chains in ascending order, so the more specific matcher (/api/**) must come first. Without @Order, Spring Boot uses a default order and may apply the wrong chain.

Disable CSRF only where safe. CSRF attacks rely on browsers automatically attaching cookies to cross-site requests. Stateless REST clients (curl, mobile apps, other microservices) do not use session cookies, so CSRF protection adds no value for them. Browser-facing Form Login flows must keep CSRF enabled.

Remember Me

Form Login supports a "remember me" cookie that re-authenticates users across sessions without re-entering credentials:

.rememberMe(rm -> rm .key("uniqueAndSecret") // signs the token; change = all tokens invalidated .tokenValiditySeconds(86400) // 1 day .userDetailsService(userDetailsService) )

The token is a hash of username + expiry + password-hash + key. Changing the user's password or the key automatically invalidates all outstanding remember-me tokens — a useful property for forced logout across devices.

Summary

Form Login and HTTP Basic are the two foundational authentication mechanisms in Spring Security. Form Login is built for browser users: it manages the login page, POST processing, redirect-on-success, and session creation. HTTP Basic is built for programmatic clients: it reads credentials from a header on every request and works best as stateless. You configure both via the HttpSecurity DSL. In production, always pair them with HTTPS, and use separate SecurityFilterChain beans when a single application serves both browsers and API consumers.