当用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 CollectiongetAttributes(Object object) { final HttpServletRequest request = ((FilterInvocation) object).getRequest(); for (Map.Entry > 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.ExpressionInterceptUrlRegistry authorizeRequests()throws Exception { ApplicationContext context = getContext(); return getOrApply(new ExpressionUrlAuthorizationConfigurer (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> configurers = getConfigurers(); for (SecurityConfigurer configurer : configurers) { configurer.configure((B) this); } } private Collection > getConfigurers() { List > result = new ArrayList >(); for (List > 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> 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, CollectionconfigAttributes) { for (RequestMatcher requestMatcher : requestMatchers) { REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping( requestMatcher, configAttributes)); } }
看到将我们设置好的路径匹配模式和对应的属性追加到了REGISTRY中,通过hasRole方法我们传入的是ADMIN字符串,是如何转换成ConfigAttribute呢,在上面的access方法中,我们看到是调用了SecurityConfig.createList(attribute)来做的
public static ListcreateList(String... attributeNames) { Assert.notNull(attributeNames, "You must supply an array of attribute names"); List attributes = new ArrayList ( 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> createRequestMap() { if (unmappedMatchers != null) { throw new IllegalStateException( "An incomplete mapping was found for " + unmappedMatchers + ". Try completing it with something like requestUrls(). .hasRole('USER')"); } LinkedHashMap > requestMap = new LinkedHashMap >(); for (UrlMapping mapping : getUrlMappings()) { RequestMatcher matcher = mapping.getRequestMatcher(); Collection 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> getDecisionVoters(H http) { List > decisionVoters = new ArrayList >(); WebExpressionVoter expressionVoter = new WebExpressionVoter(); expressionVoter.setExpressionHandler(getExpressionHandler(http)); decisionVoters.add(expressionVoter); return decisionVoters; }
因为用的是WebExpressionVoter,所以在之前创建SecurityMetadataSource时需要做一次转换。
各种AccessDecisionManager实现类的具体意义我们在讨论鉴权的时候再具体分析。
这样我们的FilterSecurityInterceptor就完全组装好了,并且也作为Filter追加到了servlet中,可以对我们的资源进行保护了。