使用JSON格式登录
Spring Security 中默认的登录参数传递格式是 key/value 形式,也就是表单登录格式,在实际项目中,我们口能会通过 JSON 格式来传递登录参数,这就需要我们自定义登录过滤器来实现。
通过 3.1.4 小节的介绍,大家己经明白登录参数的提取是在 UsernamePasswordAuthenticationFilter 过滤器中完成的。如果我们要使用 JSON 格式登录,只需要模仿 UsernamePasswordAuthenticationFilter 过滤器定义自己的过滤器,再将自定义的过滤器放到 UsernamePasswordAuthenticationFilter 过滤器所在的位置即可。
思路理清了,我们来看代码实现。首先我们自定义一个 LoginFilter 继承自 UsernamePasswordAuthenticationFilter,代码如下:
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String, String> userInfo = new HashMap<>();
try {
userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
String username = userInfo.get(getUsernameParameter());
String password = userInfo.get(getPasswordParameter());
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
e.printStackTrace();
}
}
return super.attemptAuthentication(request, response);
}
}
-
首先确角保进入该过滤器中的请求是 POST 请求。
-
根据请求的 content-type 来判断参数是 JSON 格式的还是 key/value 格式的,如果是 JSON 格式的,则直接在当前方法中处理;如果是 key/value 格式的,那直接调用父类的 attemptAuthentication 方法处理即可。
-
如果请求参数是 JSON 格式,则首先利用 jackson 提供的 ObjectMapper 工具,将输入流转为 Map 对象,然后从 Map 对象中分别提取出用户名/密码信息并构造出 UsernamePasswordAuthenticationToken 对象,然后调用 AuthenticationManager 的 authenticate 方法执行认证操作。
其实 LoginFilter中,从请求中提取出 JSON 参数之后的认证逻辑和父类 UsernamePasswordAuthenticationFilter 中的认证逻辑是一致的,读者可以回顾第 3 章中关于 UsernamePasswordAuthenticationFilter 的分析。
LoginFilter 定义完成后,接下来我们将其添加到 Spring Security 过滤器链中,代码如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("{noop}123")
.roles("admin");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(auth));
});
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
-
首先重写 configure 方法来定义一个登录用户。
-
重写父类的 authenticationManagerBean 方法来提供一个 AuthenticationManager 实例,一会将配置给 LoginFilter。
-
配置 loginFilter 实例,同时将 AuthenticationManager 实例设置给 loginFilter,然后再设置登录成功回调。当然,我们也可以在 loginFilter 中配置用户名/密码的参数名或者登录失败的回调。
-
最后在 HtpSecurity 中,调用 addFilterAt 方法将 loginFilter 过滤器添加到 UsernamePasswordAuthenticationFilter 过滤器所在的位置。
配置完成后,重启项目,此时我们就可以使用 JSON 格式的数据来进行登录操作了,如图 4-11 所示。

有读者可能会注意到,当我们想要获取一个 AuthenticationManager 实例时,有两种不同的方式,第一种方式是通过重写父类的 authenticationManager 方法获取,第二种则是通过重写父类的 authenticationManagerBean 方法获取。表面上看两种方式获取到的 AuthenticationManager 实例在这里都可以运行,但实际上是有区别的。区别在于第一种获取到的是全局的 AuthenticationManager 实例,而第二种获取到的是局部的 AuthenticationManager 实例,而 LoginFilter 作为过滤器链中的一环,显然应该配置局部的 AuthenticationManager 实例,因为如果将全局的 AuthenticationManager 实例配置给 LoginFilter,则局部 AuthenticationManager 实例所对应的用户就会失效,例如如下配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("{noop}123")
.roles("admin");
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setAuthenticationSuccessHandler((req, resp, auth) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(auth));
});
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
InMemoryUserDetailsManager users = new InMemoryUserDetailsManager();
users.createUser(User.withUsername("javagirl").password("{noop}123").roles("admin").build());
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.userDetailsService(users);
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
在上面这段代码中,我们将无法使用 javagirl/123 进行登录,因为 LoginFilter 中指定了全局的 AuthenticationManager 来做验证,所以局部的 AuthenticationManager 实例失效了。
在实际应用中,如果需要自己配置一个 AuthenticationManager 实例,大部分情况下,我们都是通过重写 authenticationManagerBean 方法来获取。