天天看點

spring-security安全架構解讀1、導入依賴2、springsecurity的過濾器鍊3、使用者認證流程

SpringSecurity安全架構學習

  • 1、導入依賴
  • 2、springsecurity的過濾器鍊
  • 3、使用者認證流程
    • 3.1 AbstractAuthenticationProcessingFilter過濾器
      • AbstractAuthenticationProcessingFilter源碼分析:
    • 3.1 UsernamePasswordAuthenticationFilter過濾器(springsecurity認證授權的核心過濾器)
      • UsernamePasswordAuthenticationFilter源碼分析

1、導入依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
           

2、springsecurity的過濾器鍊

spring-security安全架構解讀1、導入依賴2、springsecurity的過濾器鍊3、使用者認證流程

對于以上的每個過濾器的作用,可參看spring security的官方文檔spring security的官方文檔進行學習。

3、使用者認證流程

3.1 AbstractAuthenticationProcessingFilter過濾器

package org.sicFilterBean;
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	......
	//使用者認證成功後的結果處理
	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	//使用者認證失敗後的結果處理
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
	//對請求路徑進行比對,使用者可在配置類中自定義
	protected AbstractAuthenticationProcessingFilter(
			RequestMatcher requiresAuthenticationRequestMatcher) {
		Assert.notNull(requiresAuthenticationRequestMatcher,
				"requiresAuthenticationRequestMatcher cannot be null");
		this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
	}
	//核心代碼
	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:該對象為Spring Security方式的認證結果主體類
		Authentication authResult;
		try {
		//最為核心的代碼,其認證過程主要是依靠具體的子類實作,包括兩部分,認證(常用的使用者名+密碼,手機号+驗證碼,郵箱+驗證碼),授權(使用者權限的判定)
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			//認證結果放置于session會話中
			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);
	}
	//認證成功後的處理邏輯
	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//将認證結果放置于SecurityContex及安全的上下文環境中
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//記住我功能
		rememberMeServices.loginSuccess(request, response, authResult);
		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//調用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功處理邏輯
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
	//認證失敗後的處理邏輯
	protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}
		//記住我
		rememberMeServices.loginFail(request, response);
		//調用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失敗處理邏輯
		failureHandler.onAuthenticationFailure(request, response, failed);
	}
	//使用認證管理器
	public void setAuthenticationManager(AuthenticationManager authenticationManager) {
		this.authenticationManager = authenticationManager;
	}
	//使用記住我服務	
	public void setRememberMeServices(RememberMeServices rememberMeServices) {
		Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
		this.rememberMeServices = rememberMeServices;
	}
	//session環境的管理	
	public void setSessionAuthenticationStrategy(
			SessionAuthenticationStrategy sessionStrategy) {
		this.sessionStrategy = sessionStrategy;
	}
	//認證成功的處理者
	public void setAuthenticationSuccessHandler(
			AuthenticationSuccessHandler successHandler) {
		Assert.notNull(successHandler, "successHandler cannot be null");
		this.successHandler = successHandler;
	}
	//認證失敗的處理者
	public void setAuthenticationFailureHandler(
			AuthenticationFailureHandler failureHandler) {
		Assert.notNull(failureHandler, "failureHandler cannot be null");
		this.failureHandler = failureHandler;
	}
	
}
           

AbstractAuthenticationProcessingFilter源碼分析:

AbstractAuthenticationProcessingFilter 抽象類中的doFilter方法定義了一個完整的使用者認證授權流程:

  • 對請求的位址進行比對,位址比對則進行相應的認證權限校驗,不比對直接傳回
//進行請求過濾位址的比對
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
           
  • 對請求進行認證及授權處理 ,這一步是由對應的子類實作去處理的,預設實作是UsernamePasswordAuthenticationFilter
  • 将認證結果放置于session會話中
//認證結果放置于session會話中
	sessionStrategy.onAuthentication(authResult, request, response);
           
  • 如果在調用認證授權過程中出現任何異常,則會調用相應的認證失敗的業務邏輯,unsuccessfulAuthentication,并且預設調用SimpleUrlAuthenticationFailureHandler接口對象的onAuthenticationFailure方法。
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;
		}
===================================================================================================================================

//InternalAuthenticationServiceException  異常,屬于網絡連接配接的異常資訊,主要是用于 OAuth 認證伺服器與業務伺服器通信異常

//AuthenticationException 主要是指認證權限不通過的異常資訊,springsecurity架構已經有部分實作,可通過繼承關系進行檢視,是以在我們代碼過程中我們可以直接使用

===================================================================================================================================
protected void unsuccessfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException failed)
			throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication request failed: " + failed.toString(), failed);
			logger.debug("Updated SecurityContextHolder to contain null Authentication");
			logger.debug("Delegating to authentication failure handler " + failureHandler);
		}
		//取消記住我
		rememberMeServices.loginFail(request, response);
		//調用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失敗處理邏輯
		failureHandler.onAuthenticationFailure(request, response, failed);
	}
===========================================================================================================================
public void onAuthenticationFailure(HttpServletRequest request,
			HttpServletResponse response, AuthenticationException exception)
			throws IOException, ServletException {
		if (defaultFailureUrl == null) {
			logger.debug("No failure URL set, sending 401 Unauthorized error");
			response.sendError(HttpStatus.UNAUTHORIZED.value(),
				HttpStatus.UNAUTHORIZED.getReasonPhrase());
		}
		else {
			saveException(request, exception);
			if (forwardToDestination) {
				logger.debug("Forwarding to " + defaultFailureUrl);
				request.getRequestDispatcher(defaultFailureUrl)
						.forward(request, response);
			}
			else {
				logger.debug("Redirecting to " + defaultFailureUrl);
				redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
			}
		}
	}
           
  • 如果在調用認證授權成功,則會調用相應的認證成功的業務邏輯,successfulAuthentication*,并且預設調用SavedRequestAwareAuthenticationSuccessHandler接口對象的onAuthenticationSuccess方法,是以使用者可以自定義認證成功的處理結果業務邏輯,實作SavedRequestAwareAuthenticationSuccessHandler即可。
//認證成功後的處理邏輯
	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//将認證結果放置于SecurityContex及安全的上下文環境中
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//記住我功能
		rememberMeServices.loginSuccess(request, response, authResult);
		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//調用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功處理邏輯
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
==========================================================================================================================
//将本次的request請求從會話緩存中取出
private RequestCache requestCache = new HttpSessionRequestCache();
public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		clearAuthenticationAttributes(request);
		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
           

3.1 UsernamePasswordAuthenticationFilter過濾器(springsecurity認證授權的核心過濾器)

package org.springframework.security.web.authentication;

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
	// 預設頁面表單取值為username和password,
	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;
	// 預設的登入頁面請求位址為login,post請求
	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}
	//核心方法:使用者名密碼的請求認證
	// ========================================================================================================
	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);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}
	//從頁面參數中擷取使用者密碼
	protected String obtainPassword(HttpServletRequest request) {
		return request.getParameter(passwordParameter);
	}
	//從頁面參數中擷取使用者名
	protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
	//設定使用者的token資訊
	protected void setDetails(HttpServletRequest request,
			UsernamePasswordAuthenticationToken authRequest) {
		authRequest.setDetails(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;
	}
	//對于http請求參數的自定義
	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}
}
           

UsernamePasswordAuthenticationFilter源碼分析

  • 對于使用者認證和權限的校驗 ,該核心過濾器給定了請求參數的預設值,如url="/login",使用者名和使用者密碼的取值參數,http的請求方式等,但都給使用者自定義的空間。
  • 核心代碼:
username = username.trim();
		//根據使用者名和密碼,生産權限認證的主要核心類
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		// 将本次請求的詳細資訊,包括使用者通路協定、路徑以及IP位址等放置在認證主體中供下一級的調用
		setDetails(request, authRequest);
		//真正的權限校驗的方法
		return this.getAuthenticationManager().authenticate(authRequest);
           
  • UsernamePasswordAuthenticationToken的解讀

    看一下他的繼承體系

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
public abstract class AbstractAuthenticationToken implements Authentication,CredentialsContainer 
           

  是以說:Authentication 是一個接口,用來表示使用者認證資訊的,在使用者登入認證之前相關資訊會封裝為一個 Authentication 具體實作類的對象,在登入認證成功之後又會生成一個資訊更全面,包含使用者權限等資訊的 Authentication 對象,然後把它儲存在 SecurityContextHolder 所持有的 SecurityContext 中,供後續的程式進行調用,如通路權限的鑒定等。

  1. 深入了解(預設機制):
//認證開始之前,我們調用的是該構造器,是以預設的principa(需要認證的使用者對象)為我們自己所實作的UserDetils
//而我們自定義的UserDetils實作了,其實主要也就是兩個組成部分,一個是使用者詳情,一個是權限範圍
//credentials(認證憑據)預設給定的就是使用者密碼
//認證狀态給定的是false,未認證
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}
           
  1. 深入了解(自定義):
//我們使用的是
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
   		Collection<? extends GrantedAuthority> authorities) {
   	super(authorities);
   	this.principal = principal;
   	this.credentials = credentials;
   	super.setAuthenticated(true); // must use super, as we override
   }
   //及我們已經通過密碼規則的校驗,現在進行權限的校驗及配置設定
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
                   userDetails.getPassword(), userDetails.getAuthorities());
           
  • AuthenticationManager 和 AuthenticationProvider

  AuthenticationManager 是一個用來處理認證(Authentication)請求的接口。在其中隻定義了一個方法 authenticate(),該方法隻接收一個代表認證請求的 Authentication 對象作為參數,如果認證成功,則會傳回一個封裝了目前使用者權限等資訊的 Authentication 對象進行傳回。

   在 Spring Security 中,AuthenticationManager 的預設實作是 ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的 AuthenticationProvider 清單,然後會依次使用每一個 AuthenticationProvider 進行認證,如果有一個 AuthenticationProvider 認證後的結果不為 null,則表示該 AuthenticationProvider 已經認證成功,之後的 AuthenticationProvider 将不再繼續認證。然後直接以該 AuthenticationProvider 的認證結果作為 ProviderManager 的認證結果。如果所有的 AuthenticationProvider 的認證結果都為 null,則表示認證失敗,将抛出一個 ProviderNotFoundException。校驗認證請求最常用的方法是根據請求的使用者名加載對應的 UserDetails,然後比對 UserDetails 的密碼與認證請求的密碼是否一緻,一緻則表示認證通過。Spring Security 内部的 DaoAuthenticationProvider 就是使用的這種方式。其内部使用 UserDetailsService 來負責加載 UserDetails,在認證成功以後會使用加載的 UserDetails 來封裝要傳回的 Authentication 對象,加載的 UserDetails 對象是包含使用者權限等資訊的。認證成功傳回的 Authentication 對象将會儲存在目前的 SecurityContext 中。

   預設情況下,在認證成功後 ProviderManager 将清除傳回的 Authentication 中的憑證資訊,如密碼。是以如果你在無狀态的應用中将傳回的 Authentication 資訊緩存起來了,那麼以後你再利用緩存的資訊去認證将會失敗,因為它已經不存在密碼這樣的憑證資訊了。是以在使用緩存的時候你應該考慮到這個問題。一種解決辦法是設定 ProviderManager 的 eraseCredentialsAfterAuthentication 屬性為 false,或者想辦法在緩存時将憑證資訊一起緩存。

  • AuthenticationEntryPoint 用來解決匿名使用者通路無權限資源時的異常
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
        response.getWriter().flush();
    }

           
  • AccessDeineHandler 用來解決認證過的使用者通路無權限資源時的異常
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }

           
  • 加入自定義的認證處理
//添加自定義未授權和未登入結果傳回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
           

繼續閱讀