Springboot security cas源碼陶冶-CasAuthenticationFilter


Springboot security cas整合方案中不可或缺的校驗Filter類或者稱為認證Filter類,其內部包含校驗器、權限獲取等,特開辟新地啃啃

繼承結構

    - AbstractAuthenticationProcessingFilter
        - CasAuthenticationFilter

其中父類AbstractAuthenticationProcessingFilter#doFilter()是模板處理邏輯方法,而子類主要實現了校驗方法CasAuthenticationFilter#attemptAuthentication()方法。下面就對這兩塊進行代碼層面的分析

AbstractAuthenticationProcessingFilter#doFilter-處理邏輯

	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		//是否需要驗證,這里cas子類對其進行了復寫
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}
		//憑證信息
		Authentication authResult;

		try {
			//調用子類來進行相關的驗證操作,供子類復寫
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				//返回為空,則校驗停止
				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
		//認證成功后是否還往下走,默認為false
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//直接跳轉至配置好的登錄成功頁面,這里cas子類對其進行了復寫
		successfulAuthentication(request, response, chain, authResult);
	}

其中CasAuthenticationFilter對以下方法進行了復寫,分別為requiresAuthentication()successfulAuthentication()attemptAuthentication()方法

CasAuthenticationFilter#requiresAuthentication-是否校驗判斷

   protected boolean requiresAuthentication(final HttpServletRequest request,
			final HttpServletResponse response) {
		//是否與設置的登錄路徑匹配
		final boolean serviceTicketRequest = serviceTicketRequest(request, response);
		//對含有ticket參數的請求會返回true
		final boolean result = serviceTicketRequest || proxyReceptorRequest(request)
				|| (proxyTicketRequest(serviceTicketRequest, request));
		if (logger.isDebugEnabled()) {
			logger.debug("requiresAuthentication = " + result);
		}
		return result;
	}

對login請求以及token請求則返回true表示需要驗證

CasAuthenticationFilter#attemptAuthentication-具體校驗處理

        @Override
	    public Authentication attemptAuthentication(final HttpServletRequest request,
			final HttpServletResponse response) throws AuthenticationException,
			IOException {
		// if the request is a proxy request process it and return null to indicate the
		// request has been processed
		//代理服務的請求處理,涉及PGT
		if (proxyReceptorRequest(request)) {
			logger.debug("Responding to proxy receptor request");
			//直接響應輸出
			CommonUtils.readAndRespondToProxyReceptorRequest(request, response,
					this.proxyGrantingTicketStorage);
			return null;
		}
		//判斷是否對應指定的請求(login請求),支持ant-style方式
		final boolean serviceTicketRequest = serviceTicketRequest(request, response);
        //login請求為"_cas_stateful_",非login請求為"_cas_stateless_"
		final String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER
				: CAS_STATELESS_IDENTIFIER;
		//獲取ticket
		String password = obtainArtifact(request);
		
		//passwprd一般不可為空,這在provider處理類中會拋異常
		if (password == null) {
			logger.debug("Failed to obtain an artifact (cas ticket)");
			password = "";
		}

		final UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
		//通過CasAuthenticationProvider來進行具體的校驗,包含ticket驗證以及當前用戶權限集合獲取
		return this.getAuthenticationManager().authenticate(authRequest);
	    }

具體的校驗通過CasAuthenticationProvider來實現

CasAuthenticationProvider-cas校驗器

cas校驗器,看下主要的實現方法

  • CasAuthenticationProvider#afterPropertiesSet()
    主要是檢驗必須的屬性是否設置
    	public void afterPropertiesSet() throws Exception {
    	//權限獲取處理對象
    	Assert.notNull(this.authenticationUserDetailsService,
    			"An authenticationUserDetailsService must be set");
    	//ticket校驗器
    	Assert.notNull(this.ticketValidator, "A ticketValidator must be set");
    	//stateless對應的緩存,默認為NullStatelessTicketCache
    	Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
    	//必須設置key
    	Assert.hasText(
    			this.key,
    			"A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
    	//默認為SpringSecurityMessageSource.getAccessor()
    	Assert.notNull(this.messages, "A message source must be set");
    }
    
  • CasAuthenticationProvider#authenticate
    校驗處理方法,源碼如下
    	//此處傳過來的authentication類型為UsernamePasswordAuthenticationToken
    	public Authentication authenticate(Authentication authentication)
    		throws AuthenticationException {
    	//此處為true
    	if (!supports(authentication.getClass())) {
    		return null;
    	}
    
    	if (authentication instanceof UsernamePasswordAuthenticationToken
    			&& (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
    					.equals(authentication.getPrincipal().toString()) && !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
    					.equals(authentication.getPrincipal().toString()))) {
    		// UsernamePasswordAuthenticationToken not CAS related
    		return null;
    	}
    
    	// If an existing CasAuthenticationToken, just check we created it
    	if (authentication instanceof CasAuthenticationToken) {
    		if (this.key.hashCode() == ((CasAuthenticationToken) authentication)
    				.getKeyHash()) {
    			return authentication;
    		}
    		else {
    			throw new BadCredentialsException(
    					messages.getMessage("CasAuthenticationProvider.incorrectKey",
    							"The presented CasAuthenticationToken does not contain the expected key"));
    		}
    	}
    
    	// Ensure credentials are presented,確保ticket不為空,否則將拋出異常
    	if ((authentication.getCredentials() == null)
    			|| "".equals(authentication.getCredentials())) {
    		throw new BadCredentialsException(messages.getMessage(
    				"CasAuthenticationProvider.noServiceTicket",
    				"Failed to provide a CAS service ticket to validate"));
    	}
    
    	boolean stateless = false;
    
    	if (authentication instanceof UsernamePasswordAuthenticationToken
    			&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication
    					.getPrincipal())) {
    		stateless = true;
    	}
    
    	CasAuthenticationToken result = null;
    	//對非login請求的嘗試從緩存中獲取
    	if (stateless) {
    		// Try to obtain from cache
    		result = statelessTicketCache.getByTicketId(authentication.getCredentials()
    				.toString());
    	}
    
    	if (result == null) {
    		//第一次校驗則用ticketValidator去cas服務端進行ticket校驗
    		result = this.authenticateNow(authentication);
    		result.setDetails(authentication.getDetails());
    	}
    	//對非login請求的castoken進行緩存
    	if (stateless) {
    		// Add to cache
    		statelessTicketCache.putTicketInCache(result);
    	}
    
    	return result;
    }
    
  • CasAuthenticationProvider#authenticateNow
    實際的校驗處理方法,源碼如下
    	private CasAuthenticationToken authenticateNow(final Authentication authentication)
    		throws AuthenticationException {
    	try {
    		//TicketValidator一般只需要設置casServerUrlPrefix前綴,實際的請求全路徑如下,以Cas20ServiceTicketValidator為例
    		//https://example.casserver.com/cas/serviceValidator?service=https://example.casclient.com/
    		final Assertion assertion = this.ticketValidator.validate(authentication
    				.getCredentials().toString(), getServiceUrl(authentication));
    		//調用authenticationUserDetailsService獲取當前用戶所擁有的權限
    		final UserDetails userDetails = loadUserByAssertion(assertion);
    		userDetailsChecker.check(userDetails);
    		//組裝成CasAuthenticationToken來保存校驗信息,供保存至spring的安全上下文中
    		return new CasAuthenticationToken(this.key, userDetails,
    				authentication.getCredentials(),
    				authoritiesMapper.mapAuthorities(userDetails.getAuthorities()),
    				userDetails, assertion);
    	}
    	catch (final TicketValidationException e) {
    		//ticket校驗失敗則拋出異常,此異常會被父類獲取而調用failerhandler將錯誤寫向頁面
    		throw new BadCredentialsException(e.getMessage(), e);
    	}
    }
    
  1. CasAuthenticationProvider的必要屬性含義
  • authenticationUserDetailsService-權限獲取對象
  • ticketValidator-ticket校驗器,其中需要設置cas服務端的校驗地址前綴casServerUrlPrefix
  • key-設置唯一標識
  1. CasAuthenticationProvider校驗過程中如果ticket為空或者ticket校驗失敗都會由AbstractAuthenticationProcessingFilter類抓取並將錯誤信息寫入到頁面中,從而關於ticket的異常信息都會顯示至前端頁面
  2. CasAuthenticationProvider校驗成功后會生成CasAuthenticationToken,且設置authenticatedtrue並保存至spring的安全上下文中,這在FilterSecurityInterceptorFilter類會有所作用

CasAuthenticationFilter#successfulAuthentication-校驗成功處理

    	protected final void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
		//如果請求含有ticket參數,返回true
		//login請求則直接返回false從而調用父類的successfulAuthentication()來直接響應頁面
		boolean continueFilterChain = proxyTicketRequest(
				serviceTicketRequest(request, response), request);
		if (!continueFilterChain) {
			super.successfulAuthentication(request, response, chain, authResult);
			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//保存Authentication憑證信息		
               SecurityContextHolder.getContext().setAuthentication(authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//往下繼續走
		chain.doFilter(request, response);
	}

小結

  1. CasAuthenticationFilter的放行策略:非登錄請求;非代理接收請求;非ticket請求

  2. 對登錄請求的成功處理是直接跳轉至指定的頁面,可通過SimpleUrlAuthenticationSuccessHandler#setDefaultTargetUrl(String url)設置;
    對非登錄請求比如token請求的操作將保存校驗通過的Authentication對象至SecurityContextHolder.getContext()上下文中再放行

  3. CasAuthenticationProvider校驗過程中如果ticket為空或者ticket校驗失敗都會由AbstractAuthenticationProcessingFilter類抓取並將錯誤信息寫入到頁面中,從而關於ticket的異常信息都會顯示至前端頁面
    溫馨提示:cas服務端登錄成功后的service路徑不要為login請求,避免token沒拿到就被攔截從而輸出錯誤頁面

  4. 其中對ticket進行校驗的是CasAuthenticationProvider對象,包括ticket校驗以及權限獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM