Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程
- 2022 年 1 月 19 日
- 筆記
- spring security
一.前言
spring security安全框架作为spring系列组件中的一个,被广泛的运用在各项目中,那么spring security在程序中的工作流程是个什么样的呢,它是如何进行一系列的鉴权和认证呢,下面让我们走进源码,从源码的角度来从头走一遍spring security的工作流程。
二.spring security核心结构
当一个外部请求进入到我们应用中的时候,首先会通过我们的应用过滤器链ApplicationFilterChain,我们将遍历该过滤器链中每一个Filter进行对应的处理,下面我们来看下ApplicationFiterChain一般情况下有哪些Filter
//ApplicationFilterChain遍历内部Fiter进行doFilter()处理
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.pos < this.n) { ApplicationFilterConfig filterConfig = this.filters[this.pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE); } if (Globals.IS_SECURITY_ENABLED) { Principal principal = ((HttpServletRequest)request).getUserPrincipal(); Object[] args = new Object[]{request, response, this}; SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); }
从上面的图中我们可以看到,chain在一般情况下中主要存在着这么几个filter,其中有我们比较熟悉的characterEncodeingFilter字符编码的过滤器等等,以及我们本次内容的主角:springSecurityFiterChain spring security的过滤器链,可以说这就是spring security的核心所在,springSecurityFiterChain虽然为filter,但他在这里实际扮演的是一个filterChain的角色,从他的的BeanName也可以看出,那我们接下来进入springSecurityFiterChain.doFilter()方法中,看看它内部又有哪些filter,以及内部的逻辑是怎样的
可以看到springSecurityFiterChain其实是个代理bean,它的doFilter()中实际用的delegate.doFilter(),delegate是个FilterChainProxy,下面来看下FiterChainProxy的内部实现。
FilterChainProxy.doFilter()方法内部逻辑
@Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
//如果当前游标==additionalFilters的长度,即已经遍历完该列表内的Filter,则结束FilterChainProxy.doFilter() if (this.currentPosition == this.size) { if (logger.isDebugEnabled()) { logger.debug(LogMessage.of(() -> "Secured " + requestLine(this.firewalledRequest))); } // Deactivate path stripping as we exit the security filter chain this.firewalledRequest.reset(); this.originalChain.doFilter(request, response); return; }
this.currentPosition++;
//获取列表中下一个Filter Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size)); }
//执行下一个Filter的doFilter()方法 nextFilter.doFilter(request, response, this); } }
我们看到FilterChainProxy中维护了一个Filter列表 additionalFilters,doFilter()中会顺序遍历这个列表,执行每一个Filter的doFilters,那这个列表中具体有哪些Filter呢,让我们来看一下
可以看到该列表中一共有13个spring security内置实现的Filter,系列文章中我们也主要来看SecurityContextPersistenceFilter security上下文持久化的过滤器,主要用来将认证过后的Authentication从session中提取注入本地线程变量中,以及UsernamePasswordAuthenticationFilter,用户密码认证过滤器,主要用来处理通过指定的登录的POST方式的请求url来进行认证…..如果我们的项目中实现了JWT+Spring security的话,一般我们的我们会将自定义实现的JWT过滤器也加入到这条执行链中,并且执行位置放到UserNamePasswordAuthenticationFilter之前。
那么Spring security的大体工作流程就如下图:
三.UserNamePasswordAuthenticationFilter之登录的认证流程
这里我们并没有按照上面的列表顺序从头开始讲,第一个原因是本系列不会解析列表里所有的过滤器,第二个则是个人觉得登录是开启security的入口,从登录开始解析,之后再反过头串联前面的Filter,会有更好的效果。
通常情况下我们在security的配置中配置了哪些请求路径是开放的,哪些路径的需要权限的,访问了需要权限的请求时,如果没有权限便会跳转到security默认的登录页中,这时候我们可以进行输入账号密码进行登录,那这次登录请求security是如何处理的呢,让我们来看看UserNamePasswordAuthenticationFilter.doFilter()方法
//UserNamePasswordAuthenticationFilter本身并没有实现doFilter()方法,使用的是其父类的doFilter()方法
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { } public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//判断当前请求是否满足认证条件,不满足则结束流程,进行下一个过滤器的工作,如何判断满不满足认证条件的逻辑,在下面进行解析 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try {
//当前请求满足认证条件,开始尝试去认证,attemptAuthentication()由UserNamePasswordAuthencationFilter自己实现 Authentication authenticationResult = attemptAuthentication(request, response);
//如果没有一个认证管理器能认证,结果为null,则直接结束 if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; }
//调用session策略将认证通过的凭证储存在Session中,保证登陆之后后续同浏览器登陆不需要再登录,这个详细逻辑等到后续展开讲解 this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); }
//认证通过后的处理,这里会将当前的凭证填充到本地线程变量中,以及会如何在security的配置中配置了successForwardUrl,将会跳转至该URL successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } } }
requiresAuthentication()判断请求是否满足认证条件
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
//内部调用了成员属性中的RequestMatcher去进行匹配,下面逻辑可以看到只有请求是POST的并且路径跟pattern完全匹配才会返回true if (this.requiresAuthenticationRequestMatcher.matches(request)) { return true; } if (this.logger.isTraceEnabled()) { this.logger .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher)); } return false; }
我们来看下UserNamePasswordAuthencationFilter内置的AntPathRequestMatcher的属性以及匹配逻辑
可以看到该AntPathRequestMatcher有两个重要的属性,pattern=”/login”, httpMethod=”POST”,这两个属性将在matchs()方法中起到决定性作用。
public boolean matches(HttpServletRequest request) {
//如果请求方式不是httpMthod不一致,也就是非POST请求,则不匹配,返回false if (this.httpMethod != null && StringUtils.hasText(request.getMethod()) && this.httpMethod != HttpMethod.resolve(request.getMethod())) { return false; }
//如果当前的pattern=/**,即任意请求,那么匹配,返回true if (this.pattern.equals(MATCH_ALL)) { return true; }
//获取当前请求的路径(去除掉项目根路径) String url = getRequestPath(request);
//通过debug源码,这里采取的是完全匹配,即需要请求路径和pattern(/login)完全一致才返回true,当然这个pattern是可以配置的,之后会详细指出 return this.matcher.matches(url); }
来看一下UserNamePasswordAuthenticationFilter实现的attemptAuthentication()方法
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//判断security当前是否只支持POST请求 if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); }
//从请求中获取当前登录的用户名,即从request.getParamter("username")中获取 String username = obtainUsername(request);
//没有该参数的话默认空串 username = (username != null) ? username : "";
//去除空格 username = username.trim();
//获取密码,即request.getPassword("password") String password = obtainPassword(request); password = (password != null) ? password : "";
//根据用户密码初始化Authentication凭证,具体为UsernamePasswordAuthentication这种类型的凭证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest);
//调用当前环境的认证管理器进行认证, 这里内置的是ProviderManager return this.getAuthenticationManager().authenticate(authRequest); }
ProviderManager.authenticate()认证方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取传入参数中的认证凭证的类型,当前为UsernamePasswordAuthenticationToken Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size();
//遍历该认证管理器下所有的授权者(或叫认证者) for (AuthenticationProvider provider : getProviders()) {
//判断当前授权者是否支持对传入的这种类型的凭证进行认证授权 if (!provider.supports(toTest)) { continue; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)", provider.getClass().getSimpleName(), ++currentPosition, size)); } try {
//当前授权者对该凭证进行认证授权,并将结果保存在result中 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); throw ex; } catch (AuthenticationException ex) { lastException = ex; } }
//如果遍历完所有的授权者都不能认证,并且当前认证管理器存在父亲认证管理器,那么就调用他父亲认证管理器重复上述操作 if (result == null && this.parent != null) { try {
//保存父亲认证管理器的认证结果(即授权通过的凭证) parentResult = this.parent.authenticate(authentication);
//赋值到当前的result result = parentResult; } catch (ProviderNotFoundException ex) { } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null) {
//身份验证已完成。删除凭据和其他机密数据 if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); }
//如果尝试了父级AuthenticationManager并成功,则
//将发布AuthenticationSuccessEvent
//如果父级验证失败,此检查将防止重复AuthenticationSuccessEvent
//AuthenticationManager已经发布了它
if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); }
//返回认证结果 return result; } if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }
可以看到当前ProviderManager下只有一个授权者,并且该授权者也不支持对UsernamePasswordAuthenticationToken这种凭证进行授权,所以需要调用该ProviderManager的父亲ProviderManager进行尝试认证
来看下它的父亲ProviderManager有哪些授权者,是否能够对UsernamePasswordAuthenticationToken进行认证授权
可以看到这个父亲认证管理器中也只有一个授权者,DaoAuthenticationProvider,利用数据库数据进行认证授权的,而这个就是能够支持对UsernamePasswordAuthenticationToken这种凭证进行认证授权的,下面我们就来看下DaoAuthenticationProvider是如何认证授权的
DaoAuthenticationProvider.authenticate()方法,该方法由它的父类AbstractUserDetailsAuthenticationProvider实现
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//该授权者只支持对UsernamePasswordAuthenticationToken这种类型凭证进行认证授权,不是的话即抛出异常 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
//获取登录的用户名 String username = determineUsername(authentication); boolean cacheWasUsed = true;
//通过用户名尝试从缓存中获取该用户的信息 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try {
//缓存中没有,则直接根据username从数据库中查,这里用的其实就是我们自己实现的UserDetailsService.loadUserByUsername(username)方法,下面会展示代码 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); }
//数据库也查不到该用户便抛出异常 catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try {
//对查询出来的用户信息进行预校验,主要检测的就是UserDetails实体中的isAccountNonExpired是否过期,isAccountNonLocked是否被锁定,
isEnabled是否可用,只要有一个不满足就抛出异常
this.preAuthenticationChecks.check(user);
//附加信息的认证,这里主要是对密码进行认证
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//预检通过,进行后置检查,这里主要检查UserDeatils中的isCredentialNonExpired, 即检查密码是否过期,同样由我们自定义的UserDetailsService逻辑的来判断,是否过期
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
//不是从缓存中拿的就存到缓存中
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//检验通过,将传入的凭证构造成认证通过的凭证
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider.retrieveUser()从数据库中获取用户信息
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try {
//通常我们使用security 都会自己实现UserDetailService进行配置,这里就用到了,它会通过我们自定义的逻辑来找寻用户并返回用户信息 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
AbstractUserDetailsAuthenticationProvider的内部类实现的check方法()
private class DefaultPreAuthenticationChecks implements UserDetailsChecker { @Override public void check(UserDetails user) {
//校验是否被锁定 if (!user.isAccountNonLocked()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account is locked"); throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); }
//校验是否可用 if (!user.isEnabled()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account is disabled"); throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); }
//校验是否过期 if (!user.isAccountNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger .debug("Failed to authenticate since user account has expired"); throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } }
DaoAauthenticationProvider.additionalAuthenticationChecks()对凭证进行附加信息的校验,主要是校验密码
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//如果凭证中的密码==null则直接抛出异常 if (authentication.getCredentials() == null) { this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }
//从凭证中获取当前登录的密码 String presentedPassword = authentication.getCredentials().toString();
//调用security环境中的PasswordEncoder将凭证中的密码与数据库查询出的密码进行匹配,匹配不成功则抛出异常,
这个PasswordEncoder通常我们在使用security的
时候都会替换成自定义的Encoder,根据自己项目的需求进行自定义的实现其中的逻辑 if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
AbstractAuthenticationProcessingFilter.successfulAuthentication()方法:认证通过后一些收尾处理
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//创建一个新的security上下文 SecurityContext context = SecurityContextHolder.createEmptyContext();
//将凭证填充进上下文 context.setAuthentication(authResult);
//将上下文保存到本地线程变量中 SecurityContextHolder.setContext(context); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); }
//security 开启了remember me功能的话的处理 this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); }
//security中配置了successForwardUrl的话,这里登录成功后就会跳转到指定url上 this.successHandler.onAuthenticationSuccess(request, response, authResult); }
四:最后
这篇文章是spring security源码解析的第一章,主要是解析了下security 是如何处理登录流程的,主要就是来看UsernamePasswordAuthenticationFilter的内部处理逻辑,通过源码我们也发现,默认情况下只有你的请求的POST方式的 /login,security才会认为这是登录请求,才会让该请求走UsernamePasswordAuthenticationFilter的处理逻辑,当然这个路径也是可以配置的,如下代码,配置了loginProcessingUrl的路径之后,再次登录,security就会以该路径当做登录请求。
protected void configure(HttpSecurity http) throws Exception { http.csrf() .disable() .logout() .and() .formLogin() //该路径也是UserNamePasswordAuthenticationFilter用来识别当前请求是不是登录请求,是的话才进行登录处理 .loginProcessingUrl("/loginMy")
/...略../ }
关于UsernamePasswordAuthenticationFilter的内容暂时告一段落,下篇内容将继续通过源码解析剩下的过滤器SecurityContextPersistenceFilter security上下文持久化的过滤器等等….。
由于笔者水平有限,有些地方可能讲解的有错误,希望大家能够帮忙指出,共同进步。