写点什么

SpringSecurity 认证流程分析

作者:周杰伦本人
  • 2022 年 5 月 06 日
  • 本文字数:12275 字

    阅读完需:约 40 分钟

SpringSecurity 认证流程分析

AuthenticationManager

AuthenticationManager 是认证管理器 它定义了 Spring Security 过滤器要如何执行认证操作。AuthenticationManager 在认证后会返回一个 Authentication 对象,它是一个接口,默认实现类是 ProviderManager

AuthenticationProvider

AuthenticationProvider 针对不同的身份类型执行具体的身份认证。


DaoAuthenticationProvider 用来支持用户名 密码登录认证


RememberMeAuthenticationProvider 用来支持记住我的认证


当用户使用用户名密码方式登录的时候,对应的 AuthenticationProvider 是 DaoAuthenticationProvider,DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider,认证时调用 authenticate 方法

AbstractUserDetailsAuthenticationProvider



public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { protected final Log logger = LogFactory.getLog(this.getClass()); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private UserCache userCache = new NullUserCache(); private boolean forcePrincipalAsString = false; protected boolean hideUserNotFoundExceptions = true; private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks(); private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks(); private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
public AbstractUserDetailsAuthenticationProvider() { }
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public final void afterPropertiesSet() throws Exception { Assert.notNull(this.userCache, "A user cache must be set"); Assert.notNull(this.messages, "A message source must be set"); this.doAfterPropertiesSet(); }
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false;
try { user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("User '" + username + "' not found"); if (this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); }
throw var6; }
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); }
try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; }
cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); }
this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); }
Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); }
return this.createSuccessAuthentication(principalToReturn, authentication, user); }
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
protected void doAfterPropertiesSet() throws Exception { }
public UserCache getUserCache() { return this.userCache; }
public boolean isForcePrincipalAsString() { return this.forcePrincipalAsString; }
public boolean isHideUserNotFoundExceptions() { return this.hideUserNotFoundExceptions; }
protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public void setForcePrincipalAsString(boolean forcePrincipalAsString) { this.forcePrincipalAsString = forcePrincipalAsString; }
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) { this.hideUserNotFoundExceptions = hideUserNotFoundExceptions; }
public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); }
public void setUserCache(UserCache userCache) { this.userCache = userCache; }
public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); }
protected UserDetailsChecker getPreAuthenticationChecks() { return this.preAuthenticationChecks; }
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) { this.preAuthenticationChecks = preAuthenticationChecks; }
protected UserDetailsChecker getPostAuthenticationChecks() { return this.postAuthenticationChecks; }
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { this.postAuthenticationChecks = postAuthenticationChecks; }
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; }
private class DefaultPostAuthenticationChecks implements UserDetailsChecker { private DefaultPostAuthenticationChecks() { }
public void check(UserDetails user) { if (!user.isCredentialsNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired"); throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired")); } } }
private class DefaultPreAuthenticationChecks implements UserDetailsChecker { private DefaultPreAuthenticationChecks() { }
public void check(UserDetails user) { if (!user.isAccountNonLocked()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked"); throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked")); } else if (!user.isEnabled()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled"); throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled")); } else if (!user.isAccountNonExpired()) { AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired"); throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired")); } } }}
复制代码


重点看一下 authenticate 方法:首先从登录数据中获取用户名,根据用户名去缓存中查询用户对象,如果查询不到,则根据用户名调用 retrieveUser 方法从数据中加载用户;如果没有加载到用户,则抛出异常。拿到用户对象之后,首先调用 preAuthenticationChecks.check 方法进行用户状态检查,然后调用 additionalAuthenticationChecks 方法进行密码的校验操作,最后调用 postAuthenticationChecks.check 方法检查密码是否过期,当所有步骤都顺利完成后,调用 createSuccessAuthentication 创建一个认证后的 UsernamePasswordAuthenticationToken 对象并返回。

DaoAuthenticationProvider

DaoAuthenticationProvider 实现了 AbstractUserDetailsAuthenticationProvider 的抽象方法




public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; private PasswordEncoder passwordEncoder; private volatile String userNotFoundEncodedPassword; private UserDetailsService userDetailsService; private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() { this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); }
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } }
protected void doAfterPropertiesSet() { Assert.notNull(this.userDetailsService, "A UserDetailsService must be set"); }
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection();
try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } }
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword()); if (upgradeEncoding) { String presentedPassword = authentication.getCredentials().toString(); String newPassword = this.passwordEncoder.encode(presentedPassword); user = this.userDetailsPasswordService.updatePassword(user, newPassword); }
return super.createSuccessAuthentication(principal, authentication, user); }
private void prepareTimingAttackProtection() { if (this.userNotFoundEncodedPassword == null) { this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword"); }
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) { if (authentication.getCredentials() != null) { String presentedPassword = authentication.getCredentials().toString(); this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword); }
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); this.passwordEncoder = passwordEncoder; this.userNotFoundEncodedPassword = null; }
protected PasswordEncoder getPasswordEncoder() { return this.passwordEncoder; }
public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; }
protected UserDetailsService getUserDetailsService() { return this.userDetailsService; }
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) { this.userDetailsPasswordService = userDetailsPasswordService; }}
复制代码


主要是 retrieveUser 方法:获取用户对象的方法,具体做法是调用 UserDetailsService 的 loadUserByUsername 方法去数据库中查询。

ProviderMananger

ProviderMananger 是 AuthenticationManager 的一个重要实现类。


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;    boolean debug = logger.isDebugEnabled();    Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var8.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); }
try { result = provider.authenticate(authentication); if (result != null) { this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } }
if (result == null && this.parent != null) { try { result = parentResult = this.parent.authenticate(authentication); } catch (ProviderNotFoundException var11) { } catch (AuthenticationException var12) { parentException = var12; lastException = var12; } }
if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); }
if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); }
return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); }
if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); }
throw lastException; }}
复制代码


  1. 首先获取 authentication 对象的模型

  2. 定义异常,认证结果等变量

  3. getProviders 方法获取当前 ProviderManager 所代理的所有 AuthenticationProvide 对象,遍历这些 AuthenticationProvider 对象进行身份认证。

  4. 判断 AuthenticationProvider 是否支持当前 Authentication,如果不支持,继续处理下一个 AuthenticationProvider 对象

  5. 调用 provider.authenticate 方法进行身份认证,如果认证成功,返回认证后的 Authentication 对象,同时调用 copyDetails 方法给 Authentication 对象的 details 属性赋值。对于可能是多个 AuthenticationProvider 执行认证操作,所以如果抛出异常,则通过 lastException 变量记录。

  6. 在 for 循环执行完成后,如果 result 还是没有值,说明所有的 AuthenticationProvider 都认证失败,此时如果 parent 不为空,则调用 parent 的 authenticate 方法进行认证。

  7. 如果 result 不为空,将 result 中的凭证擦擦,防止泄露。如果使用了用户名密码的方式登录,那么所谓的擦除就是将密码字段设置为 null,同时将登录事件发布出去。

  8. 如果前面没能返回 result,说明认证失败。如果 lastException 为 null,说明 parent 为 null 或者没有认证或者认证失败了但没有抛出异常,此时构造 ProviderNotFoundException 赋值给 lastException

  9. 如果 parentResult 为 null,发布认证失败事件。

  10. 抛出 lastException

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是抽象类,如果使用用户名密码的方式登录,那么它对应的实现类是 UsernamePasswordAuthenticationFilter,构造出来的 Authentication 是 UsernamePasswordAuthenticationToken




public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Request is to process authentication"); }
Authentication authResult; try { authResult = this.attemptAuthentication(request, response); if (authResult == null) { return; }
this.sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); return; }
if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); }
this.successfulAuthentication(request, response, chain, authResult); } }
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return this.requiresAuthenticationRequestMatcher.matches(request); }
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); }
SecurityContextHolder.getContext().setAuthentication(authResult); this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); }
this.successHandler.onAuthenticationSuccess(request, response, authResult); }
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication request failed: " + failed.toString(), failed); this.logger.debug("Updated SecurityContextHolder to contain null Authentication"); this.logger.debug("Delegating to authentication failure handler " + this.failureHandler); }
this.rememberMeServices.loginFail(request, response); this.failureHandler.onAuthenticationFailure(request, response, failed); }}
复制代码


  1. 首先通过 requiresAuthentication 方法来判断当前请求是不是登录认证请求,如果不是,直接走下一个过滤器

  2. 调用 attemptAuthentication 方法获取一个经过认证后的 Authentication 对象,attemptAuthentication 是抽象方法,具体子类在 UsernamePasswordAuthenticationFilter 中。

  3. 认证成功后,通过 sessionStrategy.onAuthentication 方法来处理 session 并发问题。

  4. continueChainBeforeSuccessfulAuthentication 表示继续下一个过滤器链,默认是 false,即认证成功后不再执行下一个过滤器

  5. unsuccessfulAuthentication 方法处理认证失败,主要做了三件事:

  6. SecurityContextHolder 清除数据

  7. 处理 Cookie

  8. 发布认证成功调用认证失败的回调方法

  9. successfulAuthentication 方法处理认证成功,主要做了四件事:

  10. SecurityContextHolder 存入用户信息

  11. 处理 Cookie

  12. 发布认证成功事件

  13. 调用认证成功的回调方法。


AbstractAuthenticationProcessingFilter 中的 attemptAuthentication 是 UsernamePasswordAuthenticationFilter 来实现的

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 String usernameParameter = "username";    private String passwordParameter = "password";    private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); }
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()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; }
if (password == null) { password = ""; }
username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
@Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); }
@Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); }
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); }
public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; }
public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; }
public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; }
public final String getUsernameParameter() { return this.usernameParameter; }
public final String getPasswordParameter() { return this.passwordParameter; }}
复制代码


attemptAuthentication 首先确认是 post 请求,然后获取用户名密码,构造 authRequest 调用 getAuthenticationManager().authenticate(authRequest)进行认证,即 ProviderManager 里的 authenticate 方法。

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2020.02.29 加入

还未添加个人简介

评论

发布
暂无评论
SpringSecurity认证流程分析_5月月更_周杰伦本人_InfoQ写作社区