1. 程式人生 > 實用技巧 >認證和授權學習5:spring security認證原理分析

認證和授權學習5:spring security認證原理分析

認證和授權學習5:spring security認證原理分析

目錄

一、結構總覽

spring security通過Filter技術實現對web資源的保護。

當初始化spring security時會建立一個名為SpringSecurityFilterChain 的servlet,型別是

org.springframework.security.web.FilterChainProxy ,它實現了Filter介面,所以所有的請求都會經過它。

FilterChainProxy是一個代理,真正起作用的是FilterChainProxy中包含的各個過濾器,這些過濾器被spring容器管理,spring security通過這些過濾器實現認證和授權。但這些過濾器不直接處理認證和授權,而是呼叫認證管理器和決策管理器來實現。

總結一下就是spring security功能的實現是通過一些過濾器和認證管理器跟決策管理器相互配合實現的。

二、幾個重點的過濾器介紹

2.1 SecurityContextPersistenceFilter

這個Filter是整個攔截過程的入口和出口

2.2 UsernamePasswordAuthenticationFilter

用於處理來自表單提交的認證,其內部還有登入成功和登入失敗的處理器,AuthenticationSuccessHandler ,

AuthenticationFailureHandler

2.3 FilterSecurityInterceptor

用來進行授權的過濾器,通過AccessDecisionManager 對登入使用者進行授權訪問

當用戶訪問一個系統資源時,會進入這個過濾器,在這個過濾器中用決策過濾器判斷當前使用者是否可以訪問此資源。

2.4 ExceptionTranslationFilter

用來處理認證和授權過程中丟擲的異常,能夠捕獲來自過濾器鏈的所有異常,但它只會處理AuthenticationException和AccessDeniedException這兩類,其餘的會繼續向上丟擲。

三、表單登入認證過程分析

(1) 使用者提交使用者名稱和密碼被UsernamePasswordAuthenticationFilter攔截到,將使用者資訊封裝為介面Authentication 物件,實際是UsernamePasswordAuthenticationToken這個實現類的物件

(2) 將Authentication 物件提交到認證管理器AuthenticationManager 進行認證

(3) 認證成功後AuthenticationManager 會返回一個填充了使用者資訊的Authentication物件。

(4) SecurityContextHolder 將設定了使用者資訊的Authentication物件設定到其內部。

3.1Authentication 介面原始碼分析

下面是封裝使用者資訊的Authentication 介面的結構,裡邊維護了當前使用者的身份資訊

public interface Authentication extends Principal, Serializable {
	//許可權列表
	Collection<? extends GrantedAuthority> getAuthorities();
	//登入憑據,比如密碼,登入成功後會被移除
	Object getCredentials();
    //登入的細節資訊,通常是WebAuthenticationDetails
    //記錄登入者的ip和sessionId等值
	Object getDetails();

	//身份資訊,UserDetails介面的實現類物件
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

3.2 UsernamePasswordAuthenticationFilter原始碼分析

UsernamePasswordAuthenticationFilter這個過濾器用來處理表單登入請求,其繼承自抽象類

AbstractAuthenticationProcessingFilter並實現了父類的抽象方法attemptAuthentication()登入請求過來後,實際是先走了這個filter的doFilter方法,

AbstractAuthenticationProcessingFilter的部分原始碼

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
    
    ...//省略
    
    //過濾器的doFilter方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
		    //這裡呼叫子類 UsernamePasswordAuthenticationFilter 的認證方法
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
         //認證成功後的處理邏輯
		successfulAuthentication(request, response, chain, authResult);
	}
}

AbstractAuthenticationProcessingFilter 的doFilter方法中呼叫了子類的 attemptAuthentication方法進行認證

在這個方法中獲取當前登入的使用者資訊,封裝成Authentication 介面的實現類

UsernamePasswordAuthenticationToken的物件,然後把這個物件提交給認證管理器進行認證,這裡說的提交實際上就是呼叫認證管理器的認證方法。

public class UsernamePasswordAuthenticationFilter extends
    AbstractAuthenticationProcessingFilter {
    //這是開始認證的方法
    public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		setDetails(request, authRequest);
        //提交認證管理器(實際就是呼叫認證管理器的認證方法)
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

其中 this.getAuthenticationManager().authenticate(authRequest)這句是呼叫認證管理器的認證方法,如果認證成功會返回一個填充了使用者的許可權等資訊的Authentication 物件,但其中的使用者密碼會被清空

3.3 AuthenticationManager 分析

上面講到UsernamePasswordAuthenticationFilter中會呼叫認證管理器AuthenticationManager的方法進行認證。AuthenticationManager是一個介面,其中只有一個方法,

Authentication authenticate(Authentication authentication) throws AuthenticationException;

springsecurity 提供了一個實現類ProviderManager,所以進行認證時實際呼叫的是這個類中的方法

這個類的名字可以看出它的功能,Provider管理,其實真正的認證邏輯是由各種AuthenticationProvider完成的,

這也是一個介面,不同的實現類對應不同的認證方式

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

其中的support方法用來判斷當前傳遞過來的這個Authentication物件是不是自己該驗證的,authenticate方法是具體的驗證方法。

ProviderManager中維護著一個AuthenticationProvider的集合

private List<AuthenticationProvider> providers = Collections.emptyList();

UsernamePasswordAuthenticationFilter中呼叫的authenticate方法,實際就是ProviderManager中的方法

3.4 ProviderManager原始碼分析

ProviderManager類會呼叫具體的provider進行認證。外部呼叫的是其中的authenticate方法

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
    
    ...//省略變數定義
    public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
        //獲取當前被認證的Authentication的class
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
        //迴圈其中維護的provider列表,看那個可以認證當前的authentication物件
		for (AuthenticationProvider provider : getProviders()) {
		    //呼叫provider.supports()方法判斷這個provider能否處理
            if (!provider.supports(toTest)) {
				//不能處理迴圈下一個
                continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
                //可以處理的話就呼叫具體provider的authenticate方法,
                //如果是表單登入,對應的是  DaoAuthenticationProvider
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}
        //只有沒有進行認證 result才是null,認證失敗會丟擲異常,認證成功result返回authenticaion的物件
		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
                 //如果迴圈完了provider列表還沒有進行認證,就呼叫parent的方法,這個parent是構造方法傳
                 //進來的
				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 than 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 than 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;
	}
}

針對表單登入,最後進行認證的是 DaoAuthenticationProvider,這個類繼承自AbstractUserDetailsAuthenticationProvider,其中authenticate方法在父類中,通過使用者名稱查詢使用者的邏輯定義在父類中,是一個抽象方法protected abstract UserDetails retrieveUser(...),子類實現這個方法,呼叫

userDetailsService去資料庫查詢使用者物件,然後和過濾器傳過來的authentication物件進行比較。

三、總結

整個認證過程:

登入請求先走到AbstractAuthenticationProcessingFilter 的doFilter方法,在這個方法中呼叫了子類

UsernamePasswordAuthenticationFilterattemptAuthentication()方法,然後在這個方法中封裝使用者資訊為

UsernamePasswordAuthenticationToken物件,呼叫認證管理器AuthenticationManager.authenticate()方法進行校驗,校驗成功後再回到AbstractAuthenticationProcessingFilter的doFilter方法中,執行登入成功後的處理邏輯,登入成功後會把當前使用者填充到 SecurityContextHolder