Spring Security受權過程

Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組能夠在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。css

前言

本文是接上一章Spring Security認證過程進一步分析Spring Security用戶名密碼登陸受權是如何實現得;java

類圖

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/security-authentication-Diagram.png

調試過程

使用debug方式啓動https://github.com/longfeizheng/logback該項目,瀏覽器輸入http://localhost:8080/persons,用戶名隨意,密碼123456便可;git

源碼分析

如圖所示,顯示了登陸認證過程當中的 filters 相關的調用流程,做者將幾個自認爲重要的 filters 標註了出來,github

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/security-filers.png

從圖中能夠看出執行的順序。來看看幾個做者認爲比較重要的 Filter 的處理邏輯,UsernamePasswordAuthenticationFilterAnonymousAuthenticationFilterExceptionTranslationFilterFilterSecurityInterceptor 以及相關的處理流程以下所述;express

UsernamePasswordAuthenticationFilter

整個調用流程是,先調用其父類 AbstractAuthenticationProcessingFilter.doFilter() 方法,而後再執行 UsernamePasswordAuthenticationFilter.attemptAuthentication() 方法進行驗證;編程

AbstractAuthenticationProcessingFilter

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

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		#1.判斷當前的filter是否能夠處理當前請求,不能夠的話則交給下一個filter處理
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

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

		Authentication authResult;

		try {
			#2.抽象方法由子類UsernamePasswordAuthenticationFilter實現
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			#2.認證成功後,處理一些與session相關的方法 
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			#3.認證失敗後的的一些操做
			unsuccessfulAuthentication(request, response, failed);

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

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		#3. 認證成功後的相關回調方法 主要將當前的認證放到SecurityContextHolder中
		successfulAuthentication(request, response, chain, authResult);
	}

整個程序的執行流程以下:瀏覽器

  1. 判斷filter是否能夠處理當前的請求,若是不能夠則放行交給下一個filter
  2. 調用抽象方法attemptAuthentication進行驗證,該方法由子類UsernamePasswordAuthenticationFilter實現
  3. 認證成功之後,回調一些與 session 相關的方法;
  4. 認證成功之後,認證成功後的相關回調方法;認證成功之後,認證成功後的相關回調方法;
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);
		}

		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
1. 將當前認證成功的 Authentication 放置到 SecurityContextHolder 中;
2. 將當前認證成功的 Authentication 放置到 SecurityContextHolder 中;
3. 調用其它可擴展的 handlers 繼續處理該認證成功之後的回調事件;(實現`AuthenticationSuccessHandler`接口便可)

UsernamePasswordAuthenticationFilter

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.從request中獲取username和password
		String username = obtainUsername(request);
		String password = obtainPassword(request);

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

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

		username = username.trim();
		#3.構建UsernamePasswordAuthenticationToken(兩個參數的構造方法setAuthenticated(false))
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		#4. 調用 AuthenticationManager 進行驗證(子類ProviderManager遍歷全部的AuthenticationProvider認證)
		return this.getAuthenticationManager().authenticate(authRequest);
	}
  1. 認證請求的方法必須爲POST
  2. 從request中獲取 username 和 password
  3. 封裝Authenticaiton的實現類UsernamePasswordAuthenticationToken,(UsernamePasswordAuthenticationToken調用兩個參數的構造方法setAuthenticated(false))
  4. 調用 AuthenticationManagerauthenticate 方法進行驗證;可參考ProviderManager部分;

AnonymousAuthenticationFilter

從上圖中過濾器的執行順序圖中能夠看出AnonymousAuthenticationFilter過濾器是在UsernamePasswordAuthenticationFilter等過濾器以後,若是它前面的過濾器都沒有認證成功,Spring Security則爲當前的SecurityContextHolder中添加一個Authenticaiton 的匿名實現類AnonymousAuthenticationToken;安全

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		#1.若是前面的過濾器都沒認證經過,則SecurityContextHolder中Authentication爲空
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			#2.爲當前的SecurityContextHolder中添加一個匿名的AnonymousAuthenticationToken
			SecurityContextHolder.getContext().setAuthentication(
					createAuthentication((HttpServletRequest) req));

			if (logger.isDebugEnabled()) {
				logger.debug("Populated SecurityContextHolder with anonymous token: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
		}

		chain.doFilter(req, res);
	}

	#3.建立匿名的AnonymousAuthenticationToken
	protected Authentication createAuthentication(HttpServletRequest request) {
		AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
				principal, authorities);
		auth.setDetails(authenticationDetailsSource.buildDetails(request));

		return auth;
	}
	
		/**
	 * Creates a filter with a principal named "anonymousUser" and the single authority
	 * "ROLE_ANONYMOUS".
	 *
	 * @param key the key to identify tokens created by this filter
	 */
	 ##.建立一個用戶名爲anonymousUser 受權爲ROLE_ANONYMOUS
	public AnonymousAuthenticationFilter(String key) {
		this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
	}
  1. 判斷SecurityContextHolder中Authentication爲否爲空;
  2. 若是空則爲當前的SecurityContextHolder中添加一個匿名的AnonymousAuthenticationToken(用戶名爲 anonymousUser 的AnonymousAuthenticationToken

ExceptionTranslationFilter

ExceptionTranslationFilter 異常處理過濾器,該過濾器用來處理在系統認證受權過程當中拋出的異常(也就是下一個過濾器FilterSecurityInterceptor),主要是 處理 AuthenticationExceptionAccessDeniedExceptionsession

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			#.判斷是否是AuthenticationException
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

			if (ase == null) {
				#. 判斷是否是AccessDeniedException
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);
			}

			if (ase != null) {
				handleSpringSecurityException(request, response, chain, ase);
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}

FilterSecurityInterceptor

此過濾器爲認證受權過濾器鏈中最後一個過濾器,該過濾器以後就是請求真正的/persons 服務app

public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
			#1. before invocation重要
			InterceptorStatusToken token = super.beforeInvocation(fi);

			try {
				#2. 能夠理解開始請求真正的 /persons 服務
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}
			#3. after Invocation
			super.afterInvocation(token, null);
		}
	}
  1. before invocation重要
  2. 請求真正的 /persons 服務
  3. after Invocation

三個部分中,最重要的是 #1,該過程當中會調用 AccessDecisionManager 來驗證當前已認證成功的用戶是否有權限訪問該資源;

before invocation: AccessDecisionManager

protected InterceptorStatusToken beforeInvocation(Object object) {
		...

		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		...
		Authentication authenticated = authenticateIfRequired();

		// Attempt authorization
		try {
			#1.重點
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));

			throw accessDeniedException;
		}

		...
	}

authenticated就是當前認證的Authentication,那麼objectattributes又是什麼呢?

attributes和object 是什麼?

Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

調試 http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/security-authenticated.png

咱們發現object爲當前請求的 url:/persons, 那麼getAttributes方法就是使用當前的訪問資源路徑去匹配咱們本身定義的匹配規則。

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()//使用表單登陸,再也不使用默認httpBasic方式
                .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)//若是請求的URL須要認證則跳轉的URL
                .loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)//處理表單中自定義的登陸URL
                .and()
                .authorizeRequests().antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM,
                SecurityConstants.DEFAULT_REGISTER_URL,
                "/**/*.js",
                "/**/*.css",
                "/**/*.jpg",
                "/**/*.png",
                "/**/*.woff2")
                .permitAll()//以上的請求都不須要認證
                .anyRequest()//剩下的請求
                .authenticated()//都須要認證
                .and()
                .csrf().disable()//關閉csrd攔截
        ;
    }

0-7返回 permitALL即不須要認證 ,8對應anyRequest返回 authenticated即當前請求須要認證;

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/security-decide.png

能夠看到當前的authenticated爲匿名AnonymousAuthentication用戶名爲anonymousUser

AccessDecisionManager 是如何受權的?

Spring Security默認使用AffirmativeBased實現AccessDecisionManagerdecide 方法來實現受權

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;
		#1.調用AccessDecisionVoter 進行vote(投票)
		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
			#1.1只要有voter投票爲ACCESS_GRANTED,則經過 直接返回
			case AccessDecisionVoter.ACCESS_GRANTED://1
				return;
			@#1.2只要有voter投票爲ACCESS_DENIED,則記錄一下
			case AccessDecisionVoter.ACCESS_DENIED://-1
				deny++;

				break;

			default:
				break;
			}
		}

		if (deny > 0) {
		#2.若是有兩個及以上AccessDecisionVoter(姑且稱之爲投票者吧)都投ACCESS_DENIED,則直接就不經過了
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
  1. 調用AccessDecisionVoter 進行vote(投票)
  2. 只要有投經過(ACCESS_GRANTED)票,則直接判爲經過。
  3. 若是沒有投經過則 deny++ ,最後判斷if(deny>0 拋出AccessDeniedException(未受權)

WebExpressionVoter.vote()

public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;

		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}

		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);

		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

到此位置authentication當前用戶信息,fl當前訪問的資源路徑及attributes當前資源路徑的決策(便是否須要認證)。剩下就是判斷當前用戶的角色Authentication.authorites是否權限訪問決策訪問當前資源fi

時序圖

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/authenorization-Sequence%20Diagram0.png

相關文章
相關標籤/搜索