该博客可能需要结合SpringSecurity前后端分离下对登录认证的管理来看。
下面主要浅析spring security登录校验的过程和源码。
主要说说一些重要的filter
一、点击登录后,发送请求首先被UsernamePasswordAuthenticationFilter拦截,该拦截器继承AbstractAuthenticationProcessingFilter类。UsernamePasswordAuthenticationFilter类有什么用呢?下面是该类的源代码(省略了一部分)。可以看到主要就是获取前端传过来的账号密码,如果想要做更多的事情,例如不只是获取账号密码,还想处理验证码,或者Jwt的校验,想要更自由的控制业务,则可以继承UsernamePasswordAuthenticationFilter或者直接继承UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter替代它。做了这些变动需要在WebSecurityConfiguration注册配置下。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
/**
* 登录属性的默认值,如果想要自定义在loginProcessingUrl后面添
* 加.usernameParameter("xxx").passwordParameter("xxx")即可。
*/
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
/**
* 获取前端传过来的参数username, password
*/
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
// 前往下一个主要类
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
}
二、当UsernamePasswordAuthenticationFilter类中attemptAuthentication方法执行到 return this.getAuthenticationManager().authenticate(authRequest);就到下一个较为重要的类DaoAuthenticationProvider,该类继承了AbstractUserDetailsAuthenticationProvider
下面是它的源代码(一部分),这个类主要就是进行密码匹对的。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
/**
* 进行密码的匹配
*/
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
// 根据你前面WebSecurityConfiguration自定义的加密算法来对前端的密码进行加密码校验。
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
/**
* 获取前面自定义的MyuUserDetailsService类, 调用loadUserByUsername方法获取数据库数据。
*/
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
// 根据用户名,获取数据库数据。
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
}
}
该类有两个重要的方法additionalAuthenticationChecks和retrieveUser,执行顺序为retrieveUser –> additionalAuthenticationChecks
先看retrieveUser方法,方法中有一句
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
大家是不是感觉很眼熟,对就是这里调用了我面前面自定义配置的MyUserDetailsService类中的loadUserByUsername方法获取数据库的账号密码信息。
retrieveUser方法执行完后就到additionalAuthenticationChecks方法,该方法中有块代码
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
该代码块就是进行前端传过来的代码和数据库的代码进行匹对,this.passwordEncoder就是根据你前面在WebSecurityConfiguration类中自定义的加密算法进行对密码的处理(它们都实现了PasswordEncoder接口)。
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
}
对于DaoAuthenticationProvider这个类,也可以进行继承它。这样就可以在不同的业务场景下灵活运用了,例如可以进行第三方或者短信登录验证等。
三、最后如果验证成功就会到我们前面自定义的验证成功类MyAuthenticationSuccessHandler,这样子我们可以很方便的对前端进行Json数据返回等操作。
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
// 允许跨域
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
httpServletResponse.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
httpServletResponse.getWriter().write(JsonUtil.objectToJson(JsonData.success()));
}
}
假如没有自定义返回,则就会跳转到默认SimpleUrlAuthenticationSuccessHandler类中
public class SimpleUrlAuthenticationSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {
public SimpleUrlAuthenticationSuccessHandler() {
}
public SimpleUrlAuthenticationSuccessHandler(String defaultTargetUrl) {
this.setDefaultTargetUrl(defaultTargetUrl);
}
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
this.handle(request, response, authentication);
this.clearAuthenticationAttributes(request);
}
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.removeAttribute("SPRING_SECURITY_LAST_EXCEPTION");
}
}
}
这里补充下,怎么自定义UsernamePasswordAuthenticationFilter,自定义这个的目的是什么呢?假如数据在请求里面,或者验证码验证等,用默认的UsernamePasswordAuthenticationFilter是无法实现的。
下面我写了一个MyUsernamePasswordAuthenticationFilter,用来处理请求体中的数据。
@Component
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException {
//业务逻辑
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken("xxx", "xxx");
return this.getAuthenticationManager().authenticate(authRequest);
}
}
在Configuration配置类中的configure添加上这样的配饰
http.addFilter(myUsernamePasswordAuthenticationFilter());
同时在Configuration添上
@Bean
public MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter() throws Exception {
MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = new MyUsernamePasswordAuthenticationFilter();
myUsernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", "POST"));
myUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
//自定义UsernamePasswordAuthenticationFilter就会导致原来configure中的.successHandler和.failureHandler失效
myUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
myUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
return myUsernamePasswordAuthenticationFilter;
}
这么配置的话原来的UsernamePasswordAuthenticationFilter类中的方法就不会被执行到了。这样自定义登录校验就完成了。
最后综上主要的类之间的跳转过程为
UsernamePasswordAuthenticationFilter –> DaoAuthenticationProvider –> UserDetails –> PasswordEncoder
再到最后的AuthenticationSuccessHandler或AuthenticationFailureHandler
大家可以根据我前面说的几个类,打上断点,在debug一遍就更明白运行的流程了。