背景:
默认的 UsernamePasswordAuthenticationFilter过滤器通过 from 表单 post 请求形式获取用户名密码参数,并且请求头为 application/x-www-form-urlencoded(官方登陆源码说明:默认登陆表单), 并通过 ==request.getParameter(this.usernameParameter)==获取用户名密码
参考源码 :
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
然而,按实际需求我们可能需要ajax发送 json格式的请求参数(包含用户名和密码),默认的过滤器并不能满足需求,在 UserDetailService中拿到的 username会一直为 null。
这种情况下我们需要自定义一个 UsernamePasswordAuthenticationFilter来处理 json 数据格式的登陆请求
说明:
鉴于官方已有 UsernamePasswordAuthenticationFilter过滤器,所以我们可以直接复制一个过来修改一下即可
public class UsernamePasswordJsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
/**
* 登陆拦截请求
*/
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordJsonAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordJsonAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 在这里取消原有的通过 request parameter 形式获取用户名密码的方法, 替换为 json 数据格式的获取
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()) {
Map<String,String> authenticationBean = mapper.readValue(is, Map.class);
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.get("username"), authenticationBean.get("password"));
} catch (IOException e) {
e.printStackTrace();
}
// 源码方法
/*String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);*/
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// ... 以下省略其他源码方法
}
这样的话就可以获取到 json 中的用户名和密码了。
在这里有个需要注意的地方
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/user/login",
"POST");
这一句配置的是自定义登陆的拦截请求,也就是 Security 中配置的 loginProcessingUrl,而默认的拦截器中配置的是:
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
我们自定义的拦截器需要填上自己的登陆 API才能走自定义的拦截器。
下一步是将自定义的过滤器注册到 Security 中。
/**
* 自定义登录拦截器
*/
@Bean
UsernamePasswordJsonAuthenticationFilter usernamePasswordJsonAuthenticationFilter() throws Exception {
UsernamePasswordJsonAuthenticationFilter authenticationFilter = new UsernamePasswordJsonAuthenticationFilter();
authenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
authenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFilter;
}
/**
* 注意 :在使用自定义的登陆拦截器后 loginProcessingUrl 可以不配置了,在拦截器中也可以配置
* 当然这里也可以配置但是必须得和拦截器中的一样
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests((authorize) -> authorize
.antMatchers("/css/**", "/index", "/user/login", "/authentication/require").permitAll()
.antMatchers("/user/info").hasRole("USER")
)
.userDetailsService(userDetailsService)
.formLogin((formLogin) -> formLogin
.loginPage("/authentication/require")
// .loginProcessingUrl("/user/login")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
)
.csrf().disable()
.cors();
http.addFilterAt(usernamePasswordJsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
部分同学会经常遇到添加了自定义的拦截器还是走原拦截器的大多数是因为这里还是配置了 loginProcessingUrl,如果实在是想在这里配置也可以,但是配置的 API必须和自定义拦截器的
DEFAULT_ANT_PATH_REQUEST_MATCHER 的 API 一致才行
好了,到此前端就可以通过发送 json 格式的数据来登陆了
谢谢参观,仅供参考 😄🎉