共计 11581 个字符,预计需要花费 29 分钟才能阅读完成。
写在后面
在前一篇文章中,咱们介绍了如何配置 spring security 的自定义认证页面,以及前后端拆散场景下如何获取 spring security 的 CSRF Token。在这一篇文章中咱们未来剖析一下 spring security 的认证流程。
提醒:我应用的 spring security 的版本是 5.3.4.RELEASE。如果读者应用的不是和我同一个版本,源码轻微之处有些不同,然而大体流程都是一样的。
认证流程剖析
通过查阅 spring security 的官网文档咱们晓得,spring security 的认证过滤操作由 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 = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 必须为 POST 申请
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported:" + request.getMethod());
}
//2. 取出用户填写的用户名和明码
String username = obtainUsername(request);
String password = obtainPassword(request);
//3. 防止出现空指针
if (username == null) {username = "";}
if (password == null) {password = "";}
//4. 去掉用户名的空格
username = username.trim();
//5. 在层层校验后,开始对 username 和 password 进行封装
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 6. 认证逻辑
return this.getAuthenticationManager()
.authenticate(authRequest);
}
}
从下面的剖析咱们晓得了,当表单信息进入到这个过滤器之后,通过层层校验,将其封装成 UsernamePasswordAuthenticationToken 对象。接下来咱们进入到这个对象外面看看。
一下是局部源码
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
// 用户名
private final Object principal;
// 明码
private Object credentials;
//5.1 还未认证,走这个构造方法
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
}
AuthenticationManager
在上方第 6 步,进入了认证逻辑,(真正认证操作在 AuthenticationManager 外面)咱们接下来进入到 AuthenticationManager 对象的 authenticate()办法里看看。
发现这是一个接口。从图中能够晓得除了 ProviderManager 这个类之外,其余的都是外部类,所有咱们就间接进入到 ProviderManager 对象的 authenticate 办法里看看
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();
//7. 找到与之对应的认证形式(本零碎账户登录。。微信登录等)for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}
if (debug) {
logger.debug("Authentication attempt using"
+ provider.getClass().getName());
}
//8。调用认证服务提供者的办法进行认证
try {result = provider.authenticate(authentication);
if (result != null) {copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {lastException = e;}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// 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 e) {lastException = parentException = e;}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();}
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {prepareException(lastException, authentication);
}
throw lastException;
}
// spring security 将其所有认证形式都封装成一个 AuthenticationProvider 汇合,第一步便是找出对应的认证形式
public List<AuthenticationProvider> getProviders() {return providers;}
}
AuthenticationProvider
在步骤 8 中,调用了认证提供者的认证办法,接下来咱们进去看看。发现 AuthenticationProvider 是一个接口
咱们从实现类的名称当中猜一个进去看看,就看 AbstractUserDetailsAuthenticationProvider 这个类。
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();
//8.1 尝试从缓存中获取用户
boolean cacheWasUsed = true;
//UserDetails 就是 spring Security 内定义的用户对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
//8.2 如果缓存中不存在用户,则开始检索
try {
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);
}
在步骤 8.2 中,调用了 retrieveUser 办法查找用户,接下来咱们进去看看
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
发现它是一个形象的办法,接下来点进去,看看它曾经提供好的实现办法。这个办法在 DaoAuthenticationProvider 对象中
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {prepareTimingAttackProtection();
try {
//8.2.1 通过用户名加载用户
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);
}
}
通过浏览代码发现,它又调用了 UserDetailsService 对象的 loadUserByUsername(办法去做加载操作,咱们点进去看看
UserDetailsService
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
发现这是一个接口,并且到了这一步就失去了咱们的用户对象 UserDetails。如果说大家要自定义认证信息检索,查找本人定义的 User 对象话就实现这个接口,并且让本人的用户对象实现 UserDetails 接口。并且实现相干查询方法和注册。
接下来咱们看 spring security 曾经提供好的实现类它的实现类
咱们重点关注的有两个,一个是 JdbcDaoImpl,一个是 CachingUserDetailsService。前者从数据库中查问用户,后者从缓存中查问用户信息
咱们先看 CachingUserDetailsService 的源码
public class CachingUserDetailsService implements UserDetailsService {private UserCache userCache = new NullUserCache();
private final UserDetailsService delegate;
public CachingUserDetailsService(UserDetailsService delegate) {this.delegate = delegate;}
public UserCache getUserCache() {return userCache;}
public void setUserCache(UserCache userCache) {this.userCache = userCache;}
public UserDetails loadUserByUsername(String username) {UserDetails user = userCache.getUserFromCache(username);
if (user == null) {user = delegate.loadUserByUsername(username);
}
Assert.notNull(user, () -> "UserDetailsService" + delegate
+ "returned null for username" + username + "."
+ "This is an interface contract violation");
userCache.putUserInCache(user);
return user;
}
}
再看 JdbcDaoImpl(局部)
public class JdbcDaoImpl extends JdbcDaoSupport
implements UserDetailsService, MessageSourceAware {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {List<UserDetails> users = loadUsersByUsername(username);
if (users.size() == 0) {this.logger.debug("Query returned no results for user'" + username + "'");
throw new UsernameNotFoundException(
this.messages.getMessage("JdbcDaoImpl.notFound",
new Object[] { username}, "Username {0} not found"));
}
UserDetails user = users.get(0); // contains no GrantedAuthority[]
Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
if (this.enableAuthorities) {dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
}
if (this.enableGroups) {dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
}
List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
addCustomAuthorities(user.getUsername(), dbAuths);
if (dbAuths.size() == 0) {
this.logger.debug("User'" + username
+ "'has no authorities and will be treated as'not found'");
throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[] {username},
"User {0} has no GrantedAuthority"));
}
return createUserDetails(username, user, dbAuths);
}
protected List<UserDetails> loadUsersByUsername(String username) {return getJdbcTemplate().query(this.usersByUsernameQuery,
new String[] { username}, (rs, rowNum) -> {String username1 = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username1, password, enabled, true, true, true,
AuthorityUtils.NO_AUTHORITIES);
});
}
这两个获取形式的逻辑都比较简单,置信大家能看的明确。
略微总结一下:
- UsernamePasswordAuthenticationFilter拦挡到用户填写的表单信息后,先进行校参解决(判断申请是否为 POST 申请,将 null 值转为空字符串),而后将参数封装成 UsernamePasswordAuthenticationToken(这是一个 Authentication 实现类 AbstractAuthenticationToken 的子类)对象,再而后调用 AuthenticationManager 对象的实现类 ProviderManager 的 authenticate 办法进行认证操作;
- ProviderManager 在接管到 token 后,先依据 token 的 className 比对 spring security 内置的认证形式,找到后调用 AuthenticationProvider 的实现类 AbstractUserDetailsAuthenticationProvider 的 authenticate 办法进行认证操作
- AbstractUserDetailsAuthenticationProvider 对象在收到 Authentication 对象后,先确定用户名,再依据用户名从缓存里查找用户信息,找不到则调用 retrieveUser 办法在长久层查找数据(长久层数据能够是文本、数据库里的数据)。在 spring security 中,只有 DaoAuthenticationProvider 实现了这个办法(目前为止)。这时 DaoAuthenticationProvider 便调用 UserDetailsService 的 loadUserByUsername 办法找到 userDetails。在通过了一系列的判断验证后,调用 createSuccessAuthentication 办法给受权,并将其(UsernamePasswordAuthenticationToken)返回给了 AuthenticationManager 的实现类 ProviderManager。
- ProviderManager 在收到 UsernamePasswordAuthenticationToken 对象后,先进行参数校验(判空,判 null),之后调用事件发布者 eventPublisher 的 publishAuthenticationSuccess 办法将验证后果公布进来。最初将后果返回给 UsernamePasswordAuthenticationFilter。至此验证流程大体上就完结了.
也就述说,UsernamePasswordAuthenticationFilter 负责拦挡,AuthenticationManager 负责组织流程,真正执行操作的是认证 AuthenticationProvider 的子类 AbstractUserDetailsAuthenticationProvider 对象。
End
给大家画了一张简化版的认证时序图