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

本文深入探讨Spring Security的FilterSecurityInterceptor,它是处理HTTP资源安全性的重要组件。内容包括其功能、属性、核心方法(beforeInvocation、finallyInvocation、afterInvocation)的详细解释,以及在Spring Boot环境下如何通过Java配置将其添加到Servlet中进行请求拦截。
摘要由CSDN通过智能技术生成
前言:
当用spring security时,我们会用到各种各样的filter,在接下来的章节中我们我们将着重讨论几个核心的Filter,本节将讨论FilterSecurityInterceptor这个filter。
和这个类相关的对象如下图所示
[img]http://dl2.iteye.com/upload/attachment/0128/9716/26917b9a-17ba-3202-9103-503dcb506b64.png[/img]
一、FilterSecurityInterceptor功能和属性
1.FilterSecurityInterceptor的主要职责是处理http资源的安全性。从上面的关系图中,可以知道这个类中主要有以下属性
[list]
[*]AuthenticationManager-认证
[*]AccessDecisionManager-鉴权
[*]SecurityMetadataSource-获取属性列表
[*]RunAsManager-替换认证用户
[*]AfterInvocationManager-鉴权完成后续处理
[/list]
上面的属性会在下面三个主要的方法中被使用到
[list]
[*]beforeInvocation
[*]finallyInvocation
[*]afterInvocation
[/list]
1.1.beforeInvocation方法
这个方法是最主要的方法,我们的权限判断逻辑主要在这个方法里进行,主要执行下面几步逻辑
[list]
[*]调用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对象
[/list]
另外说下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中对我们的请求进行拦截的呢
在前面的章节[url=http://fengyilin.iteye.com/admin/blogs/2411127](spring-security(十六)Filter配置原理)[/url]中,我们知道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();
}

两个功能
[list]
[*]创建了一个实现了SecurityConfigurer接口的配置类ExpressionUrlAuthorizationConfigurer,通过调用getOrApply方法最终追加到HttpSecurity的configurers属性中
[*]通过 ExpressionUrlAuthorizationConfigurer的getRegistry返回了一个ExpressionInterceptUrlRegistry对象
[/list]
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);
}

在这个方法里面主要执行了下面三件事
[list]
[*]通过调用createMetadataSource方法创建了FilterInvocationSecurityMetadataSource对象
[*]调用createFilterSecurityInterceptor创建了FilterSecurityInterceptor
[*]通过http.addFilter方法追加到了HttpSecurity的filter列表中
[/list]
这样就可以明确看出我们的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中,可以对我们的资源进行保护了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值