0

I'm trying to upgrade Spring Boot from 2.7.6 to 3.0.1. I have a problem during the login action. The following is my new WebSecurityConfig:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig  {

    private final CustomUserDetailsService customUserDetailsService;

    private final  CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;

    public WebSecurityConfig(CustomUserDetailsService customUserDetailsService, CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler) {
        this.customUserDetailsService = customUserDetailsService;
        this.customizeAuthenticationSuccessHandler = customizeAuthenticationSuccessHandler;
    }
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler(){
        return new CustomAccessDeniedHandler();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests()
                .requestMatchers("/").permitAll()
                .requestMatchers("/login").permitAll()
                .authenticated()
                .and()
            .csrf().disable()
            .formLogin()
                .successHandler(customizeAuthenticationSuccessHandler)
                .loginPage("/login")
                .failureUrl("/login?error=true")
                .usernameParameter("email")
                .passwordParameter("password")
                .and()
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .invalidateHttpSession(true)
                .logoutSuccessUrl("/login?logout=true")
                .and()
            .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler())
                .and()
            .authenticationProvider(authenticationProvider());
        http
            .sessionManagement()
                .maximumSessions(1)
                .expiredUrl("/login?expired=true");
        return http.build();
    }

    // This second filter chain will secure the static resources without reading the SecurityContext from the session.
    @Bean
    @Order(0)
    SecurityFilterChain resources(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
                .anyRequest().permitAll())
            .requestCache().disable()
            .securityContext().disable()
            .sessionManagement().disable();

        return http.build();
    }
}

Follow my CustomUserDetailService:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserByEmail(String email) {
        System.out.println(email);
        User user = userRepository.findByEmail(email.toLowerCase());
        System.out.println(user.getEmail());
        return userRepository.findByEmail(email.toLowerCase());
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        User user = userRepository.findByEmail(email.toLowerCase());
        if (user != null) {
            List<GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority( user.getRole()));;
            return buildUserForAuthentication(user, authorities);
        } else {
            throw new UsernameNotFoundException("username not found");
        }
    }

    private UserDetails buildUserForAuthentication(User user, List<GrantedAuthority> authorities) {
        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), authorities);
    }
}

When I run the application I see the login page, but when I enter the credential and press submit I receive the error:

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported]

and Tomcat shows:

HTTP Status 405 – Method Not Allowed Type Status Report

Message Method 'POST' is not supported.

I searched for a solution but really I don't understand where is the problem.

dur
  • 15,689
  • 25
  • 79
  • 125
Enrico Morelli
  • 185
  • 3
  • 16

2 Answers2

0

To use multiple HttpSecurity instances, you must specify a security matcher, otherwise the first SecurityFilterChain will process all requests, and no requests will reach the second chain.
See this section of the Spring Security reference documentation.

In your case the SecurityFilterChain called resources is matching all requests, because you don't have a security matcher.
Since the resources chain does not configure formLogin then Spring Security does not create the default /login POST endpoint.

You can fix this by changing requests to:

@Bean
@Order(0)
SecurityFilterChain resources(HttpSecurity http) throws Exception {
    http
        .securityMatchers((matchers) -> matchers
            .requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**") // the requests that this SecurityFilterChain will process
        )
        .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().permitAll())
        .requestCache().disable()
        .securityContext().disable()
        .sessionManagement().disable();

    return http.build();
}

If you want more details on the difference between authorizeHttpRequests and requestMatchers you can check out this question.

-1

This error typically occurs when the method in the controller is not mapped to a post request. Should be something like:

@RequestMapping(value = "/login", method = {RequestMethod.GET, RequestMethod.POST})
public ModelAndView login(...
Prospero
  • 79
  • 6
  • 1
    I know. But usually, the authentication should be managed by Spring Security. In the 2.7.6 Spring application, I haven't s POST method to manage login authentication. – Enrico Morelli Dec 28 '22 at 13:15
  • I have been working with Spring Boot since release 1 and I have since then used the formLogin and loginPage together with a controller. I don't have experience of other configurations. – Prospero Dec 28 '22 at 14:50
  • I check this article https://javatechonline.com/spring-security-userdetailsservice-using-spring-boot-3/#Step9_Write_SecurityConfig_class_Without_Using_WebSecurityConfigurerAdapter to confirm that using Spring Security there isn't a POST to manage authentication – Enrico Morelli Dec 29 '22 at 09:01