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?
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.