SpringSecurity分析-3-訪問受權

由前兩篇博客大概瞭解了SS的啓動初始化和表單登錄的過程java

  • 在初始化過程當中,Security會加載用戶配置的權限信息,好比:
http.authorizeRequests()
//		.antMatchers("/")
//		.permitAll() // 請求路徑"/"容許訪問
//		.anyRequest()
//		.authenticated() // 其它請求都不須要校驗才能訪問
		.antMatchers("/home")
		.hasRole("LOGOPR")
	.and()
		.formLogin()
		.loginPage("/login") // 定義登陸的頁面"/login",容許訪問
		.permitAll()
		.failureUrl("/login?#error=1111")
//		.failureHandler(failHander)
	.and()
		.logout() // 默認的"/logout", 容許訪問
		.permitAll();
	}

這裏面就包含了請求及對應的權限,好比/home須要LOGOPR角色,等等web

  • 表單登錄成功以後,SS中就存儲了當前用戶的信息,也就包括了用戶的權限

那麼在訪問的時候,SS就能根據訪問路徑的權限與訪問用戶的權限進行比對,符合則經過,不符合就拋出異常重定向到指定界面spring

看看訪問受權的時序圖:express

代碼的調用比較複雜,上圖簡單化了,主要是一個受權的過程,下面就根據源碼來瞧瞧吧!app

以上面的代碼爲例,訪問/home須要LOGOPR權限,登錄用戶默認擁有ROLE_USER權限ide

 

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

//invoke
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 && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
		InterceptorStatusToken token = super.beforeInvocation(fi);
		try {
		    fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		} finally {
		    super.finallyInvocation(token);
		}
		super.afterInvocation(token, null);
	}
}

看到代碼中調用父類AbstractSecurityInterceptor的beforeInvocation(),調用前,返回一個InterceptorStatusToken,後續還會調用afterInvocation,那麼繼續跟進beforeInvocation:post

protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();

		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
			throw new IllegalArgumentException(
					"Security invocation attempted for object "
							+ object.getClass().getName()
							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
							+ getSecureObjectClass());
		}

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

		if (attributes == null || attributes.isEmpty()) {
			if (rejectPublicInvocations) {
				throw new IllegalArgumentException(
						"Secure object invocation "
								+ object
								+ " was denied as public invocations are not allowed via this interceptor. "
								+ "This indicates a configuration error because the "
								+ "rejectPublicInvocations property is set to 'true'");
			}

			if (debug) {
				logger.debug("Public object - authentication not attempted");
			}

			publishEvent(new PublicInvocationEvent(object));

			return null; // no further work post-invocation
		}

		if (debug) {
			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
		}

		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			credentialsNotFound(messages.getMessage(
					"AbstractSecurityInterceptor.authenticationNotFound",
					"An Authentication object was not found in the SecurityContext"),
					object, attributes);
		}

		Authentication authenticated = authenticateIfRequired();

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

			throw accessDeniedException;
		}

		if (debug) {
			logger.debug("Authorization successful");
		}

		if (publishAuthorizationSuccess) {
			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
		}

		// Attempt to run as a different user
		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
				attributes);

		if (runAs == null) {
			if (debug) {
				logger.debug("RunAsManager did not change Authentication object");
			}

			// no further work post-invocation
			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
					attributes, object);
		}
		else {
			if (debug) {
				logger.debug("Switching to RunAs Authentication: " + runAs);
			}

			SecurityContext origCtx = SecurityContextHolder.getContext();
			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
			SecurityContextHolder.getContext().setAuthentication(runAs);

			// need to revert to token.Authenticated post-invocation
			return new InterceptorStatusToken(origCtx, true, attributes, object);
		}
	}

先調用DefaultFilterInvocationSecurityMetadataSource的getAttributes方法,返回一個Collection<ConfigAttribute>,是一個ConfigAttribute集合,代碼比較簡單:ui

public Collection<ConfigAttribute> getAttributes(Object object) {
		final HttpServletRequest request = ((FilterInvocation) object).getRequest();
		for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
				.entrySet()) {
			if (entry.getKey().matches(request)) {
				return entry.getValue();
			}
		}
		return null;
	}

注意requestMap,以下:this

{
ExactUrl [processUrl='/login?#error=1111']=[permitAll], 
ExactUrl [processUrl='/login']=[permitAll], 
ExactUrl [processUrl='/login']=[permitAll], 
ExactUrl [processUrl='/login?logout']=[permitAll], 
Ant [pattern='/home']=[hasRole('ROLE_LOGOPR'),
Ant [pattern='/logout', POST]=[permitAll],]
}lua

能夠看到這個requestMap保存的就是在自定義HttpSecurity的配置的路徑-權限信息,遍歷此Map,和請求的request進行匹配,請求的路徑是/home,匹配成功,以下:

返回的value:

能夠看到,/home匹配的權限信息就是上面配置的ROLE_LOGOPR

繼續跟進,直到:

Authentication authenticated = authenticateIfRequired();

從方法命名(是否必須受權)能夠看出,這裏就是返回受權用戶的方法,從登錄流程就直到,登錄成功的結果就是產生一個Authentication對象的過程,返回的對象以下:

org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b43690ca: 
Principal: org.springframework.security.core.userdetails.User@f02988d6: 
Username: username; 
Password: [PROTECTED]; 
Enabled: true; 
AccountNonExpired: true; 
credentialsNonExpired: true; 
AccountNonLocked: true; 
Granted Authorities: ROLE_USER; 
Credentials: [PROTECTED]; 
Authenticated: true; 
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@21a2c: 
RemoteIpAddress: 0:0:0:0:0:0:0:1; 
SessionId: A08566CA24D4246E04EFC436D3738400; 
Granted Authorities: ROLE_USER

這個時候,訪問請求的路徑及權限,登錄用戶的信息都已經獲取了,下面就是決定是否能訪問了!!!

執行:this.accessDecisionManager.decide(authenticated, object, attributes);

調用AffirmativeBased的decide方法:

public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}
			switch (result) {
			case AccessDecisionVoter.ACCESS_GRANTED:
				return;
			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;
				break;
			default:
				break;
			}
		}
		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}
		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

經過遍歷一個投票器集合來決定int deny的值,由其判斷受權的結果!

查看投票器集合,僅有一個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;
	}

由attributes生成WebExpressionConfigAttribute對象weca,由authentication生成EvaluationContext對象ctx,關鍵調用:ctx = weca.postProcess(ctx, fi);

返回更新後的ctx:

後續主要調用spring的方法,後面再跟進

執行WebExpressionVoter完畢,返回int結果爲-1,那麼deny++,deny=1,代碼拋出異常:

if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

異常捕獲,直到doFilter結束。。。

先暫停!

相關文章
相關標籤/搜索