SpringSecurity授权过程

294 阅读7分钟

授权过程主要是由FilterSecurityInterceptor拦截器完成的.所以咱们开发分析它的源码,建议大家先看一下上一篇文章<SpringSecurity核心概念>,了解下授权的概念


这是我的SpringSecurity的配置类

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 内存中创建用户信息
     *
     * @return
     */
    @Bean
    public UserDetailsService userDetailService() {
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(
                User.withUsername("zhangsan")
                        .password("123")
                        .authorities("r1").build()
        );
        userDetailsManager.createUser(
                User.withUsername("lisi")
                        .password("123")
                        .roles("r2")
                        .build()
        );
        userDetailsManager.createUser(
                User.withUsername("wangwu")
                        .password("123")
                        .authorities("r3")
                        .build()
        );
        return userDetailsManager;
    }

    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService())
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf();
        httpSecurity.authorizeRequests()
                .antMatchers("/r/r1").hasAuthority("r1")
                .antMatchers("/r/**")
                .authenticated()
                .anyRequest()
                .permitAll()
                .and()
                .formLogin().successForwardUrl("/r/login-success")
                .and()
                .logout()
                .logoutUrl("/logout")
                ;
        httpSecurity.exceptionHandling();

    }
}


所以我们先来看一看FilterSecurityInterceptor

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {
	// ~ Static fields/initializers
	// =====================================================================================

	private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

	// ~ Instance fields
	// ================================================================================================

	/**
	 * Method that is actually called by the filter chain. Simply delegates to the
	 * {@link #invoke(FilterInvocation)} method.
	 *
	 * @param request the servlet request
	 * @param response the servlet response
	 * @param chain the filter chain
	 *
	 * @throws IOException if the filter chain fails
	 * @throws ServletException if the filter chain fails
	 */
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
        // FilterInvocation 保存与Http Filter有关联的对象 
		FilterInvocation fi = new FilterInvocation(request, response, chain);
		invoke(fi);
	}

	public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public SecurityMetadataSource obtainSecurityMetadataSource() {
		return this.securityMetadataSource;
	}

	public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
		this.securityMetadataSource = newSource;
	}

	public Class<?> getSecureObjectClass() {
		return FilterInvocation.class;
	}

	public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
            // 表示这个请求已经经过这个过滤器了,并且是一次性请求,所以直接放行
			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 {
                // 开始处理真正的业务逻辑,比如开始进入handler mapping链 interceptor ... ->controller
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}
			// 调用父类方法,调用真正业务逻辑之后要做的事	
			super.afterInvocation(token, null);
		}
	}

}

由于#beforeInvocation#afterInvocation都是父类中的逻辑,所以咱们再看下AbstractSecurityInterceptor

public abstract class AbstractSecurityInterceptor implements InitializingBean,
		ApplicationEventPublisherAware, MessageSourceAware {
            
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private ApplicationEventPublisher eventPublisher;
	private AccessDecisionManager accessDecisionManager;
	private AfterInvocationManager afterInvocationManager;
	private AuthenticationManager authenticationManager = new NoOpAuthenticationManager();
	private RunAsManager runAsManager = new NullRunAsManager();

	private boolean alwaysReauthenticate = false;
	private boolean rejectPublicInvocations = false;
	private boolean validateConfigAttributes = true;
	private boolean publishAuthorizationSuccess = false;

	protected InterceptorStatusToken beforeInvocation(Object object) {
		Assert.notNull(object, "Object was null");
		final boolean debug = logger.isDebugEnabled();
		
        // 判断传入的对象是不是FilterInvocation的对象
		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);
		}
	}


}

#beforeInvocation方法的思路是

  • 先判断传入的对象是不是FilterInvocation的对象,如果不是则抛异常(必须保证参数类型的正确性)
  • 获取授权信息属性集合
  • 获取认证信息
  • FilterInvocation,Authentication,ConfigAttribute交由认证管理器进行认证


可能大家会好奇ConfigAttribute是什么,那咱们看一下

public interface ConfigAttribute extends Serializable {

	String getAttribute();

这个接口只有一个#getAttribute()方法,咱们再看看他的实现类
image.png

下面这三个类的名字是不是有点熟悉

  • PreInvocationExpressionAttribute是对应@PreAuthorize的配置属性
  • PostInvocationExpressionAttribute是对应的@PostAuthorize的配置属性
  • SecurityConfig是对应@Secured的配置属性
  • WebExpressionConfigAttribute是在配置文件中声明的配置属性
  • Jsr250SecurityConfig是jsr250注解的实现,这个在SpringSecurity中不常用


所以在获取到授权属性后,我们看看授权管理器是如何进行决策的, 而Spring通常是面向接口编程的,所以我们看看AccessDecisionManager都由哪些实现类
image.png

public interface AccessDecisionManager {
	
	void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
			InsufficientAuthenticationException;
	
	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);
}

我们先看AffirmativeBased,这是AccessDecisionManager的默认实现类

public class AffirmativeBased extends AbstractAccessDecisionManager {

	public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	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"));
		}

		// 检查是否允许都弃权,默认允许
		checkAllowIfAllAbstainDecisions();
	}
}
  • 有一个投票器赞同通过的就通过
  • 投票器没有赞同通过的,并且有一个拒绝的则抛异常
  • 如果都弃权,则通过


既然我们看到逻辑中是通过一系列的投票器来决定是否通过的,所以我们再来看看投票权的逻辑,稍后再看其他两种实现类的逻辑

public interface AccessDecisionVoter<S> {
	// ~ Static fields/initializers
	// =====================================================================================

	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;

	boolean supports(ConfigAttribute attribute);

	boolean supports(Class<?> clazz);

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

他有众多实现类,我们挑一个WebExpressionVoter来说下吧

public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
	private SecurityExpressionHandler<FilterInvocation> expressionHandler = new DefaultWebSecurityExpressionHandler();

	public int vote(Authentication authentication, FilterInvocation fi,
			Collection<ConfigAttribute> attributes) {
		assert authentication != null;
		assert fi != null;
		assert attributes != null;
		
        // 获取WebExpressionConfigAttribute 属性
		WebExpressionConfigAttribute weca = findConfigAttribute(attributes);

		if (weca == null) {
			return ACCESS_ABSTAIN;
		}
		
		EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
				fi);
		ctx = weca.postProcess(ctx, fi);
		// 做比对, 比如/r/r1 必须具有 r1权限 与请求路径 /r/r1比对
		return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
				: ACCESS_DENIED;
	}

	private WebExpressionConfigAttribute findConfigAttribute(
			Collection<ConfigAttribute> attributes) {
		for (ConfigAttribute attribute : attributes) {
            // 这也是一个小细节,只获取第一个符合条件的就会返回,所以再Security配置文件中如果配置多条相似的配置会取第一条
            // httpSecurity.authorizeRequests()
            //    .antMatchers("/r/r1").hasAuthority("r1")
            //    .antMatchers("/r/**")
            //    .authenticated()
            // 要小的权限放在前面,大的权限放在后面
			if (attribute instanceof WebExpressionConfigAttribute) {
				return (WebExpressionConfigAttribute) attribute;
			}
		}
		return null;
	}

	public boolean supports(ConfigAttribute attribute) {
		return attribute instanceof WebExpressionConfigAttribute;
	}

	public boolean supports(Class<?> clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}

	public void setExpressionHandler(
			SecurityExpressionHandler<FilterInvocation> expressionHandler) {
		this.expressionHandler = expressionHandler;
	}
}
public class ConsensusBased extends AbstractAccessDecisionManager {
	//  // 允许赞同与拒绝数量可以相等
	private boolean allowIfEqualGrantedDeniedDecisions = true;

	public ConsensusBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int grant = 0;
		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:
				grant++;

				break;

			case AccessDecisionVoter.ACCESS_DENIED:
				deny++;

				break;

			default:
				break;
			}
		}
		// 赞同的数量大于拒绝数量,通过
		if (grant > deny) {
			return;
		}
		// 赞同数量小于拒绝数量,拒绝
		if (deny > grant) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}
		// 赞同数量与拒绝数量持平,且他们都不为0
		if ((grant == deny) && (grant != 0)) {
            // 允许赞同与拒绝数量可以相等,通过.默认允许
			if (this.allowIfEqualGrantedDeniedDecisions) {
				return;
			}
            // 不允许,则拒绝
			else {
				throw new AccessDeniedException(messages.getMessage(
						"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
			}
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}

}
  • 赞同的数量大于拒绝数量,通过
  • 赞同数量小于拒绝数量,拒绝
  • 赞同数量与拒绝数量持平,且他们都不为0,允许赞同与拒绝数量可以相等,通过.默认允许
  • 如果都弃权,则通过
public class UnanimousBased extends AbstractAccessDecisionManager {

	public UnanimousBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> attributes) throws AccessDeniedException {

		int grant = 0;

		List<ConfigAttribute> singleAttributeList = new ArrayList<>(1);
		singleAttributeList.add(null);
		// 一条属性一条属性的验证
		for (ConfigAttribute attribute : attributes) {
			singleAttributeList.set(0, attribute);

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

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

				switch (result) {
				case AccessDecisionVoter.ACCESS_GRANTED:
					grant++;

					break;

				case AccessDecisionVoter.ACCESS_DENIED:
					throw new AccessDeniedException(messages.getMessage(
							"AbstractAccessDecisionManager.accessDenied",
							"Access is denied"));

				default:
					break;
				}
			}
		}

		// To get this far, there were no deny votes
		if (grant > 0) {
			return;
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
}
  • 如果存在反对票,则拒绝
  • 如果没有反对票,有赞同票,则通过
  • 如果都弃权了默认通过


Unanimous还有一个与上面实现类两个不同的一点,他在做授权时,会把授权信息一条一条的传给投票权,让投票器把授权信息都确认一遍,都不反对才会通过; 而上面那俩实现类直接把授权信息的集合传给投票器, 这样如果条配置相似,只取第一个,符个第一个就会投赞同票

最后再通过流程图总结一下

image.png

  • 首先FilterSecurityInterceptorSecurityContextHolder中获取Authentication
  • FilterSecurityInterceptor创建FilterInvocation(包含HttpServletRequest,HttpServletResponse,FilterChain)
  • FilterInvocation传递给SecurityMetaDataSource获取ConfigAttributes
  • FilterInvocation, Authentication, ConfigAttributes传递给AccessDecisionManager
    • 如果授权失败,则抛出AccessDeniedException,然后ExceptionTranslationFilter处理这个异常
    • 如果授权成功,则FilterSecurityInteceptor继续执行FilterChain后面逻辑

最后再提一句,对于在Controller中加了注解@Secured,@PreAuthorize的方法,Spring是通过创建一个MethodSecurityInterceptor拦截器实现权限管理的,它与FilterSecurityInterceptor是兄弟类.

在一个请求进入dispatchservlet时,根据请求路径获取拦截器链,在这个过程中进行权限校验的