Search code examples
spring-bootspring-securityjava-17

Method 'POST' is not supported in Spring Security 6


I attempted an upgrade to Spring Security 6 today and after spending the whole day trying to get this working, still no success. Yes, I have read the official documentation, looked at various examples, such as https://docs.spring.io/spring-security/reference/servlet/configuration/java.html#_multiple_httpsecurity_instances or https://github.com/spring-projects/spring-security-samples/blob/main/servlet/java-configuration/authentication/username-password/form/src/main/java/example/SecurityConfiguration.java, but all to no avail. This blog post unfortunately also did not help: https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#comment-6027047044.

Now to my question. Before I converted my security configuration, it looked like this:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private DbUserService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").authenticated()
                .antMatchers("/**").permitAll()
            .and()
        .formLogin()
            .loginPage("/login")
            .permitAll()
            .defaultSuccessUrl("/admin")
            .and()
        .logout()
                .permitAll();
    }

    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

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

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

}

After the attempted conversion, it looks like this:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
    private AuthenticationManager authenticationManager;

    @Autowired
    private DbUserService userService;

    @Bean
    public SecurityFilterChain formLoginFilterChain(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userService);
        authenticationManager = authenticationManagerBuilder.build();

        http.authenticationManager(authenticationManager)
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

        http.securityMatchers((matchers) -> matchers.requestMatchers("/admin/**"))
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

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

    @Bean
    public AuthenticationManager authManager(final AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

The problem arises when I try to log in. In the previous version it worked fine, but now I keep on getting the error message:

Method 'POST' is not supported.

And I can see why. For some reason it is trying to call the post method in my own LoginController, but of course there is no POST request method in there. There is only my GET method for displaying the form.

Controller:

@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

View:

       <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>

Is there a reason why it is going to my controller instead of the spring security one, like it used to? Is this a deliberate change in the new spring security 6?

======================================================
*** Additional information after accepted solution ***
======================================================

Thanks, it works now with the accepted solution. It even works together with my second security configuration for my /api/ endpoints. For completeness, I will post that here incase anyone finds it useful (hopefully it is a valid solution):

@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {

    @Value("${security.rest.apikey}")
    private String restApiKey;

    @Order(2)
    @Bean
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/api/**")
                .csrf(AbstractHttpConfigurer::disable)
                .addFilterBefore(apiKeyAuthFilter(), BasicAuthenticationFilter.class)
                .authorizeHttpRequests(request -> request
                        .requestMatchers("/api/**").authenticated());

        return http.build();
    }

    private Filter apiKeyAuthFilter() {
        return new OncePerRequestFilter() {

            @Override protected void doFilterInternal(HttpServletRequest request,
                    HttpServletResponse response, FilterChain filterChain)
                    throws ServletException, IOException {
                String requestApiKey = request.getHeader("X-Api-Key");

                if (!restApiKey.equals(requestApiKey)) {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }

                Authentication authentication = new UsernamePasswordAuthenticationToken("apiUser", null, new ArrayList<>());
                SecurityContextHolder.getContext().setAuthentication(authentication);

                try {
                    filterChain.doFilter(request, response);
                } finally {
                    SecurityContextHolder.clearContext();
                }
            }
        };
    }
}

Solution

  • You should to do some changes to get the same configuration before migration:

    Current implementation of security layer:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration {
    
      @Autowired
      private DbUserService userService;
    
      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\
        http.authenticationManager(authenticationManager());
        http.authorizeHttpRequests(request -> {
          request.requestMatchers("/admin/**").authenticated();
          request.requestMatchers("/**").permitAll();
        });
        http.formLogin(form -> {
          form.loginPage("/login").permitAll();
          form.defaultSuccessUrl("/admin");
        });
        http.logout(LogoutConfigurer::permitAll);
        return http.build();
      }
    
      @Bean
      public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userService);
        return new ProviderManager(provider);
      }
    
      @Bean
      public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
      }
    
    }
    

    By the way: @EnableGlobalMethodSecurity(prePostEnabled = true) also was deprecate to @EnableMethodSecurity, you should replace it but not in SecurityConfiguration class. But in main

    Something like this:

        @SpringBootApplication
        @EnableMethodSecurity
        public class Application {
        
          public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
          }
    
    }
    

    It should solve the problem

    The problem arises when I try to log in. In the previous version it worked fine, but now I keep on getting the error message: Method 'POST' is not supported.