spring security OAuth實踐

@[TOC](spring security OAuth實踐(未完待續))

參考文章

核心filter

spring security維護了一個FilterChainProxy,這個類會依次調用spring security過濾器鏈,默認的過濾器鏈是11個,加上自定義的(MyusernamePasswordAuthentication是自定義過濾器,用於代替默認的UsernamePasswordAuthentication)一個是十二個,如圖,調用順序從0-12
在這裏插入圖片描述

SecurityContextPersistenceFilter

整個Spring Security 過濾器鏈的開端,它有兩個作用:一是當請求到來時,檢查Session中是否存在SecurityContext,如果不存在,就創建一個新的SecurityContext。二是請求結束時將SecurityContext放入 session中,並清空 SecurityContextHolder

UsernamePasswordAuthenticationFilter

繼承自抽象類 AbstractAuthenticationProcessingFilter,當進行表單登錄時,該Filter將用戶名和密碼封裝成一個 UsernamePasswordAuthentication進行驗證。
改filter主要校驗表單參數,之後封裝成一個 UsernamePasswordAuthentication,如果需要修改spring security自動生成的表單元素(如添加一個驗證碼參數),需要自定義一個UsernamePasswordAuthenticationFilter,在過濾器配置自定義filter以替換UsernamePasswordAuthenticationFilter實現表單校驗構造UsernamePasswordAuthentication

AnonymousAuthenticationFilter

匿名身份過濾器,當前面的Filter認證後依然沒有用戶信息時,該Filter會生成一個匿名身份——AnonymousAuthenticationToken。一般的作用是用於匿名登錄。

ExceptionTranslationFilter

異常轉換過濾器,用於處理 FilterSecurityInterceptor拋出的異常。
過濾器鏈經過此filter時會進行try…catch…,如果後面的filter拋出異常,會在此處捕獲並處理(如後面的FilterSecurityInterceptor投票後拋出SpringSecurityException)

FilterSecurityInterceptor

過濾器鏈最後的關卡,從 SecurityContextHolder中獲取 Authentication,比對用戶擁有的權限和所訪問資源需要的權限。

認證過程

在這裏插入圖片描述
認證過程之後再補充
如果需要自定義認證過程需要自定義LoginAuthenticationProvide實現AuthenticationProvider接口(AuthenticationProvider默認的實現是DaoAuthenticationProvider)
重寫authenticatesupports方法
authenticate方法參考DaoAuthenticationProvider類的authenticate方法,認證成功返回一個UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities())對象,認證失敗拋出AuthenticationException異常,AuthenticationException異常是一個spring security的抽象異常,它有很多子類實現,其中之一就是UsernameNotFoundExceptionBadCredentialsException


讀取保存在session中的認證信息


SecurityContextPersistenceFilter

當我們填寫表單完畢後,點擊登錄按鈕,請求先經過 SecurityContextPersistenceFilter 過濾器,在前面就曾提到,該Filter有兩個作用,其中之一就是在請求到來時,創建 SecurityContext安全上下文,我們來看看它內部是如何做的,部分源碼如下:

public class SecurityContextPersistenceFilter extends GenericFilterBean {

	static final String FILTER_APPLIED = "__spring_security_scpf_applied";
	/**安全上下文存儲的倉庫*/
	private SecurityContextRepository repo;

	private boolean forceEagerSessionCreation = false;
	/**使用HttpSession來存儲 SecurityContext*/
	public SecurityContextPersistenceFilter() {
		this(new HttpSessionSecurityContextRepository());
	}

	public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
		this.repo = repo;
	}

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		/**如果是第一次請求,request中肯定沒有FILTER_APPLIED屬性*/
		if (request.getAttribute(FILTER_APPLIED) != null) {
			// ensure that filter is only applied once per request
			/** 確保每個請求只應用一次過濾器*/
			chain.doFilter(request, response);
			return;
		}

		final boolean debug = logger.isDebugEnabled();
		/**
		* 在request 設置 FILTER_APPLIED 屬性爲 true,
		* 這樣同一個請求再次訪問時,就直接進入後續Filter的操作
		*/
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

		if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
		/**
		* 封裝 requset 和 response 
		*/
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		/**
		*  從存儲安全上下文的倉庫中載入 SecurityContext 安全上下文,
		*  其內部是從 Session中獲取上下文信息
		*/
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			/**
			* 安全上下文信息設置到 SecurityContextHolder 中,
			* 以便在同一個線程中,後續訪問 SecurityContextHolder 
			* 能獲取到 SecuritContext*/
		    SecurityContextHolder.setContext(contextBeforeChainExecution);

			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			/**
			* 請求結束後,清空安全上下文信息
			*/
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			/**
			* 將安全上下文信息存儲到 Session中,相當於登錄態的維護
			*/
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}

	public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
		this.forceEagerSessionCreation = forceEagerSessionCreation;
	}
}

請求到來時,利用HttpSessionSecurityContextRepository讀取安全上下文。我們這裏是第一次請求,讀取的安全上下文中是沒有 Authentication身份信息的,將安全上下文設置到 SecurityContextHolder之後,進入下一個過濾器。

請求結束時,同樣利用HttpSessionSecurityContextRepository該存儲安全上下文的倉庫將認證後的SecurityContext放入 session中,這也是登錄態維護的關鍵,具體調用的是 SecurityContextRepositorysaveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());

HttpSessionSecurityContextRepository

HttpSessionSecurityContextRepositorysave()方法

		@Override
		protected void saveContext(SecurityContext context) {
			/**從上下文中獲取到認證信息*/
			final Authentication authentication = context.getAuthentication();
			HttpSession httpSession = request.getSession(false);

			// See SEC-776
			/**認證信息爲空或者是匿名用戶*/
			if (authentication == null || trustResolver.isAnonymous(authentication)) {
				if (logger.isDebugEnabled()) {
					logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
				}

				if (httpSession != null && authBeforeExecution != null) {
					// SEC-1587 A non-anonymous context may still be in the session
					// SEC-1735 remove if the contextBeforeExecution was not anonymous
					httpSession.removeAttribute(springSecurityContextKey);
				}
				return;
			}

			if (httpSession == null) {
				httpSession = createNewSessionIfAllowed(context);
			}

			// If HttpSession exists, store current SecurityContext but only if it has
			// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
			if (httpSession != null) {
				// We may have a new session, so check also whether the context attribute
				// is set SEC-1561
				if (contextChanged(context)
						|| httpSession.getAttribute(springSecurityContextKey) == null) {
					/**設置上下文信息到session的attribute中,保存了認證狀態*/	
					httpSession.setAttribute(springSecurityContextKey, context);

					if (logger.isDebugEnabled()) {
						logger.debug("SecurityContext '" + context
								+ "' stored to HttpSession: '" + httpSession);
					}
				}
			}
		}
SecurityContext

SecurityContext是一個接口,有兩個方法

  • 獲取認證信息和設置認證信息

在這裏插入圖片描述

UsernamePasswordAuthenticationFilter 的父類是 AbstractAuthenticationProcessingFilter,首先進入父類的 doFilter方法,部分源碼如下:

該doFilter方法中一個核心就是調用子類 UsernamePasswordAuthenticationFilterattemptAuthentication方法,該方法進入真正的認證過程,並返回認證後的 Authentication,該方法的源碼如下:

該方法中有一個關鍵點就是 this.getAuthenticationManager().authenticate(authRequest),調用內部的AuthenticationManager去認證,在之前的文章就介紹過AuthenticationManager,它是身份認證的核心接口,它的實現類是 ProviderManager,而 ProviderManager又將請求委託給一個 AuthenticationProvider列表,列表中的每一個 AuthenticationProvider將會被依次查詢是否需要通過其進行驗證,每個 provider的驗證結果只有兩個情況:拋出一個異常或者完全填充一個 Authentication對象的所有屬性

下面來分析一個關鍵的 AuthenticationProvider,它就是 DaoAuthenticationProvider,它是框架最早的provider,也是最最常用的 provider。大多數情況下我們會依靠它來進行身份認證,它的父類是 AbstractUserDetailsAuthenticationProvider ,認證過程首先會調用父類的 authenticate方法,核心源碼如下:

從上面一大串源碼中,提取幾個關鍵的方法:

retrieveUser(…): 調用子類 DaoAuthenticationProviderretrieveUser()方法獲取 UserDetails
preAuthenticationChecks.check(user): 對從上面獲取的UserDetails進行預檢查,即判斷用戶是否鎖定,是否可用以及用戶是否過期
additionalAuthenticationChecks(user,authentication): 對UserDetails附加的檢查,對傳入的Authentication與獲取的UserDetails進行密碼匹配
postAuthenticationChecks.check(user): 對UserDetails進行後檢查,即檢查UserDetails的密碼是否過期
createSuccessAuthentication(principalToReturn, authentication, user): 上面所有檢查成功後,利用傳入的Authentication 和獲取的UserDetails生成一個成功驗證的Authentication

如何自定義登錄security

構造一個org.springframework.security.core.userdetails.User( user.getAccountID() + "|" + user.getUserName(), user.getPassword(), authorities);對象
構造一個UsernamePasswordAuthenticationToken
通過authenticate(usernamePasswordAuthenticationToken)進行認證,認證之後保存在上下文
SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(authentication);
調用結束SecurityContextPersistenceFilter會將上下文保存在session中,這樣就將自定義的登錄與security的認證兼容了(可以不使用security的認證了)

  UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, password, userDetails.getAuthorities());
        Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(authentication);
還有一種不經過security登錄認證兼容已有登錄

WebSecurityConfigurerAdapter的子類配置中,配置一個自定義filter,插入在
SecurityContextPersistenceFilter前,在這個filter中兼容已有登錄狀態,將認證信息放在session的attribute中,這樣執行到SecurityContextPersistenceFilter時也可以從session中加載到認證信息

 List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username, "", authorities);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, "", userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(usernamePasswordAuthenticationToken);
        request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

http.addFilterBefore(accessTokenFilter, StopSecurityDefaultEndpointFilter.class);

自定義退出登錄

退出登錄源碼參考
security的默認退出登錄邏輯在LogoutFilter

維護了一個LogoutSuccessHandler,這個類決定了退出登錄成功後的重定向,默認是SimpleUrlLogoutSuccessHandler,handler默認是CompositeLogoutHandler,如果想要修改重定向的方式,可以自定義一個LogoutSuccessHandler配置 在 SecurityConfig中配置,http.logout() .logoutSuccessHandler(logoutSuccessHandler)

自定義未認證處理

實現AccessDeniedHandler接口,重寫handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)方法自定義返回值即可

自定義TokenEndPoint異常處理
ProviderManager

ProviderManager
實現WebResponseExceptionTranslator接口,重寫translate(Exception e)方法,

獲取公鑰端點和校驗token端點權限配置
security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()")
這兩個端點是跨域配置登錄後訪問的,如果使用的是自定義登錄的方式,配置security.addTokenEndpointAuthenticationFilter(MyEndpointFilter)
MyEndpointFilter是自己實現的filter,在此filter中兼容自己的登錄方式,把認證信息存儲在session的attribute中

 List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username, "", authorities);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails, "", userDetails.getAuthorities());
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(usernamePasswordAuthenticationToken);
        request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

這樣,這兩個端點檢查認證的時候就可以識別到用戶已經登錄了
如果需要修改未登錄範湖IDE錯誤,可以實現AuthenticationEntryPoint接口,實現commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)方法,配置security.authenticationEntryPoint(MyauthenticationEntryPoint)就可以了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章