Search code examples
spring-bootspring-mvcspring-securitythymeleafhttp-status-code-404

Directs to default login form, despite having a custom login form. Spring boot (2.7.2) + Spring security(5.4) + SecurityFilterChain + Thymeleaf


I'm trying to create a simple spring boot app with spring security, with custom login form.

Issue: The app directly opens the default login page instead of the custom login view & it could not resolve any of the other thymeleaf views too.

Directly accessing all other views (/home, /error) renders the error: This localhost page can't be found. Opening http://localhost:8080/login takes only to the default login page & not the custom one.

Note: The html templates are placed under /src/main/resources/templates folder - which is the default place springboot looks into.

The key thing here is that I am using the @EnableWebSecurity annotation, with SecurityFilterChain bean (which is introduced in Spring Security 5.4), like this:


@EnableWebSecurity
public class WebSecurityConfig {
      @Bean
      public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    ...
}

instead of the more common (but, has been deprecated since Spring Security 5.7.0-M2) way of extending the WebSecurityConfigurerAdapter class as below.

 @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    ...
    }

It seems the latter works fine without any issues.

Few of the solutions suggested in various forums - which I have already tried.

  1. Keep the project structure in a way that all the other packages are placed under the base package (the one with the main class with @SprintBootApplication annotation).
  2. Use @ComponentScan={<your_base_package>} - if packages are in the order mentioned in point 1. Or, @ComponentScan={<package-1>, <package-2>, etc}, if the packages are independent of each other.
    Both the above solutions were suggested to avoid the 404 error & view not resolved issues.
  3. Use @RestController instead of @Controller.
    This was suggested both for WhiteLabel error, and when the view name is returned just as a string, instead of a view.
  4. Keep the mapping url value in the controller methods (like, /login) & the the view name different. If the mapping url as /login, change the view name as loginpage.html (or, something different).
    This was suggested for circular path issues - when resolving view names.
  5. Some suggested using @RequestMapping("/login") at class-level, rather than method-level. Although, I didn't see any difference with either approach.

Note that all of the above are based on WebSecurityConfigurerAdapter & not on SecurityFilterChain.

The only references from the official documentation/blogs, that I could find for this requirement (custom login with SecurityFilterChain) were these two:

i. https://docs.spring.io/spring-security/site/docs/4.1.3.RELEASE/guides/html5/form-javaconfig.html
ii. https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

Unfortunately, performing the steps given there didn't get the result. I think the below issue is the same (or, related) to this, but no solution was given to that either.
https://github.com/spring-projects/spring-security/issues/10542

And, almost all the other git/blogs/websites/video references available, are using the WebSecurityConfigurerAdapter class only.

Web Security Config class:

@EnableWebSecurity
public class WebSecurityConfig {
//  @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
            .antMatcher("/**")
            .authorizeRequests()
                .antMatchers("/", "/home", "/login**","/callback/", "/webjars/**", "/css**", "/error**")
                .permitAll()
            .anyRequest()
                .authenticated()
            .and()
            .formLogin()
//                  .loginPage("/loginpage")
                    .usernameParameter("email")
                    .passwordParameter("password")
                    .loginPage("/login").loginProcessingUrl("/login")
                    .permitAll()
                    .defaultSuccessUrl("/home")
                    .failureUrl("/login?message=error")
             .and()
//           .logout()
//              .logoutUrl("/perform_logout")
//              .logoutSuccessUrl("/login?message=logout");
             .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .permitAll();
        
            return http.build();
    }
}

Login Controller:
(Leaving some of the commented code, to hint what else has been tried, so far).
At some point in time, the control entered into the login method & printed the model object's value (string). However, the login view still was not getting resolved & resulted in 404 error only.

//@RestController
@Controller
@ResponseBody
//@RequestMapping("/")
public class LoginController {
    
    @GetMapping({"/", "/home"})
//  public String home() {
//  @GetMapping({"/", "/showHome"})
    public ModelAndView home(ModelAndView mav) {
        System.out.println("Inside GetMapping(/home) method of LoginController.");
        mav.setViewName("home");
        mav.addObject("Using @Controller and @ResponseBody, in the Controller");
        System.out.println("View Object: " + mav.getView());
        System.out.println("View Name: " + mav.getViewName());
        System.out.println("mav.hasView(): " + mav.hasView());
        return mav;
//      return "home";
    }
    
    @GetMapping("/login-error")
//  @GetMapping("/error")
    @RequestMapping("/login")
    public String login(HttpServletRequest request, Model model) {
        HttpSession session = request.getSession(false);
        String errorMessage = null;
        if (session != null) {
            AuthenticationException ex = (AuthenticationException) session
                    .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            if (ex != null) {
                errorMessage = ex.getMessage();
            }
        }
        System.out.println("--->" + errorMessage);
        model.addAttribute("errorMessage", errorMessage);
        return "login";
    }
    
}

I added a main configuration for MVC too, although I believe these are the default configs which Springboot assumes itself & can work even without this.

Main Configuration class:

@Configuration
@ComponentScan("vlan.test.springboot.customLogin")
public class MainConfiguration implements WebMvcConfigurer {
    
        private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
                "classpath:/static/**", "classpath:/public/**", "classpath:/templates/**", "classpath:/resources/**"
        };
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
            registry.addResourceHandler("/templates/**").addResourceLocations("/templates/");
            registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
        }
        
        @Override
        public void addViewControllers(ViewControllerRegistry viewRegistry) {
            viewRegistry.addViewController("/").setViewName("home");
            viewRegistry.addViewController("/home").setViewName("home");
            viewRegistry.addViewController("/login").setViewName("login");
        }
    }

Gradle build file:

implementation 'org.springframework.boot:spring-boot-devtools'<br/>
implementation 'org.springframework.boot:spring-boot-starter-web'<br/>
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'<br/>
implementation 'org.springframework.boot:spring-boot-starter-security'<br/>
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'<br/>
implementation 'org.zalando:logbook-spring-boot-starter:2.14.0'

Project structure:

ProjectStructure

Extract from the logs - that I found relative/worth-noting.
Check the "..invalid session id..." and "..Failed to authorize filter invocation..." parts.

2022-08-18 12:09:43.297  INFO 16596 --- [http-nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 10 ms
2022-08-18 12:09:43.334 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing GET /
2022-08-18 12:09:43.351 DEBUG 16596 --- [http-nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
2022-08-18 12:09:43.363 DEBUG 16596 --- [http-nio-8080-exec-1] ****o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
2022-08-18 12:09:43.364 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.session.SessionManagementFilter  : Request requested invalid session id EF53E44688D581C69527A5442A987DB6
2022-08-18 12:09:43.394 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.a.i.FilterSecurityInterceptor    : Failed to authorize filter invocation [GET /] with attributes [authenticated]
2022-08-18 12:09:43.445 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/ to session****
2022-08-18 12:09:43.448 DEBUG 16596 --- [http-nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using And [Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy@51020050, matchingMediaTypes=[application/xhtml+xml, image/*, text/html, text/plain], useEquals=false, ignoredMediaTypes=[*/*]]]
2022-08-18 12:09:43.450 DEBUG 16596 --- [http-nio-8080-exec-1] s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@416ee886
2022-08-18 12:09:43.454 DEBUG 16596 --- [http-nio-8080-exec-1] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/login
2022-08-18 12:09:43.459 DEBUG 16596 --- [http-nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-08-18 12:09:43.466 DEBUG 16596 --- [http-nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
2022-08-18 12:09:43.466 DEBUG 16596 --- [http-nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-08-18 12:09:43.487 DEBUG 16596 --- [http-nio-8080-exec-2] o.s.security.web.FilterChainProxy        : Securing GET /login

Complete log here: https://codeshare.io/dwlLDK
(Seems it'd live for only 24 hrs. Pls. let me know if you couldn't access it).

Edit(2022-08-22): An excerpt from the application.properties file (just the thymeleaf & security config) - which is the reason for the issue (as explained in the accepted [self-identified] answer).

# thymeLeaf
spring.thymeleaf.enabled=true
spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates
spring.thymeleaf.suffix=.html

#security
server.error.whitelabel.enabled=false

Solution

  • I fixed the issue myself.

    (Adding it as an accepted answer, so that it's easy for people who stumble upon this page.)

    The actual issue was a missing trailing / in the thymeleaf prefix config, in the application.properties file: spring.thymeleaf.prefix=classpath:/templates.

    This should have been spring.thymeleaf.prefix=classpath:/templates/.

    Once I added it, it started recognizing the templates (including the custom login template/view).