
在SpringSecurity一系列的过滤器链中,与认证相关的过滤器就是UsernamePasswordAuthenticationFilter,它继承自AbstractAuthenticationProcessingFilter。UsernamePasswordAuthenticationFilter的doFilter方法在其父类AbstractAuthenticationProcessingFilter中被声明。如果我们想要在SpringSecurity自定义一个登录验证码或者将登录参数改成 JSON 的时候,我们都需要自定义过滤器继承自AbstractAuthenticationProcessingFilter,毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication方法就是在AbstractAuthenticationProcessingFilter类的doFilter方法中被触发的。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean {private AuthenticationManager authenticationManager;private RequestMatcher requiresAuthenticationRequestMatcher;private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}if (logger.isDebugEnabled()) {logger.debug("Request is to process authentication");}Authentication authResult;try {authResult = attemptAuthentication(request, response);if (authResult == null) {// return immediately as subclass has indicated that it hasn't completed// authenticationreturn;}sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {logger.error("An internal error occurred while trying to authenticate the user.",failed);unsuccessfulAuthentication(request, response, failed);return;}catch (AuthenticationException failed) {// Authentication failedunsuccessfulAuthentication(request, response, failed);return;}// Authentication successif (continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}successfulAuthentication(request, response, chain, authResult);}}
首先判断当前请求是否需要认证?
protected boolean requiresAuthentication(HttpServletRequest request,HttpServletResponse response) {return requiresAuthenticationRequestMatcher.matches(request);}
如果需要认证,则执行子类中重写的
attemptAuthentication方法:public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String username = obtainUsername(request);String password = obtainPassword(request);if (username == null) {username = "";}if (password == null) {password = "";}username = username.trim();UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}}
从这段源码可以看出:
- 如果当前请求不是post请求,则会抛出异常。
- 首先通过
obtainUsername和obtainPassword方法提取请求里面的用户名和密码,提取方式为 request.getParameter,这也就是为什么默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数,如果想传递 JSON 参数,需要修改这里的逻辑。 - 使用获取到的用户名和密码构建成一个
UsernamePasswordAuthenticationToken对象,其中username对应它的principal属性,password对应它的credentials属性。 接下来 setDetails 方法给 details 属性赋值,
UsernamePasswordAuthenticationToken本身没有 details 属性,这个属性在它的父类AbstractAuthenticationToken中。details 是一个对象,这个对象里面放的是webAuthenticationDetails实例,该实例主要描述了两个信息,请求的 remoteAddress 以及请求的 sessionId。public WebAuthenticationDetails(HttpServletRequest request) {this.remoteAddress = request.getRemoteAddr();HttpSession session = request.getSession(false);this.sessionId = (session != null) ? session.getId() : null;}
最后一步开始做校验,校验操作首先要获取到一个
AuthenticationManager,这是一个接口,这里获取到的是它的实现类ProviderManager,然后调用authenticate方法:

public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}if (result == null && parent != null) {result = parentResult = parent.authenticate(authentication);}if (result != null) {if (eraseCredentialsAfterAuthentication&& (result instanceof CredentialsContainer)) {((CredentialsContainer) result).eraseCredentials();}if (parentResult == null) {eventPublisher.publishAuthenticationSuccess(result);}return result;}throw lastException;}
- 首先获取
authentication的 class,判断当前provider是否支持该authentication。 - 如果支持的话,则调用
provider的authenticate方法开始做校验,校验完成之后,会返回一个新的Authentication,一会儿再详细捋一下这个方法的具体逻辑。 - 如果其中某一个
provider认证成功,则调用copyDetails方法吧旧的token中的details属性拷贝到新的token中来;如果所有的provider都认证失败,则调用parent的authenticate方法继续进行校验。 - 接下来调用
eraseCredentials方法擦除凭证信息(也就是密码),这个擦除方法比较简单,就是将token中的credentials属性置空。
大致的流程就是上面这样,在for循环中,第一次拿到的provider是一个AnonymousAuthenticationProvider,这个provider只支持AnonymousAuthenticationToken,压根不支持UsernamePasswordAuthenticationToken,也就是会直接在provider中的supports方法中返回 false,结束for循环,然后会进入到下一个 if 中,直接调用parent的authenticate方法进行校验。而parent就是providerManager,所以会再次回到这个authenticate方法中。此时provider也变成了DaoAuthenticationProvider,这个provider是支持UsernamePasswordAuthenticationToken,所以会顺利进入该类的authenticate方法去执行,而DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider并且没有重写authenticate方法,所以,我们最终来到AbstractUserDetailsAuthenticationProvider#authenticate方法中:
public Authentication authenticate(Authentication authentication)throws AuthenticationException {String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();try {user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException notFound) {logger.debug("User '" + username + "' not found");if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}else {throw notFound;}}preAuthenticationChecks.check(user);additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);postAuthenticationChecks.check(user);Object principalToReturn = user;if (forcePrincipalAsString) {principalToReturn = user.getUsername();}return createSuccessAuthentication(principalToReturn, authentication, user);}
- 首先从
Authentication对象中获取处登录用户名。 - 然后拿着
username去调用子类DaoAuthenticationProbvider的retrieveUser方法获取当前用户对象,这一步会使用UserDetailsService接口中的loadUserByUsername方法返回user。可以通过实现UserDetailsService接口重写loadUserByUsername方法来从数据库中读取用户,SpringSecurity默认使用的是InMemoryUserDetailsManager实现类中的loadUserByUsername方法,从内存中获取用户。 - 在查找用户的时候,如果抛出了
UsernameNotFoundException,这个异常被捕获,捕获之后,如果hideUserNotFoundExceptions属性的值为 true,就抛出一个BadCredentialsException。相当于将UsernameNotFoundException异常隐藏了,而默认情况下,hideUserNotFoundExceptions的值就为 true。所以无论用户是账号还是密码错误,收到的都是BadCredentialsException异常。 - 接下来调用
preAuthenticationChecks#check方法来检验user中的各个账户状态属性是否正常,如账户是否被锁定、账户是否被禁用、账户是否过期等等。 additionalAuthenticationChecks方法则是做密码比对的,使用PasswordEncoder中的matches方法比对token中的credentials属性和读取出来的用户中的密码是否相同,如果不同,则会抛出BadCredentialsException异常。- 最后在
postAuthenticationChecks.check方法中检查密码是否过期。 - 接下来有一个
forcePrincipalAsString属性,这个是是否强制将Authentication中的principal属性设置为字符串,这个属性一开始在UsernamePasswordAuthenticationFilter类中其实就是设置为字符串(即 username),但是默认情况下,当用户登录成功后,这个属性就变成当前用户这个对象了。之所以会这样,就是因为forcePrincipalAsString默认为 false,不过这块其实不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。 - 最后,通过
createSuccessAuthentication方法构建一个新的UsernamePasswordAuthenticationToken。
从
AbstractAuthenticationProcessingFilter#doFilter方法的源码中可以知道,当登录成功的时候,successfulAuthentication方法则会被调用:protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authResult)throws IOException, ServletException {SecurityContextHolder.getContext().setAuthentication(authResult);rememberMeServices.loginSuccess(request, response, authResult);// Fire eventif (this.eventPublisher != null) {eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}successHandler.onAuthenticationSuccess(request, response, authResult);}
在这里有一段很重要的代码,就是
SecurityContextHolder.getContext().setAuthentication(authResult);,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从SecurityContextHolder.getContext()中获取到,想修改,也可以在这里修改。最后,大家还看到一个successHandler.onAuthenticationSuccess,这就是在SecurityConfig中配置的登录成功回调方法。认证失败
protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {SecurityContextHolder.clearContext();if (logger.isDebugEnabled()) {logger.debug("Authentication request failed: " + failed.toString(), failed);logger.debug("Updated SecurityContextHolder to contain null Authentication");logger.debug("Delegating to authentication failure handler " + failureHandler);}rememberMeServices.loginFail(request, response);failureHandler.onAuthenticationFailure(request, response, failed);}
SecurityContextHolder.clearContext();清空认证信息;以及failureHandler.onAuthenticationFailure,这就是在SecurityConfig中配置的登录失败回调方法。
