Search code examples
javaspring-bootrestspring-securityrestful-authentication

Custom AuthenticationFilter Setting `LoginProcessingUrl` is Invalid


I'm building an OAuth2 Authorization server that supports Restful API with Spring Authorization Server and Spring Security. I want a SPA application built by React to provide a login interface at /login and submit the login information to the /api/login path with a Post request. I extend UsernamePasswordauthenticationFilter to support Restful-style Post requests to parse Json data from body:

public class RestfulUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public RestfulUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    private String jsonUsername;
    private String jsonPassword;

    @Override
    protected String obtainPassword(HttpServletRequest request) {
        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            return this.jsonPassword;
        } else {
            return super.obtainPassword(request);
        }

    }

    @Override
    protected String obtainUsername(HttpServletRequest request) {

        if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
            return this.jsonUsername;
        } else {
            return super.obtainUsername(request);
        }

    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if ("application/json".equals(request.getHeader("Content-Type"))) {
            try {
                /*
                 * HttpServletRequest can be read only once
                 */

                //json transformation
                Map<String, String> requestMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);


                this.jsonUsername = requestMap.get("username");
                this.jsonPassword = requestMap.get("password");
            } catch (Exception e) {
                throw new AuthenticationServiceException(e.getMessage(), e);
            }
        }

        return super.attemptAuthentication(request, response);
    }
}

In the configuration, I replaced the custom RestfulUsernamePasswordauthenticationFilter with UsernamePasswordauthenticationFilter, and used .loginProcessUrl to set the path for processing Post requests to /api/login:

    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
        http.addFilterAt(new RestfulUsernamePasswordAuthenticationFilter(authenticationManagerBean()),
                UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/api/all")
                .permitAll()
                .antMatchers("/api/auth")
                .hasRole("USER")
                .antMatchers("/api/admin")
                .hasRole("ADMIN")
                .antMatchers("/login", "/register", "/api/login")
                .permitAll();



        http.formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/api/login");
        http.csrf().disable();
    }

The problem is that although I set the path to process Post requests through .loginProcessingUrl, it doesn't seem to work. When a Post request is submitted to /api/login, it will be redirected to/login like all unauthenticated requests, and the request submitted to /login will take effect normally.

In the process of debugging, I found that .loginProcessingUrl will register this path in a UsernamePasswordconfirationFilter, but will not be processed by my custom RestfulUsernamePasswordShareationFilter. In the process of debugging, I found that .loginProcessingUrl will register this path in a UsernamePasswordFilter. I want to know if there is any way to make .loginProcessingUrl work on my custom AuthenticationFilter. At the same time, can I easily customize the path to which they accept requests when I add more custom Filter? Maybe I will add more AuthenticationProvider that need to read Restful information in the future. how should I design the architecture of these Filter and Provider to make it easier to expand?


Solution

  • I think I solved the problem after read this blog. As it said:

    After you provided your custom filter, you can no longer use the Spring HttpSecurity builder. If you still use it, you’ll configure the default Filter, not yours!

    We tell Spring to use our implementatin by the “addFIlterBefore” function.

    After this little modification, the test APIs work the same way, the only difference is that you should provide ‘login’ and ‘password’ params in the POST request body (and not the query string).

    So I have to manually set the AntPathRequestMatcher withsetRequiresAuthenticationRequestMatcher of the filter class like this:

    RestfulUsernamePasswordAuthenticationFilter restfulFilter = new RestfulUsernamePasswordAuthenticationFilter(
                    this.authenticationManagerBean());
    restfulFilter.setRequiresAuthenticationRequestMatcher(
                    new AntPathRequestMatcher("/api/login", "POST"));
    http.addFilterBefore(restfulFilter,
                    UsernamePasswordAuthenticationFilter.class);
    

    By this way, you can configure the path to handle the POST request just like you use loginProcessingUrl in the formal way.

    You can also use same method to add more custom filter and authenticationProvider in the future, just configure them manually.