前言
要搞清楚 SpringSecurity 的认证流程,我们就必须要认识与之相关的4个基本组件,同时还要熟悉接入认证功能的过滤器 AbstractAuthenticationProcessingFilter,这几个类搞明白了,那么 SpringSecurity 的认证流程也就清楚了。
简介
关于 SpringSecurity 认证流程的 5 个基本组件有:Authentication、AuthenticationManager、ProviderManager、AuthenticationProvider 和 AbstractAuthenticationProcessingFilter。
1、Authentication
在 SpringSecurity 中,用户的认证信息主要由 Authentication 的实现类来保存,Authentication 的接口定义如下:
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
- getAuthorities 方法:用来获取用户的权限。
- getCredentials 方法:用来获取用户凭证,一般来说就是密码。
- getDetails 方法:用来获取用户携带的详细信息,可能是当前请求之类等。
- getPrincipal 方法:用来获取当前用户,例如是一个用户名或者一个用户对象。
- isAuthenticated 方法:当前用户是否认证成功。
Authentication 接口拥有多个实现类,不同的实现类对应着不同的认证处理方式。
2、AuthenticationManager 认证管理器
AuthenticationManager 定义了 SpringSecurity 要如何执行认证操作。当 AuthenticationManager 认证成功后,会返回一个 Authentication 对象,这个 Authentication 对象会被设置到 SecurityContextHolder 中。接口定义如下:
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationManager {
/**
* 尝试对传入的 Authentication 对象进行身份验证,如果成功则返回完全填充的 Authentication 对象(包括授予的权限)。
*
* AuthenticationManager 必须遵守以下有关异常的约定:
* 如果帐户被禁用,则必须抛出 DisabledException
* 如果帐户被锁定,则必须抛出 LockedException
* 如果提供的凭据不正确,则必须抛出 BadCredentialsException。
* 虽然上述异常是可选的,但 AuthenticationManager 必须始终验证凭据。
*
* 应按上述顺序测试异常并抛出(即,如果帐户被禁用或锁定,则立即拒绝身份验证请求,并且不执行凭据测试过程)。
* 这可以防止针对禁用或锁定的帐户测试凭据。
*
* @param authentication – 身份验证请求对象
* @return 包括凭据的完全身份验证对象
* @throws AuthenticationException – 如果身份验证失败
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager 对传入的 Authentication 对象进行身份认证,此时传入的 Authentication 参数只有用户名/密码等简单的属性,如果认证成功,返回的 Authentication 对象的属性会得到完全填充,包括用户所具备的角色信息。
AuthenticationManager 接口有诸多实现类,在 SpringSecurity 中,默认用的是 ProviderManager 实现类,一般我们在实际应用中用的最多的也是 ProviderManager 实现类。
3、AuthenticationProvider
SpringSecurity 支持多种不同的认证方式,不同的认证方式对应不同的身份类型,AuthenticationProvider 就是针对不同的身份类型执行具体的身份认证。比如 DaoAuthenticationProvider 用来支持用户名/密码登录认证,RememberAuthenticationProvider 用来支持 “记住我” 的认证。AuthenticationProvider 的源码如下所示:
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
/**
* 使用与 AuthenticationManager#authenticate(Authentication) 相同的契约执行身份验证。
*
* @param authentication 身份验证请求对象
* @return 返回一个完全经过身份验证的对象(包括凭据)。
* 如果 AuthenticationProvider 无法支持传入的 Authentication 对象的身份验证,则可能返回 null。
* 在这种情况下,将尝试下一个支持所呈现的 Authentication 类的 AuthenticationProvider。
*
* @throws 如果身份验证失败将会抛出 AuthenticationException 异常
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 如果此 AuthenticationProvider 支持指示的 Authentication 对象,则返回 true。
*
* 返回 true 并不保证 AuthenticationProvider 能够对所呈现的 Authentication 类实例进行身份验证。
* 它只是表明它可以支持对其进行更仔细的评估。AuthenticationProvider 仍可以从 authenticate(Authentication) 方法
* 返回 null,以指示应尝试另一个 AuthenticationProvider。
*
* 在运行时由 ProviderManager 选择能够执行身份验证的 AuthenticationProvider。
*
* @param authentication
* @return 如果实现可以更仔细地评估所呈现的 Authentication 类,则返回 true
*/
boolean supports(Class<?> authentication);
}
authenticate 方法用来执行具体的身份认证。
supports 方法用来判断当前 AuthenticationProvider 是否支持对应的身份类型。
当使用用户名/密码的方式登录时,对应的 AuthenticationProvider 实现类是 DaoAuthenticationProvider,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authentication 方法,所以具体的认证逻辑在 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中。
AbstractUserDetailsAuthenticationProvider 的源码如下所示:
package org.springframework.security.authentication.dao;
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
// 是否隐藏用户名查找失败的异常,默认为true
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
@Override
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");
doAfterPropertiesSet();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
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);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
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 username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
this.forcePrincipalAsString = forcePrincipalAsString;
}
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
@Override
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 DefaultPreAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
}
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
}
该类是个抽象类,抽象方法在它的实现类 DaoAuthenticationProvider 中完成。
(1)该类先声明了一个用户缓存对象 userCache,默认情况下是没有启用缓存对象的。
(2)hideUserNotFoundExceptions 表示是否隐藏用户名查找失败的异常,默认为 true。
为了确保系统安全,用户在登录失败时只会给出一个模糊提示,例如 “用户名或密码输入错误”。在 SpringSecurity 内部,如果用户名查找失败,则会抛出 UsernameNotFoundException 异常,但是该异常会被自动隐藏,转而通过一个 BadCredentialsException 异常来代替它,这样开发者在处理登录失败异常时无论是用户名输入错误还是密码输入错误收到的总是 BadCredentialsException 异常。
(3)forcePrincipalAsString 表示是否强制将 Principal 对象当成字符串来处理,默认是 false。Authentication 中的 principal 属性类型是一个 Object,正常来说,通过 principal 属性可以获取到当前登录用户对象(即 UserDetails),但是如果 forcePrincipalAsString 设置为 true,则 Authentication 中的 principal 属性返回的就是当前登录用户名而不是用户对象。
(4)preAuthenticationChecks 对象则是用于做用户状态检查,在用户认证过程中,需要检验用户状态是否正常,例如账户是否被锁定、账户是否可用、账户是否过期等。
(5)postAuthenticationChecks 对象主要负责在密码校验成功后,检查密码是否过期。
(6)additionalAuthenticationChecks 是一个抽象方怯,主要就是校验密码,具体的实现在 DaoAuthenticationProvider 中。
(7)authenticate 方法就是核心的校验方法了。在该方法中,首先从登录数据中获取用户名,然后根据用户名到缓存中查询用户对象,如果查询不到,则根据用户名调用 retrieveUser 方法从数据库中加载用户;如果没有加载到用户,则抛出异常(用户不存在异常会被隐藏)。拿到用户对象之后,首先调用 preAuthenticationChecks.check 方法进行用户状态检查,然后调用 additionalAuthenticationChecks 方法进行密码的校验操作,最后调用 postAuthenticationChecks.check 方法检查密码是否过期,当所有的步骤都顺利完成后,调用 createSuccessAuthentication 方法创建一个认证后的 UsernamePasswordAuthenticationToken 对象并返回,认证后的对象中包含了认证主题、凭证以及角色等信息。
有几个抽象方法是在 DaoAuthenticationProvider 中实现的,下面是对应的源码:
package org.springframework.security.authentication.dao;
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() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@Override
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
@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
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(USER_NOT_FOUND_PASSWORD);
}
}
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;
}
}
在 DaoAuthenticationProvider 中:
(1)首先定义了 USER_NOT_FOUND_PASSWORD 常量,这个是当用户查找失败时的默认密码;passwordEncoder 是一个密码加密和比对工具;userNotFoundEncodedPassword 变量则用来保存默认密码加密后的值;userDetailsService 是一个用户查找工具;userDetailsPasswordService 则用来提供密码修改服务。
(2)在 DaoAuthenticationProvider 的构造方法中,默认就会指定 PasswordEncoder,当然开发者也可以通过 set 方法自定义 PasswordEncoder。
(3)additionalAuthenticationChecks 方法主要进行密码校验,该方法第一个参数 userDetails 是从数据库中查询出来的用户对象,第二个参数 authentication 则是登录用户输入的参数。从这两个参数中分别提取出来用户密码,然后调用 passwordEncoder.matches 方法进行密码比对。
(4)retrieveUser 方法则是获取用户对象的方法,具体就是调用 loadUserByUsername 方法从数据库中进行查询。
在 retrieveUser 方法中,有一个值得关注的地方。在该方法一开始,首先会调用 prepareTimingAttackProtection 方法,该方法的作用是使用 PasswordEncoder 对常量 USER_NOT_FOUND_PASSWORD 进行加密,并将加密结果保存在 userNotFoundEncodedPassword 变量中。当根据用户名查找用户时,如果抛出了 UsernameNotFoundException 异常,则调用 mitigateAgainstTimingAttack 方法进行密码比对。可能有的人会说,用户都没查找到,怎么比对密码?这里需要注意的是在调用 mitigateAgainstTimingAttack 方法进行密码比对时,使用了 userNotFoundEncodedPassword 变量作为默认密码和登录请求传来的用户密码进行比对。这是一个一开始就注定要失败的密码比对,那么为什么还要进行比对呢?这主要是为了避免旁道攻击。如果根据用户名查找用户失败,就直接抛出异常而不进行密码比对,那么黑客在经过大量的测试后就会发现有的请求耗费时间明显小于其他请求,进而可以得出该请求的用户名是一个不存在的用户名(因为用户名不存在,所以不需要密码比对,进而节省时间),这样就可以获取到系统信息。为了避免这一问题,所以当用户查找失败时也会调用 mitigateAgainstTimingAttack 方法进行密码比对,这样就可以迷惑黑客。
(5)createSuccessAuthentication 方法则是在登录成功后,创建一个全新的 UsernamePasswordAuthenticationToken 对象,同时会判断是否需要进行密码升级,如果需要进行密码升级,就会在该方法中进行加密方案升级。
4、ProviderManager
ProviderManager 是 AuthenticationManager 的一个重要实现类。
在 SpringSecurity 中,由于系统可能同时支持多种不同的认证方式,例如同时支持用户名/密码认证、RememberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。换句话说,在 ProviderManager 中存在一个AuthenticationProvider 列表,在 ProviderManager 中遍历列表中的每一个 AuthenticationProvider 去执行身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当 ProviderManager 认证失败之后就可以进入到parent 中再次进行认证。理论上来说,ProviderManager 的 parent 可以是任意类型的 AuthenticationManager,但是通常都是由 ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。
ProviderManager 本身也可以有多个,多个 ProviderManager 共用同一个 parent,当存在多 个过滤器链的时候非常有用。当存在多个过滤器链时不同的路径可能对应不同的认证方式,但是不同路径可能又会同时存在一些共有的认证方式,这些共有的认证方式可以在parent 中统 一处理。
ProviderManager 的 authenticate 方法如下:
@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;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
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;
}
}
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();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
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;
}
(1)首先获取 authentication 对象的类型。
(2)分别定义当前认证过程抛出的异常、parent 中认证时抛出的异常、当前认证结果以及 parent 中认证结果对应的变量。
(3)getProviders 方法用来获取当前 ProviderManager 所代理的所有 AuthenticationProvider 对象,遍历这些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,同时将登录成功的事件发布出去。如果用户认证成功,此时就将 result 返回,后面的代码也就不再执行了。
发布登录成功事件需要 parentResult 为 null。如果 parentResult 不为 null,表示在 parent 中已经认证成功了,认证成功的事件也已经在 parent 中发布出去了,这样会导致认证成功的事件重复发布。
(8)如果前面没能返回 result,说明认证失败。如果 lastException 为 null,说明 parent 为 null 或者没有认证亦或者认证失败了但是没有抛出异常,此时构造 ProviderNotFoundException 异常赋值给 lastException。
(9)如果 parentException 为 null,发布认证失败事件(如果 parentException 不为 null,则说明认证失败事件已经发布过了)
(10)最后抛出 lastException 异常。
5、AbstractAuthenticationProcessingFilter 过滤器
经过上面的介绍,相信大家已经熟悉 Authentication、AuthenticationManager、ProviderManager 和 AuthenticationProvider 的工作原理了,那么这些组件是如何关联起来的呢,答案是通过 AbstractAuthenticationProcessingFilter 过滤器进行关联。
AbstractAuthenticationProcessingFilter 是 SpringSecurity 的一个非常重要的过滤器,它可以用来处理任何提交给它的身份认证,下面是它的工作流程图:
AbstractAuthenticationProcessingFilter 作为一个抽象类,如果使用用户名/密码的方式登录,那么他对应的实现类是 UsernamePasswordAuthenticationFilter,构造出来的 Authentication 对象则是 UsernamePasswordAuthenticationToken。至于 AuthenticationManager,一般他的实现类就是 ProviderManager,这里在 ProviderManager 中进行认证,认证成功就会进入认证成功的回调,否则就进入认证失败的回调。对上个流程图再做进一步的细化如下所示:
我们先看下 AbstractAuthenticationProcessingFilter 的源码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
......
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
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);
}
}
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;
}
......
}
(1)首先通过 requiresAuthentication 方法来判断当前请求是不是登录认证请求,如果是登录认证请求,就执行接下来的认证代码;如果不是认证请求,则直接继续走剩余的过滤器即可。
(2)调用 attemptAuthentication 方法来获取一个经过认证后的 Authentication 对象,attemptAuthentication 方法是一个抽象方法,具体实现在它的子类 UsernamePasswordAuthenticationFilter 中。
(3)认证成功后,通过 sessionStrategy.onAuthentication 方法来处理 session 并发问题。
(4)continueChainBeforeSuccessfulAuthentication 变量用来判断请求是否还需耍继续问下走。默认情况下该参数的值为 false,即认证成功后,后续的过滤器将不再执行了。
(5)unsuccessfulAuthentication 方法用来处理认证失败事宜,主要做了三件事:① 从 SecurityContextHolder 中清除数据;② 清除Cookie 等信息;③ 调用认证失败的回调方法。
(6)successfulAuthentication 方法主要用来处理认证成功事宜,主要做了四件事:① 向 SecurityContextHolder 中存入用户信息;② 处理 Cookie;③ 发布认证成功事件,这个事件类型是 InteractiveAuthenticationSuccessEvent,表示通过一些自动交互的方式认证成功;④ 调用认证成功的回调方法。
还有一个抽象方法是在它的继承类中实现的,源码如下:
package org.springframework.security.web.authentication;
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.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
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;
}
}
(1)首先声明了默认情况下登录表单的用户名字段和密码字段,用户名字段的 key 默认是 username,密码字段的 key 默认是password。当然,这两个字段也可以自定义,自定义的话可以在 SecurityConfig 中配置的 .usernameParameter(“uname”) 和 .passwordParameter(“passwd”)。
(2)在 UsernamePasswordAuthenticationFilter 过滤器构建的时候,指定了当前过滤器只用来处理登录请求,默认的登录请求是 /login,当然我们也可以自行配置。
(3)接下来就是最重要的 attemptAuthentication 方法了,在该方法中,首先确认请求是 post 类型;然后通过 obtainUsername 和obtainPassword 方法分别从请求中提取出用户名和密码,具体的提取过程就是调用 request.getParameter 方法;拿到登录请求传来的用户名/密码之后,构造出一个 authRequest,然后调用 getAuthenticationManager().authenticate 方法进行认证,这就进入到前面所说的 ProviderManager 的流程中了,可以参考前面的流程说明。
以上就是整个认证流程。