Spring Security系列之二 认证流程分析

2,533 阅读11分钟

Spring Security系列之二 认证流程分析

章节

Spring Security系列之一 简单介绍和实战

Spring Security系列之二 认证流程分析

Spring Security系列之三 自定义短信登录认证

Spring Security系列之四 前后端分离项目用jwt做认证

Spring Security系列之五 前后端分离项目用户授权

Spring Security系列之六 授权流程分析

之前写的demo,相信对于初学者,有很多疑惑,而且,现在基本上都是前后端分离的项目了,像之前那个样子要配置登录页面和跳转页面是行不通的,一般我们只需要编写一个登录接口,获取前端传过来的用户名密码,然后传给security,再从security获取到验证结果,最后封装结果返回给前端。

但是,能做到上面的前提是要对security的认证流程有一定了解,所以我们今天啥代码都不写,就讲讲认证流程。

登录过滤器

前端进行登录操作的时候,会执行UsernamePasswordAuthenticationFilter过滤器,这个过滤器的代码比较少:

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";
	private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
			"POST");
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
	private boolean postOnly = true;
	public UsernamePasswordAuthenticationFilter() {
		super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
	}

	public UsernamePasswordAuthenticationFilter(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());
		}
		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);
	}
}

这里定义了一个路径:DEFAULT_ANT_PATH_REQUEST_MATCHER,传给了父类,根据它的定义,大胆猜测一下,可能是拦截了一个路径为/login,类型为post的接口,我们稍后再看它的父类拿到这个做了什么。

attemptAuthentication这个方法里,主要做了4件事儿:

  1. 判断请求类型是否为post,如果不是就抛出异常。
  2. request中获取到用户名和密码,字段名为usernamepassword(所以前端传过来的字段必须是这两个)
  3. 将取出来的用户名和密码组装成一个UsernamePasswordAuthenticationToken
  4. 交给内部的AuthenticationManager去认证,并返回认证信息

这个地方的AuthenticationManager放到后面再说,现在我们已经知道这个方法就是获取前端传来的用户名密码,并且验证。那这个attemptAuthentication方法是什么时候调用的呢?这个得去它的父类AbstractAuthenticationProcessingFilter探一探究竟,先看看类继承关系:

可以看到AbstractAuthenticationProcessingFilter继承了Filter,对于servlet中的filter我们应该是非常的熟悉了,不多逼逼,直接去这个类的doFilter方法:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    //1.验证url是否是我们需要的
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
        //2.执行实际身份验证 返回已验证用户的已填充身份验证令牌,说明验证成功,验证失败则泡出异常
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// 身份验证过程仍在进行中
				return;
			}
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			// Authentication success
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
        //3.验证成功之后 发布事件通知监听者做处理,successHandler做回调,跳转到成功页面
			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);
		}
	}

我们对上面的步骤一步一步的分析,先是验证url:

protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
    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;
}

这个requiresAuthenticationRequestMatcher就是从构造方法中传过来的,也就是说这个地方就判断了是否为登录的url,如果是,就进行下面的身份验证。

身份验证是一个抽象方法,具体实现交给它的子类:

public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException;

这个方法就是我们刚才在UsernamePasswordAuthenticationFilter中实现的方法了,验证完成会返回一个Authentication对象。Authentication类是描述当前用户的相关信息的,包含了用户拥有的权限信息列表、用户细节信息。常见的实现类有UsernamePasswordAuthenticationToken,有兴趣的可以深入看一下源码。

我们现在直到了,attemptAuthentication方法的调用时机,不过在调用这个方法完成之后,还做了一些事情。我们这里继续往下看。

保存登录状态

身份验证成功之后的处理:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		SecurityContextHolder.getContext().setAuthentication(authResult);
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
		}
		this.rememberMeServices.loginSuccess(request, response, authResult);
		if (this.eventPublisher != null) {
			this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
		}
     //我们之前在securityconfig中配置的回调调用
		this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

这个地方的SecurityContextHolder获取了SecurityContext,并且调用方法存储了用户信息。既然存储用户信息,肯定就要和用户线程关联,所以SecurityContextHolder提供了以下工作模式:

  1. SecurityContextHolder.MODE_THREADLOCAL(默认):使用threadlocal,用户信息可供线程下的所有方法使用,一种与线程绑定的策略,很适合Servlet Web应用。
  2. SecurityContextHolder.MODE_GLOBAL:使用于独立应用
  3. SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全标示的线程

我们一般也使用第一种工作模式,在默认ThreadLocal策略中,我们可以很方便的通过SecurityContextHolder获取到当前的登录用户信息:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {      
 	String username = ((UserDetails)principal).getUsername(); 
} else {
 	String username = principal.toString();
}

后面的事件发布和事件监听,有兴趣的兄弟可以自己去看源码了,这里也就不再研究了。

登录认证起点

上面的整个调用关系如下:

Filter -> AbstractAuthenticationProcessingFilter -> UsernamePasswordAuthenticationFilter -> AuthenticationManager -> successHandler

可以看见,具体的认证是交给了AuthenticationManager类来完成,AuthenticationManager是认证相关的核心接口,是认证一切的起点。常见的认证流程都是AuthenticationManager的实现类ProviderManager处理的。

我们先看一下AuthenticationManager接口定义如下:

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

ProviderManager类实现:

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		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;
			}
			try {
           //对每个AuthenticationProvider进行认证,直到认证成功
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
      //如果上面没有认证成功,那么进行父类AuthenticationProvider认证
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// 认证完成,清除密码
				((CredentialsContainer) result).eraseCredentials();
			}
			//如果父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 中有一个AuthenticationProvider列表,会依照次序去认证,并且只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。为了安全会进行清除密码。如果所有provider都验证不通过,那么就直接抛出异常。

AuthenticationProvider的接口定义如下:

public interface AuthenticationProvider {
	/**
	 * 执行与以下合同相同的身份验证
	 * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
	 * .
	 * @param authentication 接受验证的对象
	 * @return 返回经过验证的对象. 可能为空
	 * @throws AuthenticationException 验证失败抛出异常
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
   /**
	 * 如果此AuthenticationProvide支持验证该Authentication对象,则返回true。
	 *返回true不代表一定支持身份验证,它只是表明它可以支持对其进行更仔细的评估。
	 */
	boolean supports(Class<?> authentication);
}

它主要有下面几个实现:

  1. DaoAuthenticationProvider:默认实现,使用账号密码认证方式,到数据库库获取认证数据信息。
  2. AnonymousAuthenticationProvider:游客身份登录认证方式
  3. RememberMeAuthenticationProvider:从cookies获取认证方式

数据库获取用户信息

我们一般是从数据库中获取用户数据验证,所以就挑第一个来看。DaoAuthenticationProvider类继承结构如下:

先看一下AbstractUserDetailsAuthenticationProviderauthenticate的实现:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                        () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                                                       "Only UsernamePasswordAuthenticationToken is supported"));
    //从authentication中获取到用户名
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    //尝试从缓存中获取到用户
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
        cacheWasUsed = false;
        try {
            //如果缓存中没有,就执行抽象方法,交给具体的子类获取用户数据
            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 {
        //检查用户账号状态,是否被禁用等
        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);
    }
    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
    //创建用户凭证
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;

总结一下这个类做的事:

  1. 从authentication中获取到用户名
  2. 根据用户名查询用户信息,查询细节交给子类
  3. 检查用户状态
  4. 验证用户身份是否正确(密码是否正确),也是交给子类实现
  5. 创建用户凭证并返回

其中,第二步和第四步需要看一下DaoAuthenticationProvider的实现:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			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);
		}
	}
}

@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    if (authentication.getCredentials() == null) {
       throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    }
    String presentedPassword = authentication.getCredentials().toString();
    if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
       throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    }
}

事情发展到这个地步,已经非常明确了,这里调用了UserDetailsServiceloadUserByUsername方法,并传入了username,我们之前自己实现了UserDetailsService接口,从数据库中将用户查询出来了。在查询出来之后,执行additionalAuthenticationChecks方法通过passwordEncoder验证了密码是否正确。至此,整个验证流程就走完了。

补充一下链路:

Filter -> AbstractAuthenticationProcessingFilter -> UsernamePasswordAuthenticationFilter -> AuthenticationManager -> ProviderManager -> AuthenticationProvider -> DaoAuthenticationProvider -> successHandler

其他过滤器

上面的整个流程都是我们基于UsernamePasswordAuthenticationFilter来做分析的,实际上,在Spring security中还有很多的filter,通过这些filter,spring security不仅实现了认证的逻辑,还实现了常见的web攻击防护。

下面列举一些常用的filter:

filter说明
SecurityContextPersistenceFilter用于将SecurityContext放入到session中
UsernamePasswordAuthenticationFilter登录认证的filter
RememberMeAuthenticationFilter通过cookie来实现记住我的功能的Filter
AnonymousAuthenticationFilter匿名认证处理过滤器,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中
SessionManagementFilter会话管理Filter,持久化用户登录信息,可以保存到session中,也可以保存到cookie或者redis中
ExceptionTranslationFilter异常处理过滤器,主要拦截后续过滤器(FilterSecurityInterceptor)操作中抛出的异常。
FilterSecurityInterceptor安全拦截过滤器类,访问的url权限不足时会抛出异常

上面那么多filter,它们在FilterChain中的先后顺序是非常重要的。对于每一个系统或者用户自定义的filter,spring security都要求必须指定一个order,用来做排序。对于系统的filter的顺序,是在一个FilterComparator类中定义的:

FilterComparator() {
		Step order = new Step(INITIAL_ORDER, ORDER_STEP);
		put(ChannelProcessingFilter.class, order.next());
		order.next(); // gh-8105
		put(WebAsyncManagerIntegrationFilter.class, order.next());
		put(SecurityContextPersistenceFilter.class, order.next());
		put(HeaderWriterFilter.class, order.next());
		put(CorsFilter.class, order.next());
		put(CsrfFilter.class, order.next());
		put(LogoutFilter.class, order.next());
		this.filterToOrder.put(
				"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
				order.next());
		this.filterToOrder.put(
				"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
				order.next());
		put(X509AuthenticationFilter.class, order.next());
		put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
		this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
		this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
				order.next());
		this.filterToOrder.put(
				"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
				order.next());
		put(UsernamePasswordAuthenticationFilter.class, order.next());
		order.next(); // gh-8105
		this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
		put(DefaultLoginPageGeneratingFilter.class, order.next());
		put(DefaultLogoutPageGeneratingFilter.class, order.next());
		put(ConcurrentSessionFilter.class, order.next());
		put(DigestAuthenticationFilter.class, order.next());
		this.filterToOrder.put(
				"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
				order.next());
		put(BasicAuthenticationFilter.class, order.next());
		put(RequestCacheAwareFilter.class, order.next());
		put(SecurityContextHolderAwareRequestFilter.class, order.next());
		put(JaasApiIntegrationFilter.class, order.next());
		put(RememberMeAuthenticationFilter.class, order.next());
		put(AnonymousAuthenticationFilter.class, order.next());
		this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
				order.next());
		put(SessionManagementFilter.class, order.next());
		put(ExceptionTranslationFilter.class, order.next());
		put(FilterSecurityInterceptor.class, order.next());
		put(SwitchUserFilter.class, order.next());
}

我们如果要自定义security的filter,也必须指定加入到那个filter的前面或者后面。这个在后面的实战中也会用到。

拓展:用户登录认证通过之后,下次进入的时候是怎么判断用户无需再次认证的呢?有兴趣的朋友可以自己研究一下源码。

总结

深入源码查看了一番,可能对很多类的职责还不太明确,接下来做一个总结。

  • AbstractAuthenticationProcessingFilter:基于filter的身份验证请求抽象处理类,需要传入一个请求路径,如果请求路径匹配上了,此过滤器将拦截这个请求并且将参数封装为一个token类,将这个token类交给AuthenticationManager。尝试执行身份验证(也就是登录认证)。

  • AuthenticationManager:用户认证的管理类,所有的认证请求都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。

  • ProviderManager:提供用户认证方式的管理类,AuthenticationManager提交的请求交由这个类,这个类再循环去Provider做认证,直到有一个认证成功为止,如果都没有认证成功,则说明该用户没有通过认证。

  • AuthenticationProvider:认证方式的实现类,一个provider对应一种认证方式的实现,比如用户账号密码登录,对应的实现类就是DaoAuthenticationProvider,如果是oauth2方式登录,那么就有一个OAuth2Provider的实现。

  • UserDetailService:获取用户信息的类,用户认证AuthenticationProvider中需要拿到系统中保存的用户信息,然后做认证。需要我们自己实现这个接口,返回用户信息。

认证流程图: