寫在前面
在前一篇文章中,我們介紹了如何配置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
給大家畫了一張簡化版的認證時序圖