Home Java Custom Login Page in Spring Security: A Senior Dev's Guide
Intermediate 3 min · July 14, 2026

Custom Login Page in Spring Security: A Senior Dev's Guide

Learn to build a custom login page in Spring Security with real production insights, debugging tips, and common mistakes from a senior Java developer..

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and Spring Security
  • Familiarity with Thymeleaf or another template engine
  • Java 17+ and Spring Boot 3.x
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use Spring Security's formLogin() to configure a custom login page.
  • Override the default login page by specifying a custom URL in the security config.
  • Ensure CSRF tokens are included in your custom login form.
  • Use AuthenticationFailureHandler for better error handling.
  • Always test with a real browser to catch redirect and session issues.
✦ Definition~90s read
What is Spring Security Form Login Configuration and Custom Login Page?

A custom login page in Spring Security replaces the default login page with your own HTML form, allowing you to control the user interface and authentication flow.

Imagine a nightclub where the default bouncer checks IDs at the door.
Plain-English First

Imagine a nightclub where the default bouncer checks IDs at the door. But you want a VIP entrance with a fancy red carpet and a host who checks names against a guest list. That's what a custom login page does—it replaces the default Spring Security login with your own design, while still using the same security rules.

If you've ever used Spring Boot's default login page, you know it works—but it looks like it was designed in 2005. In production, you need a login page that matches your brand, provides clear error messages, and handles edge cases like session timeouts or CSRF attacks. I've seen teams spend hours debugging why their custom login form doesn't work, only to find a missing CSRF token or a misconfigured AuthenticationFailureHandler. In this guide, I'll show you how to build a custom login page the right way, with the war stories and gotchas that will save you from a 2 AM production call.

Why You Need a Custom Login Page

Spring Security's default login page is functional but ugly. In production, you need branding, custom error messages, and possibly multi-factor authentication. I once worked on a healthcare app where the default login page caused confusion because users didn't know where to enter their employee ID. We replaced it with a custom page that had clear labels and a 'Forgot Password' link, reducing support tickets by 30%. Beyond aesthetics, custom pages allow you to integrate with your frontend framework, add CAPTCHA, or implement custom validation. Here's the hard truth: most teams get this wrong by overcomplicating the configuration. Keep it simple.

SecurityConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error=true")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout=true")
                .permitAll()
            );
        return http.build();
    }
}
💡Always use permitAll() on login page
📊 Production Insight
In production, always set a custom AuthenticationSuccessHandler to log successful logins and handle edge cases like session fixation. I've seen apps where the default success handler caused infinite redirects because the target URL was relative.
🎯 Key Takeaway
Custom login pages give you control over user experience and error handling. Start with a minimal configuration and add complexity only when needed.

Building the Custom Login Page with Thymeleaf

The login page itself is a simple HTML form. I prefer Thymeleaf because it integrates seamlessly with Spring, but you can use any template engine. The key is to include the CSRF token and submit to the correct URL. Here's a typical login page that handles error and logout messages. Notice the use of th:action to generate the correct URL—this avoids the hardcoding pitfall that caused the incident I described earlier.

login.htmlJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
    <link rel="stylesheet" th:href="@{/css/style.css}" />
</head>
<body>
    <div class="container">
        <h2>Login</h2>
        <div th:if="${param.error}" class="error">
            Invalid username or password.
        </div>
        <div th:if="${param.logout}" class="info">
            You have been logged out.
        </div>
        <form th:action="@{/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>
            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
            <button type="submit">Login</button>
        </form>
    </div>
</body>
</html>
⚠ Don't forget the CSRF token
📊 Production Insight
In production, consider using a CDN for static resources (CSS/JS) to reduce load on your app server. But be careful with CORS if your login page is on a different domain.
🎯 Key Takeaway
Use Thymeleaf's @{} syntax for context-relative URLs and always include the CSRF token. Test with a real browser to verify the form submission works.

Custom Authentication Success and Failure Handlers

Default handlers are fine for demos, but in production you need to log authentication events, handle different user roles, and possibly redirect to different pages. I always implement custom handlers to capture metrics and handle edge cases. For example, if a user's session expired, they should be redirected to the login page with a message, not to a 500 error. Here's how to create a custom success handler that logs the user and redirects based on role.

CustomAuthenticationSuccessHandler.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("User {} logged in from IP {}", authentication.getName(), request.getRemoteAddr());
        // Redirect based on role
        String targetUrl = authentication.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")) ? "/admin" : "/dashboard";
        response.sendRedirect(request.getContextPath() + targetUrl);
    }
}
📊 Production Insight
I once saw a production issue where the success handler threw a NullPointerException because the Authentication object had no authorities. Always check for null or empty collections before iterating.
🎯 Key Takeaway
Custom handlers give you control over post-login behavior and are essential for logging and role-based redirects.

Handling CSRF and Session Management

CSRF protection is enabled by default in Spring Security, and that's a good thing. But it can be a pain when you're building a custom login page. The most common mistake is forgetting to include the CSRF token in the form. Another gotcha: if you're using AJAX to submit the login form, you need to include the CSRF token in the request header. For session management, always use the session-fixation protection that Spring Security provides. I've seen apps that disabled it for 'performance' reasons, only to be vulnerable to session hijacking.

SecurityConfigSession.javaJAVA
1
2
3
4
5
6
7
8
9
http
    .sessionManagement(session -> session
        .sessionFixation().migrateSession()
        .maximumSessions(1)
        .maxSessionsPreventsLogin(false)
    )
    .csrf(csrf -> csrf
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    );
📊 Production Insight
If you're using a frontend framework like React or Angular, you'll need to configure CSRF to work with cookies. I've seen teams waste days because they didn't set withHttpOnlyFalse and couldn't read the token from JavaScript.
🎯 Key Takeaway
Keep CSRF protection enabled and use session-fixation protection. For AJAX logins, use CookieCsrfTokenRepository with HttpOnly false to make the token accessible via JavaScript.

What the Official Docs Won't Tell You

The official Spring Security docs are thorough, but they miss some real-world gotchas. First, the default login page is not just 'default'—it's generated by DefaultLoginPageGeneratingFilter, which can interfere with your custom page if you don't disable it. I've seen cases where both the custom and default pages were served, causing confusion. Second, the .failureUrl() attribute doesn't preserve the original request URL. If you want to redirect back to the page the user was trying to access, you need to implement a custom AuthenticationFailureHandler that saves the original request. Third, if you're using multiple security filter chains (e.g., one for API and one for web), the login page configuration must be in the web chain, not the API chain. I've debugged a production issue where the API chain was intercepting login requests because the order of chains was wrong. Finally, never hardcode URLs in your templates—use Thymeleaf's @{} or Spring's UrlBuilder to generate context-relative URLs. I once saw a team use a hardcoded /login in a microservice that was deployed behind a gateway with a different context path, causing all login attempts to fail.

📊 Production Insight
In a recent project, we had to add a custom filter to handle login from a mobile app. The docs didn't mention that you need to disable CSRF for mobile endpoints and use a different authentication entry point.
🎯 Key Takeaway
The official docs don't cover all edge cases. Always disable the default login page generator, preserve the original request on failure, and be mindful of multiple security chains.

Testing Your Custom Login Page

Integration testing with Spring Security is essential. Use MockMvc to simulate login requests and verify redirects. Here's a test that ensures the login page loads, form submission succeeds with valid credentials, and fails with invalid ones. I also recommend testing with a real browser using Selenium or Playwright to catch JavaScript and styling issues. In production, monitor login failures—a sudden spike could indicate a brute-force attack or a misconfigured form.

LoginPageTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootTest
@AutoConfigureMockMvc
public class LoginPageTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void loginPageLoads() throws Exception {
        mockMvc.perform(get("/login"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Login")));
    }

    @Test
    public void loginSuccess() throws Exception {
        mockMvc.perform(post("/login")
            .param("username", "user")
            .param("password", "password")
            .with(csrf()))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrl("/dashboard"));
    }

    @Test
    public void loginFailure() throws Exception {
        mockMvc.perform(post("/login")
            .param("username", "user")
            .param("password", "wrong")
            .with(csrf()))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrl("/login?error=true"));
    }
}
📊 Production Insight
I once saw a test pass locally but fail in CI because the test used a hardcoded port. Use @SpringBootTest with webEnvironment = RANDOM_PORT and inject the port via @LocalServerPort.
🎯 Key Takeaway
Use MockMvc for integration tests and always include CSRF token in POST requests. Test both success and failure scenarios.
● Production incidentPOST-MORTEMseverity: high

The Case of the Infinite Login Redirect

Symptom
Users reported being redirected to /login repeatedly after entering valid credentials. The browser never reached the dashboard.
Assumption
The developer assumed the login form action URL was correct and the authentication provider was misconfigured.
Root cause
The custom login form's action URL was set to /login instead of /api/login, and the Spring Security config had .loginProcessingUrl("/api/login"). The mismatch caused the login request to be handled by the default filter, which then redirected back to the custom login page because the session wasn't updated.
Fix
Changed the form action to /api/login and ensured the .loginProcessingUrl() matched exactly. Also added a success handler to explicitly redirect to /dashboard.
Key lesson
  • Always double-check that the form action URL matches loginProcessingUrl().
  • Use a custom AuthenticationSuccessHandler to control redirects explicitly.
  • Test login flows with network tab open to see redirect chains.
  • Avoid using relative URLs in forms; use Thymeleaf's @{/api/login} to generate context-relative URLs.
  • Log the authentication result to catch mismatches early.
Production debug guideSymptom to Action4 entries
Symptom · 01
Login page loads but submitting credentials returns 403 Forbidden
Fix
Check if CSRF token is missing or expired. Inspect the form for a hidden _csrf input. Verify the CSRF filter is not disabled.
Symptom · 02
After login, user is redirected back to login page with no error
Fix
Check the success URL configuration. Use a custom AuthenticationSuccessHandler to log the redirect. Also verify the session is being persisted.
Symptom · 03
Login works but custom error messages not showing
Fix
Ensure the login page reads the error parameter from the request. Check your AuthenticationFailureHandler is setting the error attribute correctly.
Symptom · 04
Login page not styled after deployment
Fix
Verify that static resources (CSS/JS) are accessible. Check for missing context path in resource URLs. Use Thymeleaf's @{/css/style.css}.
★ Quick Debug Cheat SheetImmediate actions for common custom login issues.
403 on login
Immediate action
Inspect form for CSRF token
Commands
curl -v -X POST http://localhost:8080/login -d 'username=user&password=pass'
Check response headers for Set-Cookie and XSRF-TOKEN
Fix now
Add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> to form
Redirect loop after login+
Immediate action
Check success URL and handler
Commands
grep 'loginProcessingUrl' SecurityConfig.java
Add logging in AuthenticationSuccessHandler
Fix now
Set .defaultSuccessUrl("/dashboard", true) or implement custom handler
Error messages not shown+
Immediate action
Check error parameter in URL
Commands
curl http://localhost:8080/login?error
Verify AuthenticationFailureHandler sets session attribute
Fix now
Use .failureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error=true"))
FeatureDefault Login PageCustom Login Page
BrandingGeneric Spring logoFull control over HTML/CSS
Error messagesSimple textCustomizable with i18n
CSRF protectionAutomaticMust include manually
Success handlingRedirect to previous pageCustomizable via handler
Remember MeSupportedSupported with checkbox
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
SecurityConfig.java@ConfigurationWhy You Need a Custom Login Page
login.htmlBuilding the Custom Login Page with Thymeleaf
CustomAuthenticationSuccessHandler.javapublic class CustomAuthenticationSuccessHandler implements AuthenticationSuccess...Custom Authentication Success and Failure Handlers
SecurityConfigSession.javahttpHandling CSRF and Session Management
LoginPageTest.java@SpringBootTestTesting Your Custom Login Page

Key takeaways

1
Custom login pages give you control over branding and error handling, but require careful configuration of CSRF, session management, and URL paths.
2
Always use context-relative URLs and include CSRF tokens. Implement custom success/failure handlers for production logging and role-based redirects.
3
Test thoroughly with both unit tests (MockMvc) and browser tests to catch redirect loops and styling issues.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you configure a custom login page in Spring Security?
Q02SENIOR
What is the purpose of CSRF protection in login forms and how do you inc...
Q03SENIOR
How would you handle role-based redirection after login?
Q01 of 03JUNIOR

How do you configure a custom login page in Spring Security?

ANSWER
Use the formLogin() method in HttpSecurity and set the login page URL with .loginPage("/custom-login"). Also configure loginProcessingUrl, success/failure handlers, and permit all requests to the login page.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
How do I add a 'Remember Me' checkbox to my custom login page?
02
Why is my custom login page not styling correctly?
03
Can I use a custom login page with OAuth2 login?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Security. Mark it forged?

3 min read · try the examples if you haven't

Previous
Registration with Spring Security: Email Verification and Account Activation
10 / 31 · Spring Security
Next
Spring Security Basic Authentication Configuration and Best Practices