认证流程

图片来自于:黑马程序员 SpringSecurity 认证课程
认证过程:
- 用户提交用户名、密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
- 然后过滤器将 Authentication 提交至认证管理器(AuthenticationManager)进行认证
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到其中。可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它
的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个
List 列表,存放多种认证方式,最终实际的认证工作是由
AuthenticationProvider 完成的。咱们知道 web 表单的对应的 AuthenticationProvider 实现类为
DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService 负责 UserDetails 的获取。最终
AuthenticationProvider 将 UserDetails 填充至 Authentication。
认证核心组件的大体关系如下:
图片来自于:黑马程序员 SpringSecurity 认证课程
知识点认识
Authentication
我们所面对的系统中的用户,在 Spring Security 中被称为主体(principal)。主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。主体的概念实际上来自 Java Security,Spring Security 通过一层包装将其定义为一个 Authentication。
public interface Authentication extends Principal, Serializable {// 获取主体授权列表Collection<? extends GrantedAuthority> getAuthorities();// 获取主体凭证,一般为密码Object getCredentials();// 获取主体携带的详细信息Object getDetails();// 获取主体,通常为usernameObject getPrincipal();// 获取当前主体是否认证成功boolean isAuthenticated();// 设置当前主体是否认证成功状态void setAuthenticated(boolean var1) throws IllegalArgumentException;}
AuthenticateProvider
Spring Security 认证的过程其实就是一个构建 Authentication 的过程。Authentication 在 Spring Security 的各个 AuthenticationProvider 中流动,AuthenticationProvider 被 Spring Security 定义为一个验证过程:
public interface AuthenticationProvider {// 验证完成,成功,返回一个验证完成的AuthenticationAuthentication authenticate(Authentication var1) throws AuthenticationException;// 是否支持验证当前的Authentication类型boolean supports(Class<?> var1);}
大部分场景下身份验证都是基于用户名和密码进行的,所以 Spring Security 提供了一个 UsernamePasswordAuthenticationToken 用于代指这一类证。,每一个登录用户即主体都被包装为一个 UsernamePasswordAuthenticationToken,从而在 Spring Security 的各个 AuthenticationProvider 中流动。
ProviderManager
一次完整的认证可以包含多个 AuthenticationProvider,一般由 ProviderManager 管理。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {private static final Log logger = LogFactory.getLog(ProviderManager.class);private AuthenticationEventPublisher eventPublisher;// AuthenticationProvider 列表private List<AuthenticationProvider> providers;protected MessageSourceAccessor messages;private AuthenticationManager parent;private boolean eraseCredentialsAfterAuthentication;public ProviderManager(List<AuthenticationProvider> providers) {this(providers, (AuthenticationManager)null);}public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {this.eventPublisher = new ProviderManager.NullEventPublisher();this.providers = Collections.emptyList();this.messages = SpringSecurityMessageSource.getAccessor();this.eraseCredentialsAfterAuthentication = true;Assert.notNull(providers, "providers list cannot be null");this.providers = providers;this.parent = parent;this.checkState();}public void afterPropertiesSet() {this.checkState();}private void checkState() {if (this.parent == null && this.providers.isEmpty()) {throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");}}// 迭代AuthenticationProvider 列表,进行认证,返回最终结果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();// 判断AuthenticationProvider 是否支持当前验证if (provider.supports(toTest)) {if (debug) {logger.debug("Authentication attempt using " + provider.getClass().getName());}try {// 执行AuthenticationProvider的认证。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;}}private void prepareException(AuthenticationException ex, Authentication auth) {this.eventPublisher.publishAuthenticationFailure(ex, auth);}private void copyDetails(Authentication source, Authentication dest) {if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;token.setDetails(source.getDetails());}}public List<AuthenticationProvider> getProviders() {return this.providers;}public void setMessageSource(MessageSource messageSource) {this.messages = new MessageSourceAccessor(messageSource);}public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");this.eventPublisher = eventPublisher;}public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {this.eraseCredentialsAfterAuthentication = eraseSecretData;}public boolean isEraseCredentialsAfterAuthentication() {return this.eraseCredentialsAfterAuthentication;}private static final class NullEventPublisher implements AuthenticationEventPublisher {private NullEventPublisher() {}public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {}public void publishAuthenticationSuccess(Authentication authentication) {}}}
自定义 AuthenticationProvider
Spring Security 提供了多种常见的认证技术,包括但不限于以下几种:
- HTTP 层面的认证技术,包括 HTTP 基本认证和 HTTP 摘要认证两种。
- 基于 LDAP 的认证技术(Lightweight Directory Access Protocol,轻量目录访问协议)。
- 聚焦于证明用户身份的 OpenID 认证技术。
- 聚焦于授权的 OAuth 认证技术。
- 系统内维护的用户名和密码认证技术。
其中,使用最为广泛的是由系统维护的用户名和密码认证技术,通常会涉及数据库访问。为了更好地按需定制,Spring Security 并没有直接糅合整个认证过程,而是提供了一个抽象的 AuthenticationProvider,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();}// 返回一个认证通过的Authenticationreturn 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;}// 此认证支持UsernamePasswordAuthenticationToken及其衍生类认证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"));}}}}
在 AbstractUserDetailsAuthenticationProvider 中实现了基本的认证流程,通过继承 AbstractUserDetailsAuthenticationProvider,并实现 retrieveUser 和 additionalAuthenticationChecks 两个抽象方法即可自定义核心认证过程,灵活性非常高。示例,Spring Security 用于处理 UsernamePasswordAuthenticationToken 的 DaoAuthenticationProvider:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";// 密码加密private PasswordEncoder passwordEncoder;private volatile String userNotFoundEncodedPassword;// UserDetailsService 用来获取用户信息private UserDetailsService userDetailsService;private UserDetailsPasswordService userDetailsPasswordService;public DaoAuthenticationProvider() {this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());}// 添加附加认证@Overrideprotected 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");}// 获取用户@Overrideprotected 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;}}
UserDetailsService
道 DaoAuthenticationProvider 处理了 web 表单的认证逻辑,认证成功后既得到一个 Authentication(UsernamePasswordAuthenticationToken 实现),里面包含了身份信息(Principal)。
这个身份信息就是一个 Object,大多数情况下它可以被强转为 UserDetails 对象。DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取用户信息 UserDetails(包含密码)。
而后 DaoAuthenticationProvider 会去对比 UserDetailsService 提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据。
因此可以通过将自定义的 UserDetailsService 公开为 spring bean 来定义自定义身份验证。
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}
PasswordEncoder
DaoAuthenticationProvider 认证处理器通过 UserDetailsService 获取到 UserDetails 后,它是如何与请求 Authentication 中的密码做对比呢?
在这里 Spring Security 为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider 通过 PasswordEncoder 接口的 matches 方法进行密码的对比,而具体的密码对比细节取决于实现:
public interface PasswordEncoder {String encode(CharSequence var1);boolean matches(CharSequence var1, String var2);default boolean upgradeEncoding(String encodedPassword) {return false;}}
而 Spring Security 提供很多内置的 PasswordEncoder,能够开箱即用,使用某种 PasswordEncoder 只需要进行如下声明即可,如下:
@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}
