1.概述
上面的2个章节我们分别介绍了:
下面我们在此基础上进一步的分析认证过程的细节。本章节主要深入分析,客户端的用户名密码认证过程。整合认证流程如下图所示:
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)方法进行设置。