1. 程式人生 > 實用技巧 >Spring Security認證流程分析--練氣後期

Spring Security認證流程分析--練氣後期

寫在前面

在前一篇文章中,我們介紹瞭如何配置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);
				});
	}
		

這兩個獲取方式的邏輯都比較簡單,相信大家能看的明白。

稍微總結一下:

  1. UsernamePasswordAuthenticationFilter攔截到使用者填寫的表單資訊後,先進行校參處理(判斷請求是否為POST請求,將null值轉為空字串),然後將引數封裝成UsernamePasswordAuthenticationToken(這是一個Authentication實現類AbstractAuthenticationToken的子類)物件,再然後呼叫AuthenticationManager物件的實現類ProviderManager的authenticate方法進行認證操作;

  2. ProviderManager在接收到token後,先根據token的className比對spring security內建的認證方式,找到後呼叫AuthenticationProvider的實現類AbstractUserDetailsAuthenticationProvider的authenticate方法進行認證操作

  3. AbstractUserDetailsAuthenticationProvider物件在收到Authentication物件後,先確定使用者名稱,再根據使用者名稱從快取裡查詢使用者資訊,找不到則呼叫retrieveUser方法在持久層查詢資料(持久層資料可以是文字、資料庫裡的資料)。在spring security中,只有DaoAuthenticationProvider實現了這個方法(目前為止)。這時DaoAuthenticationProvider便呼叫UserDetailsService的loadUserByUsername方法找到userDetails。在通過了一系列的判斷驗證後,呼叫createSuccessAuthentication方法給授權,並將其(UsernamePasswordAuthenticationToken)返回給了AuthenticationManager的實現類ProviderManager。

  4. ProviderManager在收到UsernamePasswordAuthenticationToken物件後,先進行引數校驗(判空,判null),之後呼叫事件釋出者eventPublisher的publishAuthenticationSuccess方法將驗證結果釋出出去。最後將結果返回給UsernamePasswordAuthenticationFilter。至此驗證流程大體上就結束了.

也就述說,UsernamePasswordAuthenticationFilter負責攔截,AuthenticationManager負責組織流程,真正執行操作的是認證AuthenticationProvider的子類AbstractUserDetailsAuthenticationProvider物件。

End

給大家畫了一張簡化版的認證時序圖