SpringOauth2.0源码分析之客户端认证(三)

1.概述

上面的2个章节我们分别介绍了:

  1. SpringOauth2.0源码分析之认证流程分析(一)
  2. SpringOauth2.0源码分析之 ProviderManager(二)

下面我们在此基础上进一步的分析认证过程的细节。本章节主要深入分析,客户端的用户名密码认证过程。整合认证流程如下图所示:
在这里插入图片描述


2.客户端认证流程源码详解

当用户通过用户名密码进行认证获取access_token的时候,首先需要认证的是客户端是否正确。验证方式是通过用户设置Header的Authorization ,最终序列化成Basic编码发送给认证服务。认证服务器通过BasicAuthenticationFilter过滤器进行实现。

2.1 BasicAuthenticationFilter 类结构分析

BasicAuthenticationFilter 类继承了OncePerRequestFilter,而OncePerRequestFilter是Spring框架自带的基础过滤器抽象类。

public class BasicAuthenticationFilter extends OncePerRequestFilter {
......
}

OncePerRequestFilter 是Spring默认的基础过滤器抽象类,其使用的设计模式是模板方法。封装核心的过滤条件,将需要实现的细节,移交给子类实现:

public abstract class OncePerRequestFilter extends GenericFilterBean {
   
    // 通过 final 定义的模板方法
	@Override
	public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
			throw new ServletException("OncePerRequestFilter just supports HTTP requests");
		}
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
		boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
		if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
			// Proceed without invoking this filter...
			filterChain.doFilter(request, response);
		}
		else {
			// Do invoke this filter...
			request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
			try {
			 // 具体的实现方式交给子类去实现
				doFilterInternal(httpRequest, httpResponse, filterChain);
			}
			finally {
				// Remove the "already filtered" request attribute for this request.
				request.removeAttribute(alreadyFilteredAttributeName);
			}
		}
	}
	// 子类需要实现的抽象方法,这里的实现是:BasicAuthenticationFilter 的doFilterInternal 方法
	protected abstract void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException;
}
2.2 BasicAuthenticationFilter 核心方法和参数分析

提取BasicAuthenticationFilter 的核心参数和代码块。

public class BasicAuthenticationFilter extends OncePerRequestFilter {
  // 通过构造方法,引入AuthenticationManager认证管理器。其核心的实现就是:ProviderManager
	private AuthenticationManager authenticationManager;
  // 构造方法
	public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
		this.authenticationManager = authenticationManager;
	}

 //模板方法的核心实现,用于认证客户端的正确性
	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
					throws IOException, ServletException {
		// 从Header 头信息中获取Authorization的值
		// 通过方法extractAndDecodeHeader(header, request) 反序列出用户名和密码
		final boolean debug = this.logger.isDebugEnabled();
		String header = request.getHeader("Authorization");
		if (header == null || !header.toLowerCase().startsWith("basic ")) {
			chain.doFilter(request, response);
			return;
		}
		try {
			String[] tokens = extractAndDecodeHeader(header, request);
			assert tokens.length == 2;
			String username = tokens[0];
			if (debug) {
				this.logger
						.debug("Basic Authentication Authorization header found for user '"
								+ username + "'");
			}
			// 校验当前客户端用户是不是需要重新认证
			if (authenticationIsRequired(username)) {
			// 封装UsernamePasswordAuthenticationToken对象,该对象实现了Authentication 接口。
				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
						username, tokens[1]);
				authRequest.setDetails(
						this.authenticationDetailsSource.buildDetails(request));
				// 通过构造函数引入的AuthenticationManager进行认证
				//具体的显现方式,根据UsernamePasswordAuthenticationToken所对应的认证策略
				// 这里使用的认证策略是DaoAuthenticationProvider
				Authentication authResult = this.authenticationManager
						.authenticate(authRequest);
				if (debug) {
					this.logger.debug("Authentication success: " + authResult);
				}
				// 认证通过已经,将当前信息写入到SecurityContextHolder中
				SecurityContextHolder.getContext().setAuthentication(authResult);
				this.rememberMeServices.loginSuccess(request, response, authResult);
	
				onSuccessfulAuthentication(request, response, authResult);
			}

		}
		catch (AuthenticationException failed) {
			SecurityContextHolder.clearContext();
			if (debug) {
				this.logger.debug("Authentication request for failed: " + failed);
			}
			this.rememberMeServices.loginFail(request, response);
			onUnsuccessfulAuthentication(request, response, failed);
			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, failed);
			}
			return;
		}
		// 认证通过以后,调用下一个过滤器
		chain.doFilter(request, response);
	}

	private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
			throws IOException {
		byte[] base64Token = header.substring(6).getBytes("UTF-8");
		byte[] decoded;
		try {
			decoded = Base64.getDecoder().decode(base64Token);
		}
		catch (IllegalArgumentException e) {
			throw new BadCredentialsException(
					"Failed to decode basic authentication token");
		}
		String token = new String(decoded, getCredentialsCharset(request));
		int delim = token.indexOf(":");
		if (delim == -1) {
			throw new BadCredentialsException("Invalid basic authentication token");
		}
		return new String[] { token.substring(0, delim), token.substring(delim + 1) };
	}

	private boolean authenticationIsRequired(String username) {
		// Only reauthenticate if username doesn't match SecurityContextHolder and user
		// isn't authenticated
		// (see SEC-53)
		Authentication existingAuth = SecurityContextHolder.getContext()
				.getAuthentication();

		if (existingAuth == null || !existingAuth.isAuthenticated()) {
			return true;
		}
		if (existingAuth instanceof UsernamePasswordAuthenticationToken
				&& !existingAuth.getName().equals(username)) {
			return true;
		}
}

通过基础认证服务器的核心代码模块分析可以知道,主要完成两件事情,第一:反序列化客户端的Header参数Authorization。第二:封装UsernamePasswordAuthenticationToken对象,调用认证管理器ProviderManager进行认证。

2.3 DaoAuthenticationProvider 类结构分析

DaoAuthenticationProvider 类实现了AbstractUserDetailsAuthenticationProvider抽象类的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
.....
}
2.4 AbstractUserDetailsAuthenticationProvider 类结构分析

AbstractUserDetailsAuthenticationProvider 实现了AuthenticationProvider的 supports(Class<?> authentication)
方法

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
		......
		}
2.5 AbstractUserDetailsAuthenticationProvider 核心方法参数分析

AbstractUserDetailsAuthenticationProvider 主要实现了AuthenticationProvider的两个核心方法:
AuthenticationProvider,supports。其次抽象出具体是实现细节方法:retrieveUser。交给子类:
DaoAuthenticationProvider 进行实现

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {
		// 实现	AuthenticationProvider 的认证方法
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
			// 调用子类实现的retrieveUser()的方法
				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;
				}
			}
			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}
		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// 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);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}
		postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	
	//实现了AuthenticationProvider的supports方法
	public boolean supports(Class<?> authentication) {
	//根据配置的策略方法为UsernamePasswordAuthenticationToken
			return (UsernamePasswordAuthenticationToken.class
					.isAssignableFrom(authentication));
		}

  // 检索用户细节交给子类实现
	protected abstract UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException;
}
2.6 DaoAuthenticationProvider 核心方法参数分析
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
	
	// 设置当前密码的加密模式
	private PasswordEncoder passwordEncoder;
   // 设置查询用户实现细节
	private UserDetailsService userDetailsService;

	public DaoAuthenticationProvider() {
		setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
	}

	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);
		}
	}

	private void prepareTimingAttackProtection() {
		if (this.userNotFoundEncodedPassword == null) {
			this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
		}
	}

	private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
		if (authentication.getCredentials() != null) {
			String presentedPassword = authentication.getCredentials().toString();
			this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
		}
	}
}
2.7 ClientDetailsUserDetailsService 类说明

ClientDetailsUserDetailsService 实现了UserDetailsService,通过loadUserByUsername()方法查询当前客户端是否存在。

public class ClientDetailsUserDetailsService implements UserDetailsService {

	private final ClientDetailsService clientDetailsService;
	private String emptyPassword = "";
	
	public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
		this.clientDetailsService = clientDetailsService;
	}
	
	/**
	 * @param passwordEncoder the password encoder to set
	 */
	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		this.emptyPassword = passwordEncoder.encode("");
	}

 // 通过 loadUserByUsername 查询当前的Client是否存在。
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		ClientDetails clientDetails;
		try {
			clientDetails = clientDetailsService.loadClientByClientId(username);
		} catch (NoSuchClientException e) {
			throw new UsernameNotFoundException(e.getMessage(), e);
		}
		String clientSecret = clientDetails.getClientSecret();
		if (clientSecret== null || clientSecret.trim().length()==0) {
			clientSecret = emptyPassword;
		}
		return new User(username, clientSecret, clientDetails.getAuthorities());
	}

}

由于我们采用的配置是默认配置,把当前的客户端存储在内存中,InMemoryClientDetailsService,通过InMemoryClientDetailsService的源码可以发现,其内部通过HashMap维护客户端信息,具体实现如下:

public class InMemoryClientDetailsService implements ClientDetailsService {
// 默认存储客户端信息的内存集合
  private Map<String, ClientDetails> clientDetailsStore = new HashMap<String, ClientDetails>();

  public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
  // 查询是否存在当前客户端
    ClientDetails details = clientDetailsStore.get(clientId);
    if (details == null) {
      throw new NoSuchClientException("No client with requested id: " + clientId);
    }
    return details;
  }

  public void setClientDetailsStore(Map<String, ? extends ClientDetails> clientDetailsStore) {
    this.clientDetailsStore = new HashMap<String, ClientDetails>(clientDetailsStore);
  }
}

至此,客户端认证的过程源码基本上分析完成。这里用户可以自己配置ClientDetailsService,UserDetailsService可以用户自己实现。


3.结语

通过客户端认证源码分析可以得出,客户端的认证会发生在过滤器:BasicAuthenticationFilter中,其发生在用户的用户名密码认证之前。其内部认证通过ProviderManager策略模板,根据传入的Authentication类型指定认证的策略DaoAuthenticationProvider,通过DaoAuthenticationProvider查询当前客户端用户密码是否存在。我们项目目前默认采用的是:InMemoryClientDetailsService,这里用户可以自己去实现客户端查询细节,通过启动配置类进行配置通过ClientDetailsServiceConfigurer的withClientDetails(ClientDetailsService clientDetailsService)方法进行设置。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值