How to Build an Extensible Form Login with Spring Security

This tutorial walks through customizing Spring Security to create a scalable, extensible form‑login mechanism, covering the basic login flow, Spring Security's built‑in login options, FormLoginConfigurer settings, practical controller code, security configuration, and how to add multiple login types such as JSON or CAPTCHA.

Programmer DD
Programmer DD
Programmer DD
How to Build an Extensible Form Login with Spring Security

1. Introduction

Previous articles about Spring Security were a warm‑up. The first step of secure access is authentication, and the first step of authentication is login. This article shows how to customize Spring Security to create an extensible, scalable form‑login.

2. Form login flow

Basic flow of a form login is illustrated below:

Any form login can be transformed into this flow. Next we see how Spring Security handles it.

3. Login in Spring Security

In the previous “WebSecurityConfigurerAdapter” article we saw that custom access control is usually built with HttpSecurity. Spring Security provides three login methods: formLogin() – standard form login oauth2Login() – OAuth2.0 authentication/authorization openidLogin() – OpenID authentication

All three are implemented by AbstractAuthenticationFilterConfigurer.

4. Form login with HttpSecurity

Form login can be enabled in two ways: by constructing a custom AbstractAuthenticationFilterConfigurer via HttpSecurity.apply(...) (advanced) or by using the common http.formLogin() which creates a FormLoginConfigurer. The article focuses on the latter.

4.1 FormLoginConfigurer

The class provides several configuration methods: loginPage(String) – custom login page (default “/login”) loginProcessingUrl(String) – URL that receives the username/password (handled by UsernamePasswordAuthenticationFilter) usernameParameter(String) – name of the username parameter (default “username”) passwordParameter(String) – name of the password parameter (default “password”) failureUrl(String) – redirect on failure (rarely used in SPA) failureForwardUrl(String) – forward on failure (can be handled by a controller) defaultSuccessUrl(String, boolean) – redirect after success; alwaysUse determines whether it is always used successForwardUrl(String) – forward on success (similar to defaultSuccessUrl with alwaysUse=true) successHandler(AuthenticationSuccessHandler) – custom success handling failureHandler(AuthenticationFailureHandler) – custom failure handling permitAll(boolean) – whether the form login endpoint is publicly accessible

With these options we can build a customized login.

5. Practical implementation

Endpoints must return JSON on success or failure. Example controller:

@RestController
@RequestMapping("/login")
public class LoginController {
    @Resource
    private SysUserService sysUserService;

    @PostMapping("/failure")
    public Rest loginFailure() {
        return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
    }

    @PostMapping("/success")
    public Rest loginSuccess() {
        User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = principal.getUsername();
        SysUser sysUser = sysUserService.queryByUsername(username);
        sysUser.setEncodePassword("[PROTECT]");
        return RestBody.okData(sysUser, "登录成功");
    }
}

Custom security configuration disables CSRF and sets the processing URL, success and failure forwards:

@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

    @Configuration
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            super.configure(auth);
        }

        @Override
        public void configure(WebSecurity web) throws Exception {
            super.configure(web);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                .cors()
                .and()
                .authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/process")
                .successForwardUrl("/login/success")
                .failureForwardUrl("/login/failure");
        }
    }
}

Testing with Postman:

http://localhost:8080/process?username=Felordcn&password=12345

returns user data with status 200. Changing the password returns status 401 with an error message.

6. Extensible login types

To support multiple login styles (JSON, CAPTCHA, etc.) an adapter is added before UsernamePasswordAuthenticationFilter. Define an enum:

public enum LoginTypeEnum {
    /** Original form login */
    FORM,
    /** JSON submission */
    JSON,
    /** CAPTCHA */
    CAPTCHA
}

Define a LoginPostProcessor interface to obtain username and password for each type, and implement a default processor for FORM. Then create a PreLoginFilter that selects the processor based on a request parameter login_type and populates the standard Spring Security parameters before delegating to the filter chain.

public class PreLoginFilter extends GenericFilterBean {
    private static final String LOGIN_TYPE_KEY = "login_type";
    private RequestMatcher requiresAuthenticationRequestMatcher;
    private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();

    public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
        // initialization logic…
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ParameterRequestWrapper wrapper = new ParameterRequestWrapper((HttpServletRequest) request);
        if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
            LoginTypeEnum type = getTypeFromReq(request);
            LoginPostProcessor processor = processors.get(type);
            String username = processor.obtainUsername(request);
            String password = processor.obtainPassword(request);
            wrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
            wrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
        }
        chain.doFilter(wrapper, response);
    }
}

Submitting a POST request with login_type=0 (FORM) works, and additional login types can be added by implementing LoginPostProcessor and registering it with the filter.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AuthenticationCustom Filterform login
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.