spring-security(十八)核心Filter-FilterSecurityInterceptor

前言:
  当用spring security时,我们会用到各种各样的filter,在接下来的章节中我们我们将着重讨论几个核心的Filter,本节将讨论FilterSecurityInterceptor这个filter。
和这个类相关的对象如下图所示

一、FilterSecurityInterceptor功能和属性
1.FilterSecurityInterceptor的主要职责是处理http资源的安全性。从上面的关系图中,可以知道这个类中主要有以下属性
  • AuthenticationManager-认证
  • AccessDecisionManager-鉴权
  • SecurityMetadataSource-获取属性列表
  • RunAsManager-替换认证用户
  • AfterInvocationManager-鉴权完成后续处理

上面的属性会在下面三个主要的方法中被使用到
  • beforeInvocation
  • finallyInvocation
  • afterInvocation

1.1.beforeInvocation方法
这个方法是最主要的方法,我们的权限判断逻辑主要在这个方法里进行,主要执行下面几步逻辑
  • 调用SecurityMetadataSource(实际执行时spring boot为我们装配的实例是ExpressionBasedFilterInvocationSecurityMetadataSource,可以通过FilterSecurityInterceptor.setSecurityMetadataSource方法修改)来获取匹配当前请求的ConfigAttribute列表,如果获取的列表为null并且rejectPublicInvocations属性配置的是true(不允许存在不受保护的调用),则直接抛出异常,否则鉴权处理结束,如果不为null,执行下一步,因为ExpressionBasedFilterInvocationSecurityMetadataSource在获取当前request相匹配的ConfigAttribute列表时是按照定义的顺序来查找的,一旦找到匹配的就直接返回,所有越具体的匹配规则应配置的越靠前
  • 判断SecurityContextHolder中是否包含Authentication对象,如果没有,就是说程序执行到这个鉴权的filter了却还没有认证过,直接抛出AuthenticationException异常,如果有Authentication,执行下一步
  • 判断alwaysReauthenticate的值,如果设置成true,即所有请求在鉴权前都需要重新认证,则会调用AuthenticationManager(实际执行时是ProviderManager实例)的authenticate方法,进行具体的再认证过程,并把认证结果放入SecurityContextHolder中。
  • 接着调用AccessDecisionManager(默认情况下是AffirmativeBased实例)decide方法,传入Authentication对象、当前的安全对象、以及对应的ConfigAttribute列表开始鉴权,在鉴权过程中如果发生AccessDeniedException,发布鉴权异常事件并抛出异常
  • 如果配置了RunAsManager(在有些特殊场合下,如我们的业务层的某个方法中需要访问外部系统,需要我们提供一个不同的证书,我们可以配置这个RunAsManager,将当前认证过的用户替换成外部系统需要的认证者,之后spring security会自动把安全证书传递到外部系统中,默认是NullRunAsManager即不需要转换用户),则对用户进行转换,将转换后的用户存入SecurityContextHolder中,放回一个InterceptorStatusToken,否则直接返回InterceptorStatusToken对象

另外说下FilterSecurityInterceptor的securityMetadataSource属性实际定义的是SecurityMetadataSource的子类FilterInvocationSecurityMetadataSource,这个接口是一个标记接口,里面没有方法,仅仅说明用这个接口的实现类知道传入的安全对象是一个FilterInvocation,并能从里面获取到request,
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;
}

1.2.finallyInvocation方法
这个方法的逻辑比较简单,如果配置了RunAsManager,在前一步执行过程中我们会把Authentication对象替换掉,这个方法里就是把原始的Authentication给替换回来,如果没有Authentication没有被替换过,这个方法什么都不做。
1.3.afterInvocation
如果配置了AfterInvocationManager属性,在这个方法中会调用AfterInvocationManager.decide方法,这个主要是在一些特定场合下,我们需要修改安全认证的返回结果,例如在MethodSecurityInterceptor认证中,如果我们方法放回的是一个list,我们想把这个list中的某些数据过滤掉则会配置这个属性。在FilterSecurityInterceptor认证中,不会有返回值,所以这个属性正常不会被配置。
二、在spring boot环境下,采用Java config机制这个filter是如何追加到servlet中对我们的请求进行拦截的呢
在前面的章节 (spring-security(十六)Filter配置原理)中,我们知道spring 安全相关的Filter是在WebSecurity的build方法中调用HttpSecurity的build来将追加到HttpSecurity中filter列表排好序后构建成SecurityFilterChain,再把所有的SecurityFilterChain追加到FilterChainProxy中,最后通过DelegatingFilterProxy注册到ServletContext中的,下面我们主要来看下这个类是如何追加到HttpSecuriy的filter列表中的,以及对应的主要属性是如何配置的。
1. 从我们的配置入口WebSecurityConfigurerAdapter类开始,首先是在WebSecurityConfigurerAdapter类的configure(HttpSecurity http)方法中会调用http.authorizeRequests()方法,在实际应用中我们会重写configure方法来自定义安全策略,但是一定会执行http.authorizeRequests()方法。下面看下这个方法
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()throws Exception {
	ApplicationContext context = getContext();
	return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>(context)).getRegistry();
	}

两个功能
  • 创建了一个实现了SecurityConfigurer接口的配置类ExpressionUrlAuthorizationConfigurer,通过调用getOrApply方法最终追加到HttpSecurity的configurers属性中
  • 通过 ExpressionUrlAuthorizationConfigurer的getRegistry返回了一个ExpressionInterceptUrlRegistry对象

2. WebSecurity在构建HttpSecurity时,会调用HttpSecurity的build方法,这个方法会先执行HttpSecurity的configure()方法,就是依次调用configurers属性中各个SecurityConfigurer的configure方法
private void configure() throws Exception {
	Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
		for (SecurityConfigurer<O, B> configurer : configurers) {
			configurer.configure((B) this);
		}
}

private Collection<SecurityConfigurer<O, B>> getConfigurers() {
	List<SecurityConfigurer<O, B>> result = new ArrayList<SecurityConfigurer<O, B>>();
		for (List<SecurityConfigurer<O, B>> configs : this.configurers.values()) {
			result.addAll(configs);
		}
		return result;
}

3. 下面来看下ExpressionUrlAuthorizationConfigurer的configure方法,代码逻辑在父类AbstractInterceptUrlConfigurer中
@Override
	public void configure(H http) throws Exception {
		FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
		if (metadataSource == null) {
			return;
		}
		FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
				http, metadataSource, http.getSharedObject(AuthenticationManager.class));
		if (filterSecurityInterceptorOncePerRequest != null) {
			securityInterceptor
					.setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
		}
		securityInterceptor = postProcess(securityInterceptor);
		http.addFilter(securityInterceptor);
		http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
	}

在这个方法里面主要执行了下面三件事
  • 通过调用createMetadataSource方法创建了FilterInvocationSecurityMetadataSource对象
  • 调用createFilterSecurityInterceptor创建了FilterSecurityInterceptor
  • 通过http.addFilter方法追加到了HttpSecurity的filter列表中

这样就可以明确看出我们的FilterSecurityInterceptor被加入到了httpsecurity的filter列表中了,下面我们看下这个filter中几个主要属性是怎么设置的
首先是创建metadataSource的方法createMetadataSource,具体代码逻辑就在ExpressionUrlAuthorizationConfigurer中
@Override
	final ExpressionBasedFilterInvocationSecurityMetadataSource createMetadataSource(
			H http) {
		LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = REGISTRY.createRequestMap();
		if (requestMap.isEmpty()) {
			throw new IllegalStateException("At least one mapping is required (i.e. authorizeRequests().anyRequest().authenticated())");
		}
		return new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap,
				getExpressionHandler(http));
	}

主要是通过REGISTRY.createRequestMap来获取,这个REGISTRY就是我们前面看到的Httpsecurity.authorizeRequests返回的值,我们设置的安全规则主要就是通过这个类设置的,例如下面的代码段
protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()
			.authorizeRequests()
				.antMatchers("/admin/**").hasRole("ADMIN")
				.anyRequest().hasRole("USER")
				.and()
			.formLogin()
				.permitAll();
	}

通过antMatchers方法,会创建一个AuthorizedUrl,里面的requestMatchers是AntPathRequestMatcher列表,对应的匹配路径是/admin/**,之后调用他的hasRole方法,具体代码在AuthorizedUrl类中,如下
public ExpressionInterceptUrlRegistry hasRole(String role) {
			return access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
		}
...
public ExpressionInterceptUrlRegistry access(String attribute) {
			if (not) {
				attribute = "!" + attribute;
			}
			interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
			return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
		}
...
private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
			Collection<ConfigAttribute> configAttributes) {
		for (RequestMatcher requestMatcher : requestMatchers) {
			REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
					requestMatcher, configAttributes));
		}
	}

看到将我们设置好的路径匹配模式和对应的属性追加到了REGISTRY中,通过hasRole方法我们传入的是ADMIN字符串,是如何转换成ConfigAttribute呢,在上面的access方法中,我们看到是调用了SecurityConfig.createList(attribute)来做的
public static List<ConfigAttribute> createList(String... attributeNames) {
		Assert.notNull(attributeNames, "You must supply an array of attribute names");
		List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(
				attributeNames.length);

		for (String attribute : attributeNames) {
			attributes.add(new SecurityConfig(attribute.trim()));
		}

		return attributes;
	}

就是简单的将我们的字符串包装成ConfigAttribute 的一个具体实现类SecurityConfig。

这样通过
.antMatchers("/admin/**").hasRole("ADMIN")

这样一句配资,我们最终在ExpressionUrlAuthorizationConfigurer的REGISTRY属性中追加了如下一条匹配模式是/admin/**,对应的attributes是包含ADMIN字符串的SecurityConfig。
现在我们再回过头在看下创建SecurityMetadataSource的REGISTRY.createRequestMap方法
final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> createRequestMap() {
		if (unmappedMatchers != null) {
			throw new IllegalStateException(
					"An incomplete mapping was found for "
							+ unmappedMatchers
							+ ". Try completing it with something like requestUrls().<something>.hasRole('USER')");
		}

		LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>();
		for (UrlMapping mapping : getUrlMappings()) {
			RequestMatcher matcher = mapping.getRequestMatcher();
			Collection<ConfigAttribute> configAttrs = mapping.getConfigAttrs();
			requestMap.put(matcher, configAttrs);
		}
		return requestMap;
	}

很明显,就是将我们追加到里面的UrlMapping转换成Map返回,最终构造出了一个ExpressionBasedFilterInvocationSecurityMetadataSource返回出去,在这个类的构造函数中,还会做一次转换把配置属性的类型转换成WebExpressionConfigAttribute(在鉴权类中用的Voter类是WebExpressionVoter,对应的属性是WebExpressionConfigAttribute,下面会提到)。
这样SecurityMetadataSource的创建过程就结束了。当一个请求进入FilterSecurityInterceptor中,就利用SecurityMetadataSource中的requestMap对路径进行匹配,找到第一个匹配的配置项后就获取到了对应的ConfigAttribute列表,传入到具体的鉴权类中进行处理。
4. 下面看下AuthenticationManager和AccessDecisionManager如何设置
private FilterSecurityInterceptor createFilterSecurityInterceptor(H http,
			FilterInvocationSecurityMetadataSource metadataSource,
			AuthenticationManager authenticationManager) throws Exception {
		FilterSecurityInterceptor securityInterceptor = new FilterSecurityInterceptor();
		securityInterceptor.setSecurityMetadataSource(metadataSource);
		securityInterceptor.setAccessDecisionManager(getAccessDecisionManager(http));
		securityInterceptor.setAuthenticationManager(authenticationManager);
		securityInterceptor.afterPropertiesSet();
		return securityInterceptor;
	}

可以看到authenticationManager直接用的是通过AuthenticationManagerBuilder构建出来后存在Httpsecurity中的对象,AccessDecisionManager是通过getAccessDecisionManager这个方法获取的
private AccessDecisionManager getAccessDecisionManager(H http) {
		if (accessDecisionManager == null) {
			accessDecisionManager = createDefaultAccessDecisionManager(http);
		}
		return accessDecisionManager;
	}
...
private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
		AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
		return postProcess(result);
	}

在默认情况下,spring 给我们组装了一个AffirmativeBased类,用的Voter类通过getDecisionVoters获取,具体在ExpressionUrlAuthorizationConfigurer类中
@Override
	@SuppressWarnings("rawtypes")
	final List<AccessDecisionVoter<? extends Object>> getDecisionVoters(H http) {
		List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();
		WebExpressionVoter expressionVoter = new WebExpressionVoter();
		expressionVoter.setExpressionHandler(getExpressionHandler(http));
		decisionVoters.add(expressionVoter);
		return decisionVoters;
	}

因为用的是WebExpressionVoter,所以在之前创建SecurityMetadataSource时需要做一次转换。

各种AccessDecisionManager实现类的具体意义我们在讨论鉴权的时候再具体分析。

这样我们的FilterSecurityInterceptor就完全组装好了,并且也作为Filter追加到了servlet中,可以对我们的资源进行保护了。

猜你喜欢

转载自fengyilin.iteye.com/blog/2411449
今日推荐