Spring实战----Security4.1.3鉴权之美--基于投票的AccessDecisionManager实现及源码分析

Posted Herman-Hong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring实战----Security4.1.3鉴权之美--基于投票的AccessDecisionManager实现及源码分析相关的知识,希望对你有一定的参考价值。

一、背景知识

Spring实战篇系列----Security4.1.3认证过程源码分析Spring实战篇系列----Security4.1.3实现根据请求跳转不同登录页以及登录后根据权限跳转到不同页配置中均有提到,每一次请求都会走Security Filter,鉴权的过滤器为FilterSecurityInterceptor,其中会判断是否要对请求进行鉴权,以及需要鉴权的会基于投票的AccessDecisionManager实现鉴权操作。

1)判断是否鉴权是根据,配置文件中是否有如下配置:

<intercept-url pattern="/order/**" access="hasRole('ROLE_USER')"/>
<intercept-url pattern="/manager" access="hasRole('ROLE_MANAGER')"/>


关键代码:AbstractSecurityInterceptor.java

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);
		//这里获取上面属性access的值,根据是否有值判断是否需要鉴权
		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);   //需要鉴权的请求有accessDecisionManager进行鉴权
		
		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);
		
	

2)需要鉴权的有accessDecisionManager进行鉴权


二、鉴权的过程

1)基本知识:

Spring Security是通过拦截器来控制受保护对象的访问的,如方法调用和Web请求。在正式访问受保护对象之前,Spring Security将使用AccessDecisionManager来鉴定当前用户是否有访问对应受保护对象的权限。

AccessDecisionManager是由AbstractSecurityInterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或URL)的权限。AccessDecisionManager是一个接口,其中只定义了三个方法,其定义如下。

public interface AccessDecisionManager 

 

    /**

     * 通过传递的参数来决定用户是否有访问对应受保护对象的权限

     *

     * @param authentication 当前正在请求受包含对象的Authentication

     * @param object 受保护对象,其可以是一个MethodInvocation、JoinPoint或FilterInvocation。

     * @param configAttributes 与正在请求的受保护对象相关联的配置属性

     *

     */

    void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)

        throws AccessDeniedException, InsufficientAuthenticationException;

 

    /**

     * 表示当前AccessDecisionManager是否支持对应的ConfigAttribute

     */

    boolean supports(ConfigAttribute attribute);

 

    /**

     * 表示当前AccessDecisionManager是否支持对应的受保护对象类型

     */

    boolean supports(Class<?> clazz);


decide()方法用于决定authentication是否符合受保护对象要求的configAttributessupports(ConfigAttribute attribute)方法是用来判断AccessDecisionManager是否能够处理对应的ConfigAttribute的。supports(Class<?> clazz)方法用于判断配置的AccessDecisionManager是否支持对应的受保护对象类型。

2)投票系统










Spring Security已经内置了几个基于投票的AccessDecisionManager,当然如果需要你也可以实现自己的AccessDecisionManager。以下是Spring Security官方文档提供的一个图,其展示了与基于投票的AccessDecisionManager实现相关的类。还有比较重要的WebExpressionVoter



 

       使用这种方式,一系列的AccessDecisionVoter将会被AccessDecisionManager用来对Authentication是否有权访问受保护对象进行投票,然后再根据投票结果来决定是否要抛出AccessDeniedExceptionAccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

public interface AccessDecisionVoter<S> 

 

    intACCESS_GRANTED = 1;

    intACCESS_ABSTAIN = 0;

    intACCESS_DENIED = -1;

 

    boolean supports(ConfigAttribute attribute);

 

    boolean supports(Class<?> clazz);

 

    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);



vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意,ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN

       Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBasedConsensusBasedUnanimousBased

       AffirmativeBased的逻辑是这样的:

       1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

       2)如果全部弃权也表示通过;

       3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException

       ConsensusBased的逻辑是这样的:

       1)如果赞成票多于反对票则表示通过。

       2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException

       3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true

       4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false

       UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttributeAccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:

       1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException

       2)如果没有反对票,但是有赞成票,则表示通过。

       3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException

投票者: 

RoleVoterSpring Security内置的一个AccessDecisionVoter,其会将ConfigAttribute简单的看作是一个角色名称,在投票的时如果拥有该角色即投赞成票。如果ConfigAttribute是以“ROLE_”开头的,则将使用RoleVoter进行投票。当用户拥有的权限中有一个或多个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute时其将投赞成票;如果用户拥有的权限中没有一个能匹配受保护对象配置的以“ROLE_”开头的ConfigAttribute,则RoleVoter将投反对票;如果受保护对象配置的ConfigAttribute中没有以“ROLE_”开头的,则RoleVoter将弃权。


AuthenticatedVoter也是Spring Security内置的一个AccessDecisionVoter实现。其主要用来区分匿名用户、通过Remember-Me认证的用户和完全认证的用户。完全认证的用户是指由系统提供的登录入口进行成功登录认证的用户。

       AuthenticatedVoter可以处理的ConfigAttributeIS_AUTHENTICATED_FULLYIS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY。如果ConfigAttribute不在这三者范围之内,则AuthenticatedVoter将弃权。否则将视ConfigAttribute而定,如果ConfigAttributeIS_AUTHENTICATED_ANONYMOUSLY,则不管用户是匿名的还是已经认证的都将投赞成票;如果是IS_AUTHENTICATED_REMEMBERED则仅当用户是由Remember-Me自动登录,或者是通过登录入口进行登录认证时才会投赞成票,否则将投反对票;而当ConfigAttributeIS_AUTHENTICATED_FULLY时仅当用户是通过登录入口进行登录的才会投赞成票,否则将投反对票。

       AuthenticatedVoter是通过AuthenticationTrustResolverisAnonymous()方法和isRememberMe()方法来判断SecurityContextHolder持有的Authentication是否为AnonymousAuthenticationTokenRememberMeAuthenticationToken的,即是否为IS_AUTHENTICATED_ANONYMOUSLYIS_AUTHENTICATED_REMEMBERED


自定义Voter及重要的WebExpressionVoter

当然,用户也可以通过实现AccessDecisionVoter来实现自己的投票逻辑。

以上参考:http://elim.iteye.com/blog/2247057


三:源码分析

那么怎么选择accessDecisionManager及Voter,看下最初配置FilterSecurityInterceptor的源码

private void createFilterSecurityInterceptor(BeanReference authManager) 
		boolean useExpressions = FilterInvocationSecurityMetadataSourceParser
				.isUseExpressions(httpElt);
		RootBeanDefinition securityMds = FilterInvocationSecurityMetadataSourceParser
				.createSecurityMetadataSource(interceptUrls, addAllAuth, httpElt, pc);

		RootBeanDefinition accessDecisionMgr;
		ManagedList<BeanDefinition> voters = new ManagedList<BeanDefinition>(2);

		if (useExpressions) 
			BeanDefinitionBuilder expressionVoter = BeanDefinitionBuilder
					.rootBeanDefinition(WebExpressionVoter.class);
			// Read the expression handler from the FISMS
			RuntimeBeanReference expressionHandler = (RuntimeBeanReference) securityMds
					.getConstructorArgumentValues()
					.getArgumentValue(1, RuntimeBeanReference.class).getValue();

			expressionVoter.addPropertyValue("expressionHandler", expressionHandler);

			voters.add(expressionVoter.getBeanDefinition());
		
		else 
			voters.add(new RootBeanDefinition(RoleVoter.class));
			voters.add(new RootBeanDefinition(AuthenticatedVoter.class));
		
		accessDecisionMgr = new RootBeanDefinition(AffirmativeBased.class);
		accessDecisionMgr.getConstructorArgumentValues().addGenericArgumentValue(voters);
		accessDecisionMgr.setSource(pc.extractSource(httpElt));

		// Set up the access manager reference for http
		String accessManagerId = httpElt.getAttribute(ATT_ACCESS_MGR);

		if (!StringUtils.hasText(accessManagerId)) 
			accessManagerId = pc.getReaderContext().generateBeanName(accessDecisionMgr);
			pc.registerBeanComponent(new BeanComponentDefinition(accessDecisionMgr,
					accessManagerId));
		

		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.rootBeanDefinition(FilterSecurityInterceptor.class);

		builder.addPropertyReference("accessDecisionManager", accessManagerId);
		builder.addPropertyValue("authenticationManager", authManager);

		if ("false".equals(httpElt.getAttribute(ATT_ONCE_PER_REQUEST))) 
			builder.addPropertyValue("observeOncePerRequest", Boolean.FALSE);
		

		builder.addPropertyValue("securityMetadataSource", securityMds);
		BeanDefinition fsiBean = builder.getBeanDefinition();
		String fsiId = pc.getReaderContext().generateBeanName(fsiBean);
		pc.registerBeanComponent(new BeanComponentDefinition(fsiBean, fsiId));

		// Create and register a DefaultWebInvocationPrivilegeEvaluator for use with
		// taglibs etc.
		BeanDefinition wipe = new RootBeanDefinition(
				DefaultWebInvocationPrivilegeEvaluator.class);
		wipe.getConstructorArgumentValues().addGenericArgumentValue(
				new RuntimeBeanReference(fsiId));

		pc.registerBeanComponent(new BeanComponentDefinition(wipe, pc.getReaderContext()
				.generateBeanName(wipe)));

		this.fsi = new RuntimeBeanReference(fsiId);
	

以上如果实用了useExpressions(有属性use-expressions指定,默认的也是true)即SPEL表达式,则选择WebExpressionVoter,否则选择RoleVoter及AuthenticatedVoter

如果配置了access-decision-manager-ref属性则将accessDecisionManager设置为配置的,否则默认为AffirmativeBased

根据这里描述,本系列的Security配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
		
	<http auto-config="true" use-expressions="true" entry-point-ref="myAuthenticationEntryPoint" >
		<form-login 
			login-page="/login"
            login-processing-url="/**/login"
            authentication-failure-handler-ref="myAuthenticationFailureHandler"
            authentication-success-handler-ref="myAuthenticationSuccessHandler" />   
         <!-- 认证成功用自定义类myAuthenticationSuccessHandler处理 -->
         
         <logout logout-url="/logout" 
				logout-success-url="/" 
				invalidate-session="true"
				delete-cookies="JSESSIONID"/>
		
		<!-- 登录成功后拒绝访问跳转的页面 -->		
		<access-denied-handler error-page="/security/deny" />
		
		<csrf disabled="true" />
		<intercept-url pattern="/order/**" access="hasRole('ROLE_USER')"/>
		<intercept-url pattern="/manager" access="hasRole('ROLE_MANAGER')"/>
	</http>
	
	<!-- 使用自定义类myUserDetailsService从数据库获取用户信息 -->
	<authentication-manager>  
        <authentication-provider user-service-ref="myUserDetailsService">  
        	<!-- 加密 -->
            <password-encoder hash="md5">
            </password-encoder>
        </authentication-provider>
    </authentication-manager>
    
    <!-- 被认证请求根据所需权限跳转到不同的登录界面 -->
    <beans:bean id="myAuthenticationEntryPoint" 
    	class="com.mango.jtt.springSecurity.MyAuthenticationEntryPoint">
    	<beans:property name="authEntryPointMap" ref="loginFormsMap"></beans:property>
    	<beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg>
    </beans:bean>
    
    <!-- 根据不同请求所需权限跳转到不同的登录界面 -->
	<beans:bean id="loginFormsMap" class="java.util.HashMap">
		<beans:constructor-arg>
			<beans:map>
				<beans:entry key="/user/**" value="/login" />
				<beans:entry key="/manager/**" value="/manager/login" />
				<beans:entry key="/**" value="/login" />
			</beans:map>
		</beans:constructor-arg>
	</beans:bean>
	
	<!-- 授权成功后控制 -->
    <beans:bean id="myAuthenticationSuccessHandler" 
    	class="com.mango.jtt.springSecurity.MyAuthenticationSuccessHandler">
    	<beans:property name="authDispatcherMap" ref="dispatcherMap"></beans:property>
    </beans:bean>
	
	<!-- 根据不同的权限,跳转到不同的页面(直接点击登录页面用) -->
	<beans:bean id="dispatcherMap" class="java.util.HashMap">
	  	<beans:constructor-arg>
		    <beans:map>
				<beans:entry key="ROLE_USER" value="/"/>
				<beans:entry key="ROLE_MANAGER" value="/manager"/>
	      	</beans:map>
	  	</beans:constructor-arg>
	</beans:bean>
	
	<!-- 登录失败后控制 -->
    <beans:bean id="myAuthenticationFailureHandler" 
    	class="com.mango.jtt.springSecurity.MyAuthenticationFailureHandler">
    	<beans:property name="loginEntry" ref="myAuthenticationEntryPoint"></beans:property>
    </beans:bean>
</beans:beans>
本系列实用的accessDecisionManager为AffirmativeBased,Voter为WebExpressionVoter


AffirmativeBased.java

/**
	 * This concrete implementation simply polls all configured
	 * @link AccessDecisionVoters and grants access if any
	 * <code>AccessDecisionVoter</code> voted affirmatively. Denies access only if there
	 * was a deny vote AND no affirmative votes.
	 * <p>
	 * If every <code>AccessDecisionVoter</code> abstained from voting, the decision will
	 * be based on the @link #isAllowIfAllAbstainDecisions() property (defaults to
	 * false).
	 * </p>
	 *
	 * @param authentication the caller invoking the method
	 * @param object the secured object
	 * @param configAttributes the configuration attributes associated with the method
	 * being invoked
	 *
	 * @throws AccessDeniedException if access is denied
	 */
	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();
	

有一个同意则通过

WebExpressionVoter.java

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;
	


最终权限的判断交给SecurityExpressionRoot.java

public final boolean hasRole(String role) 
		return hasAnyRole(role);
	

	public final boolean hasAnyRole(String... roles) 
		return hasAnyAuthorityName(defaultRolePrefix, roles);
	

	private boolean hasAnyAuthorityName(String prefix, String... roles) 
		Set<String> roleSet = getAuthoritySet();

		for (String role : roles) 
			String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
			if (roleSet.contains(defaultedRole)) 
				return true;
			
		

		return false;
	


getAuthoritySet()获取到目前用户具有的权限,判断其中是否包含请求所需要的权限,如果包含则返回true,否则false。


总结:

Security的鉴权过程设计的别具一格,设计之美令人叹服!Security的学习也到此为止,当然还有很多内容没有涉及到(总体来说,Security包含"authentication" and "authorization" (or"access-control"). 认证和授权)登录的过程是认证,鉴权的过程是授权。



以上是关于Spring实战----Security4.1.3鉴权之美--基于投票的AccessDecisionManager实现及源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Spring实战----源码解析Spring Security4.1.3中的过滤器Filter配置

Spring实战----security4.1.3认证的过程以及原请求信息的缓存及恢复(RequestCache)

Spring实战----Security4.1.3实现根据请求跳转不同登录页以及登录后根据权限跳转到不同页配置

Spring Security4.1.3实现拦截登录后向登录页面跳转方式(redirect或forward)返回被拦截界面

Spring Security4.1.3实现拦截登录后向登录页面跳转方式(redirect或forward)返回被拦截界面

Spring实战----开篇(包含系列目录链接)