Spring Security是我一直想要研究的一个开源项目,之前也尝试着用它做个几个小小的demo,但是总觉得不得要领。所以干脆从源码入手,来分析一下它的加载过程。
一、DelegatingFilterProxy
Spring Security整合到项目中,首先要做的就是配置web.xml:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
filter的声明,我们应该已经很熟悉了,但是新加入的这个filter class却貌似并不是spring security下的类,这不是很奇怪么?
带着这个问题,我们来看下DelegatingFilterProxy。
1.定义
org.springframework.web.filter中有一个特殊的类——DelegatingFilterProxy,该类其实并不能说是一个过滤器,它的原型是FilterToBeanProxy,即:将Filter作为spring的bean,由spring来管理。
配置DelegatingFilterProxy的常用方法如下所示:
<filter>
<filter-name>testFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>testBean</param-value>
</init-param>
<init-param>
<param-name>contextAttribute</param-name>
<param-value>session</param-value>
</init-param>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>testFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
下面对上述的配置做一些解释:
初始化属性init-param
1. targetBeanName:DelegatingFilterProxy将会委托这个Bean来执行拦截或者过滤。如果没有配置targetBeanName,那么默认会以filter-name来作为targetBeanName。
2. contextAttribute:被委托的Filter的作用范围,其值必须从org.springframework.context.ApplicationContext.WebApplicationContext中取得,默认值是session;
3. targetFilterLifecycle:这个厉害了,可以控制targetBeanName所对应的那个filter是否会经历init和destory。(默认是false,如果没有指定是不会执行初始化和销毁动作的,这个一定要注意)
2.示例
我们自己来写一个小例子验证一下上面的内容吧,为了方便理解,尽量写成了和Spring Security配置相同的形式:
@Service
public class ForDelegateFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter init");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filter doing");
}
public void destroy() {
System.out.println("filter destroy");
}
}
web.xml配置:
<filter>
<filter-name>forDelegateFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>forDelegateFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
随便写了一个Controller:
@Controller
@RequestMapping("/")
public class BasicController {
@RequestMapping("hello")
@ResponseBody
public String sayHello() {
return "Hello";
}
}
扫描包的配置就省略不写了。
运行后,调用/hello
接口,控制台输出如下:
filter doing
如果我们改web.xml如下:
<filter>
<filter-name>forDelegateFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>forDelegateFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
则运行结果如下:
18:34:29.786 [main] DEBUG o.s.web.filter.DelegatingFilterProxy - Initializing filter 'forDelegateFilter'
18:34:29.792 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'forDelegateFilter'
18:34:29.792 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean 'forDelegateFilter'
18:34:29.792 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'forDelegateFilter' to allow for resolving potential circular references
18:34:29.796 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'forDelegateFilter'
filter init
...
[INFO] Started Jetty Server
filter doing
经过上面的分析,我们已经明白了DelegatingFilterProxy的作用和使用方法,所以Spring Security所委托的Filter也就呼之欲出了,正是springSecurityFilterChain。
二、SpringSecurityFilterChain的定义与注册
现在你一定和我一样,信心满满的准备找到SpringSecurityFilterChain这个类,然后分析它了吧。然而当你查找这个类的时候,你可能要失望了,因为根本就没发现任何关于这个类的痕迹啊。
看来,只有一种可能了,bean的名字是springSecurityFilterChain,但其实它的类名并不是这个。肿么办?我们从配置开始看起,来找一下能否嗅探到springSecurityFilterChain这个bean的一些信息。
1.Spring Security 的配置
先看一下spring security最简单的一种配置方式:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
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-3.0.xsd">
<http auto-config="true">
<intercept-url pattern="/*" access="ROLE_USER"/>
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider>
<user-service>
<user authorities="ROLE_USER" name="guest" password="guest"/>
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
首先需要说明一下,有关于spring security的配置,是spring-security-config的相关Jar包提供的。根据spring.handlers中定义的Handler。我们知道这些配置其实是由SecurityNamespaceHandler来解析处理的。
2.Security命名空间的解析
上面已经提到过,Security命名空间使用SecurityNaqmespaceHandler来进行解析。
SecurityNamespaceHandler继承于spring的NamespaceHandler接口。该接口提供三个方法:
public interface NamespaceHandler {
//用于自定义标签的初始化
void init();
//用于解析标签
BeanDefinition parse(Element element, ParserContext parserContext);
//用于装饰
BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);
}
那我们一起来看下SecurityNamespaceHandler是怎么初始化的(init):
public void init() {
// Parsers
parsers.put(Elements.LDAP_PROVIDER, new LdapProviderBeanDefinitionParser());
parsers.put(Elements.LDAP_SERVER, new LdapServerBeanDefinitionParser());
parsers.put(Elements.LDAP_USER_SERVICE, new LdapUserServiceBeanDefinitionParser());
parsers.put(Elements.USER_SERVICE, new UserServiceBeanDefinitionParser());
parsers.put(Elements.JDBC_USER_SERVICE, new JdbcUserServiceBeanDefinitionParser());
parsers.put(Elements.AUTHENTICATION_PROVIDER, new AuthenticationProviderBeanDefinitionParser());
parsers.put(Elements.GLOBAL_METHOD_SECURITY, new GlobalMethodSecurityBeanDefinitionParser());
parsers.put(Elements.AUTHENTICATION_MANAGER, new AuthenticationManagerBeanDefinitionParser());
// registerBeanDefinitionDecorator(Elements.INTERCEPT_METHODS, new InterceptMethodsBeanDefinitionDecorator());
// Only load the web-namespace parsers if the web classes are available
if (ClassUtils.isPresent("org.springframework.security.web.FilterChainProxy", getClass().getClassLoader())) {
parsers.put(Elements.HTTP, new HttpSecurityBeanDefinitionParser());
parsers.put(Elements.FILTER_INVOCATION_DEFINITION_SOURCE, new FilterInvocationSecurityMetadataSourceParser());
parsers.put(Elements.FILTER_SECURITY_METADATA_SOURCE, new FilterInvocationSecurityMetadataSourceParser());
filterChainMapBDD = new FilterChainMapBeanDefinitionDecorator();
//registerBeanDefinitionDecorator(Elements.FILTER_CHAIN_MAP, new FilterChainMapBeanDefinitionDecorator());
}
}
看起来很简单,就是把各种标签和其Parser的对应关系缓存起来,我们可以理解这一步为注册parser。例如,http标签,注册了使用HttpSecurityBeanDefinitionParser来解析。
public BeanDefinition parse(Element element, ParserContext pc) {
if (!namespaceMatchesVersion(element)) {
pc.getReaderContext().fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or spring-security-3.1.xsd schema " +
"with Spring Security 3.2. Please update your schema declarations to the 3.2 schema.", element);
}
String name = pc.getDelegate().getLocalName(element);
BeanDefinitionParser parser = parsers.get(name);
...
return parser.parse(element, pc);
}
parse的方法就是这么简单,之前注册了parser,这一步就是拿到parser,然后用parser的parse方法解析。
3.HttpSecurityBeanDefinitionParser和http标签的解析
我们通过HttpSecurityBeanDefinitionParser来了解一下Parser到底是怎么执行解析的:
public BeanDefinition parse(Element element, ParserContext pc) {
CompositeComponentDefinition compositeDef =
new CompositeComponentDefinition(element.getTagName(), pc.extractSource(element));
pc.pushContainingComponent(compositeDef);
registerFilterChainProxyIfNecessary(pc, pc.extractSource(element));
// Obtain the filter chains and add the new chain to it
BeanDefinition listFactoryBean = pc.getRegistry().getBeanDefinition(BeanIds.FILTER_CHAINS);
List<BeanReference> filterChains = (List<BeanReference>)
listFactoryBean.getPropertyValues().getPropertyValue("sourceList").getValue();
filterChains.add(createFilterChain(element, pc));
pc.popAndRegisterContainingComponent();
return null;
}
我们来一步一步的分析这个方法:
第一行,新建了一个CompositeComponentDefinition。
CompositeComponentDefinition是ComponentDefinition的子类,ComponentDefinition用来描述“对组件定义”,而CompositeComponentDefinition则是包含了一个或多个多层嵌套的ComponentDefinition实例,将它们聚合在一起,成为命名的组件组。既然是一个命名的组件组,那么肯定要有一个name属性,还要有一个List来存放ComponentDefinition。这些CompositeComponentDefinition也确实定义了,除此之外,还有一个scource,它用来表示所有这些组件的根元素。提供添加嵌套组件或者获取嵌套组件的方法。由此可以看出, CompositeComponentDefinition定义一种多级的嵌套关系,如果一个组件里包含另外一个组件的话,那么外层的组件肯定是一个 CompositeComponentDefinition,而里层的则是ComponentDifinition,至于是哪个实现,这就要看里层的组件是不是也有嵌套的组件了。
这里新建的这个CompositeComponentDefinition,是http标签对应的ComponentDefinition,name是”http”,source是pc.extractSource(element)从element中提取出来的一个对象,应该是根节点元素。具体是什么,怎么获取到的,我们后面再分析。
第二行,向ParserContext中push了我们刚刚创建的compositeDef。这一步具体做了什么,主要就是ParserContext中维护了一个ComponentDifinition的列表,而pushContainingComponent就是把compositeDef加入到这个列表中去。
第三行,registerFilterChainProxyIfNecessary(pc, pc.extractSource(element)); 这个是重点了,如果有必要的话就注册一个FilterChainProxy。
紧接着,第4-6行,从Registry中取出了filterChains的BeanDefinition。然后通过这个BeanDefinition获取到了filterChains这个列表。然后createFilterChain(element,pc)来创建一个新的filter的BeanDefinition并且把这个Filter的BeanDifinition也加入到了filterChains里。
最后第七行,从ParserContext里又pop出了compositeDef,并且将其注册。
第八行,reutrn null。
这里面有几个疑问:
第一,registerFilterChainProxyIfNecessary,如果需要就注册,那么什么情况下就是需要呢?注册又是注册什么,注册到哪里呢?
第二,createFilterChain,到底是如何创建FilterChain的呢?
第三,pc.popAndRegisterContainingComponent(),同样也涉及到一个注册的问题,注册什么,又要注册到哪里呢?
第四,为什么最后return null,而不是return一个具体的BeanDefinition?
第五,(补充一个)我们使用Parser进行解析,解析的是什么,解析的结果是什么?
带着这些疑问,我们再深入去看一下parse()的代码。
4.深入理解http标签解析过程
首先registerFilterChainProxyIfNecessary(pc, pc.extractSource(element));
(1)registerFilterChainProxyIfNecessary
先上源码:
static void registerFilterChainProxyIfNecessary(ParserContext pc, Object source) {
if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) {
return;
}
// Not already registered, so register the list of filter chains and the FilterChainProxy
BeanDefinition listFactoryBean = new RootBeanDefinition(ListFactoryBean.class);
listFactoryBean.getPropertyValues().add("sourceList", new ManagedList());
pc.registerBeanComponent(new BeanComponentDefinition(listFactoryBean, BeanIds.FILTER_CHAINS));
BeanDefinitionBuilder fcpBldr = BeanDefinitionBuilder.rootBeanDefinition(FilterChainProxy.class);
fcpBldr.getRawBeanDefinition().setSource(source);
fcpBldr.addConstructorArgReference(BeanIds.FILTER_CHAINS);
fcpBldr.addPropertyValue("filterChainValidator", new RootBeanDefinition(DefaultFilterChainValidator.class));
BeanDefinition fcpBean = fcpBldr.getBeanDefinition();
pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY));
pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN);
}
从ParserContext的Registry中寻找是否有filterChainProxy这个BeanDefinition。有就证明已经创建过了,可以return了。
没用就需要注册了,注册filterChainProxy和filterChains。
首先注册了listFactoryBean,有一个名为sourceList的List,作为listFactoryBean的PropertyValues。然后以filterChains为name,注册了这个listFactoryBean。
然后又创建了一个fcpBean,其实就是filter chain proxy bean。说是bean,因为现在还在spring的初始化阶段,所有的bean在这个阶段都是被表示成BeanDefinition的。所以这个fcpBean其实和listFactoryBean一样,也是一个BeanDefinition。这个Bean的类型是FilterChainProxy。跟进去瞅一眼,是一个Filter。既是Filter又是一个Proxy,看来需要重点关照一下了。继续往下看,以filterChainProxy为名注册了这个fcpBean。
fcpBean有一个成员叫做filterChainValidator,看名字是个filterChain的验证器。
BTW:这里插一点,BeanDefinition是对Bean的状态的一种持有,在特定的时候就可以使用BeanDefinition来创建一个Bean的实例。也就是说,BeanDeifinition里有创建一个Bean所需要的所有的信息。之前我们多次看到了PropertyValue,其实这个列表里存的就是Bean的成员变量的值;而对应的还有ConstructorArg,也就是构造方法的参数值列表。
最后,又给filterChainProxy注册了一个别名叫springSecurityFilterChain。
啥?!springSecurityFilterChain?!这不就是我们苦苦找寻的那个Bean吗?幸福来得太突然,没有一点点防备……
不过,上面还有很多问题没有解决呢,所以不要太过激动。Mark一下,然后下面我们来看createFilterChain方法。
(2)createFilterChain
上代码:
private BeanReference createFilterChain(Element element, ParserContext pc) {
boolean secured = !OPT_SECURITY_NONE.equals(element.getAttribute(ATT_SECURED));
if (!secured) {
if (!StringUtils.hasText(element.getAttribute(ATT_PATH_PATTERN)) &&
!StringUtils.hasText(ATT_REQUEST_MATCHER_REF)) {
pc.getReaderContext().error("The '" + ATT_SECURED + "' attribute must be used in combination with" +
" the '" + ATT_PATH_PATTERN +"' or '" + ATT_REQUEST_MATCHER_REF + "' attributes.",
pc.extractSource(element));
}
for (int n=0; n < element.getChildNodes().getLength(); n ++) {
if (element.getChildNodes().item(n) instanceof Element) {
pc.getReaderContext().error("If you are using <http> to define an unsecured pattern, " +
"it cannot contain child elements.", pc.extractSource(element));
}
}
return createSecurityFilterChainBean(element, pc, Collections.emptyList());
}
final BeanReference portMapper = createPortMapper(element, pc);
final BeanReference portResolver = createPortResolver(portMapper, pc);
ManagedList<BeanReference> authenticationProviders = new ManagedList<BeanReference>();
BeanReference authenticationManager = createAuthenticationManager(element, pc, authenticationProviders);
HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, pc,
portMapper, portResolver, authenticationManager);
AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, pc,
httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
httpBldr.getSessionStrategy(), portMapper, portResolver, httpBldr.getCsrfLogoutHandler());
httpBldr.setLogoutHandlers(authBldr.getLogoutHandlers());
httpBldr.setEntryPoint(authBldr.getEntryPointBean());
httpBldr.setAccessDeniedHandler(authBldr.getAccessDeniedHandlerBean());
authenticationProviders.addAll(authBldr.getProviders());
List<OrderDecorator> unorderedFilterChain = new ArrayList<OrderDecorator>();
unorderedFilterChain.addAll(httpBldr.getFilters());
unorderedFilterChain.addAll(authBldr.getFilters());
unorderedFilterChain.addAll(buildCustomFilterList(element, pc));
Collections.sort(unorderedFilterChain, new OrderComparator());
checkFilterChainOrder(unorderedFilterChain, pc, pc.extractSource(element));
// The list of filter beans
List<BeanMetadataElement> filterChain = new ManagedList<BeanMetadataElement>();
for (OrderDecorator od : unorderedFilterChain) {
filterChain.add(od.bean);
}
return createSecurityFilterChainBean(element, pc, filterChain);
}
有点长,我们拆分一下,上来就有一个判断,判断http标签是否定义了security属性。如果security=none。则定义了当前不处于secured状态,如果没有指定,则默认就是secured状态。
所以下面就根据是否处于secured状态分为了两种情况去创建FilterChain。
先看不处于secured状态的情况:
if (!StringUtils.hasText(element.getAttribute(ATT_PATH_PATTERN)) &&
!StringUtils.hasText(ATT_REQUEST_MATCHER_REF)) {
pc.getReaderContext().error("The '" + ATT_SECURED + "' attribute must be used in combination with" +
" the '" + ATT_PATH_PATTERN +"' or '" + ATT_REQUEST_MATCHER_REF + "' attributes.",
pc.extractSource(element));
}
for (int n=0; n < element.getChildNodes().getLength(); n ++) {
if (element.getChildNodes().item(n) instanceof Element) {
pc.getReaderContext().error("If you are using <http> to define an unsecured pattern, " +
"it cannot contain child elements.", pc.extractSource(element));
}
}
return createSecurityFilterChainBean(element, pc, Collections.emptyList());
StringUtils的hasText方法实际上检测的是字符串中有无实际有意义的值,null和空字符串还有只包含空格的字符串都被认为是没有意义的。
/**
* StringUtils.hasText(null) = false
* StringUtils.hasText("") = false
* StringUtils.hasText(" ") = false
* StringUtils.hasText("12345") = true
* StringUtils.hasText(" 12345 ") = true
*/
当pattern属性为空并且ATT_REQUEST_MATCHER_REF也为空的时候,会产生错误信息。这里这个条件是永远不可能成立的,因为ATT_REQUEST_MATCHER_REF总是有值的。我不清楚这是不是一个bug。按照我的推理,它应该是想要检测是否有ATT_REQUEST_MATCHER_REF这个属性的值才对。
其后是一个验证和容错的过程。按照约定,使用unsecured模式的http标签是不能含有字标签的,这个在编写xml时也有DTD来约束,但是为了保险起见,同时也在程序中进行判断,如果http标签在unsecured模式下有子标签,则产生错误信息。注意这里的错误信息都是提交给了ReaderContext的error方法。如果可以,后续会分析ReaderContext的错误和异常处理方式。
unsecured模式的最后,是调用了createSecurityFilterChainBean()方法。我们下面会分析;
private BeanReference createSecurityFilterChainBean(Element element, ParserContext pc, List<?> filterChain) {
BeanMetadataElement filterChainMatcher;
String requestMatcherRef = element.getAttribute(ATT_REQUEST_MATCHER_REF);
String filterChainPattern = element.getAttribute(ATT_PATH_PATTERN);
if (StringUtils.hasText(requestMatcherRef)) {
if (StringUtils.hasText(filterChainPattern)) {
pc.getReaderContext().error("You can't define a pattern and a request-matcher-ref for the " +
"same filter chain", pc.extractSource(element));
}
filterChainMatcher = new RuntimeBeanReference(requestMatcherRef);
} else if (StringUtils.hasText(filterChainPattern)) {
filterChainMatcher = MatcherType.fromElement(element).createMatcher(filterChainPattern, null);
} else {
filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class);
}
BeanDefinitionBuilder filterChainBldr = BeanDefinitionBuilder.rootBeanDefinition(DefaultSecurityFilterChain.class);
filterChainBldr.addConstructorArgValue(filterChainMatcher);
filterChainBldr.addConstructorArgValue(filterChain);
BeanDefinition filterChainBean = filterChainBldr.getBeanDefinition();
String id = element.getAttribute("name");
if (!StringUtils.hasText(id)) {
id = element.getAttribute("id");
if (!StringUtils.hasText(id)) {
id = pc.getReaderContext().generateBeanName(filterChainBean);
}
}
pc.registerBeanComponent(new BeanComponentDefinition(filterChainBean, id));
return new RuntimeBeanReference(id);
}
BeanMetadataElement是一个接口,bean metadata element是bean的原始数据元素,它含有一个配置元对象。这里又要获取两个我们总是在提到的属性,索性我把这两个属性的配置所代表的意义在这里说明一下:
- request-matcher-ref: 设置需要处理的request的匹配器。
- pattern: 当前http元素的对应filter chain需要处理的request url, 依赖于 request-matcher的配置; 如果没有配置, 所有的request都将匹配, 最具体的pattern 应该放置在最前面。
所以这里上来应该是一个创建requestMatcher的过程。request-matcher的创建和两个xml中配置的属性相关,一个是request-matcher-ref
,另一个是pattern
。他们决定了filterChain需要处理的request的范围。
所以,你可能也猜到了,parrtern和request-matcher-ref它两个是不能共存的。所以程序上来就先做了判断。如果说request-matcher-ref存在,创建的时RuntimeBeanReference,而如果是pattern存在,则是:
filterChainMatcher = MatcherType.fromElement(element).createMatcher(filterChainPattern, null);
如果是空的,则创建一个AnyRequestMatcher
的BeanDefinition。
filterChainBldr是BeanDefinitionBuilder的实例,filterChainBldr要创建的是DefaultSecurityFilterChain的实例。Bldr构建出filterChainBean,设置id,然后注册到ParseContext的上下文中。整个创建FilterChain的过程算是已经结束了。最终会返回一个RuntimeBeanReference对象。
RuntimeBeanReference对象是一个不可变的占位符,用来在运行时解析时作为属性的占位符。例如我有一个School的bean,还有一个Student的bean,在运行时解析了这两个bean的配置。如果School中要使用到Student这个bean,那么只要返回一个Student的RuntimeBeanReference(stdId)对象即可。正所谓BeanReference,正是对Bean的引用。而无需Bean本身或者BeanDefinition。(其实在这个解析期间,还只是BeanDefinition,并不存在Bean)
这里我们研究的是unsecured模式,那么在这种情况下,传入进来的filterChain列表是空的,也就是说不进行任何拦截。
secured模式
下面看secured的模式吧:
PortMapper:这里是用PortMapper的解析器对PortMapper标签进行解析。在http标签中可以嵌套有port-mappers标签。
默认情况下,会创建一个PortMapperImpl的实例,用于重定向安全和不安全的URLs,需要指定HTTP端口:HTTPS端口。
默认的mappings是80:443和8080:8443。
PortResolver:用于解析PortMapper对象的工具对象。
ManagedList其实是ArrayList的子类,定义在spring-beans中。
authenticationManager是authentication-manager-ref对应的bean
简单说下创建过程
利用标签解析创建portMapper,依据上下文和portMapper创建对应的解析器。
创建httpBldr和AuthenticationBldr ,最终会从这两个builder中取出filter并创建unorderedFilterChain
Collections.sort(unorderedFilterChain, new OrderComparator()); 按照优先级对filters进行排序;
最后是把OrderDecorator中的bean依次取出后,放在了filterChain中,最后也是调用了createSecurityFilterChainBean方法。
经过上面的分析,之前的那几个疑惑点也都迎刃而解了,我们来总结下:
第一,registerFilterChainProxyIfNecessary,如果需要就注册,那么什么情况下就是需要呢?注册又是注册什么,注册到哪里呢?
答:注册的就是我们一直想找的springSecurityFilterChain。在springSecurityFilterChain之前还没有被注册过的情况下需要注册。
第二,createFilterChain,到底是如何创建FilterChain的呢?
答:这个问题分以下几步:
1.在注册springSecurityFilterChain时,先注册了FILTER_CHAINS。
FILTER_CHAINS和SPRING_SECURITY_FILTER_CHAIN有什么关系呢?后者是前者的代理类。实际的逻辑由前者执行。
2.注册FILTER_CHAINS之后,将创建的filterChain添加到FILTER_CHAINS中。问题中所说的filterChain创建过程就发生在这里。
3.创建FilterChain同时也是解析HTTP标签的过程,分为安全模式和非安全模式两种情况。
非安全模式:
非安全模式必须搭配pattern或者request-matcher-ref其中的一种来使用,且http标签内不能再嵌套任何子标签。
创建出的FilterChain实质上是不包含任何filter的。
安全模式:
安全模式是我们常用的模式,它会解析http标签及其子标签,及AuthenticationConfigBuilder。最终从HttpConfigurationBuilder和AuthenticationConfigBuilder中取出Filters拼装成filterChain列表,最终生成filterChainBean的BeanDefinition,并且注册到ParserContext中。
第三,pc.popAndRegisterContainingComponent(),同样也涉及到一个注册的问题,注册什么,又要注册到哪里呢?
答:pop的是parserContext中的containingComponents栈内数据,放的都是ComponentDefinition。最终调用fireComponentRegistered,通知组件已被注册事件。
这里应该是使用栈维持了一种组件间的嵌套关系。具体和Spring Security也没太大关系,容我以后再分析。
第四,为什么最后return null,而不是return一个具体的BeanDefinition?
答:该注册的都已经注册了,return什么已经不重要了。
第五,(补充一个)我们使用Parser进行解析,解析的是什么,解析的结果是什么?
答:我们使用parser进行解析,解析的http标签,解析的结果就是生成了springSecurityFilterChain并且注册到了ParserContext中。
三、继续追踪SpringSecurityFilterChain
追踪之前,先来看个DefaultSecurityFilterChain的代码热热身,DefaultSecurityFilterChain对应的是filterChainBean,一个SpringSecurityFilterChain中包含了多个DefaultSecurityFilterChain。
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private static final Log logger = LogFactory.getLog(DefaultSecurityFilterChain.class);
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
this.requestMatcher = requestMatcher;
this.filters = new ArrayList<Filter>(filters);
}
public RequestMatcher getRequestMatcher() {
return requestMatcher;
}
public List<Filter> getFilters() {
return filters;
}
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
@Override
public String toString() {
return "[ " + requestMatcher + ", " + filters + "]";
}
}
这个代理类很简单,一个reqeustMatcher,一个List。可以通过getFilters方法拿到filter的列表。
下面正式开始对FilterChainProxy的分析。简单起见,我们开门见山直接上doFilter方法。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if(clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
} else {
doFilterInternal(request, response, chain);
}
}
从request里取出属性FILTER_APPLIED,也就是org.springframework.security.web.FilterChainProxy.APPLIED 如果为空,则清理上下文。
if 清理 :设置request属性FILTER_APPLIED为TRUE,然后doFilterInternal。最后SecurityContextHolder.clearContext()清除上下文。再移除FILTER_APPLIED属性。
if 不清理:直接doFilterInternal
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest fwRequest = firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest) +
(filters == null ? " has no matching filters" : " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
List<Filter> filters = getFilters(fwRequest);
这一句,就是用requestMatcher逐一去匹配Request了。把需要对此Request进行拦截的Filter都找出来。这里的FilterChains是在Proxy构造的时候就已经传入进去的了,最终会把这些filter和doFilter方法的chain混合在一起,形成一个VirtualFilterChain。
private static class VirtualFilterChain implements FilterChain {
private final FilterChain originalChain;
private final List<Filter> additionalFilters;
private final FirewalledRequest firewalledRequest;
private final int size;
private int currentPosition = 0;
private VirtualFilterChain(FirewalledRequest firewalledRequest, FilterChain chain, List<Filter> additionalFilters) {
this.originalChain = chain;
this.additionalFilters = additionalFilters;
this.size = additionalFilters.size();
this.firewalledRequest = firewalledRequest;
}
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (currentPosition == size) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
+ " reached end of additional filter chain; proceeding with original chain");
}
// Deactivate path stripping as we exit the security filter chain
this.firewalledRequest.reset();
originalChain.doFilter(request, response);
} else {
currentPosition++;
Filter nextFilter = additionalFilters.get(currentPosition - 1);
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(firewalledRequest) + " at position " + currentPosition + " of "
+ size + " in additional filter chain; firing Filter: '"
+ nextFilter.getClass().getSimpleName() + "'");
}
nextFilter.doFilter(request, response, this);
}
}
}
可以看出它是分了原始Chain和附加Filters,原始Chain好说,它是符合Servlet-API的,附加的Filters怎么办呢?它在类的内部维护了一个currentPosition,用于标记当前已经执行到第几个附加拦截器了。然后每次从列表中取出第n个filter,执行nextFilter.doFilter(request, response, this);
,即可。
从这里可以看出,FilterChainProxy也没有什么难的。也就是对附加Filters的一个处理技巧吧。
debug一下,让我们来看看filters都有些什么吧。
竟然有这么多的拦截器,仔细看下,可以发现它们涵盖了授权和验证的各个环节。
现在,我又有一个疑问了,这些拦截器是什么时候产生的?如果我要向这个列表里再添加一个拦截器,应该如何添加呢?带着这个问题,我们继续向下看。
四、Security中Filter的产生与自定义
1. 简要分析
上面我们提到过的,在HttpSecurityBeanDefinitionParser中我们创建Security Filter Chain的Bean之前,为了收集filterChain,做过这样一个操作:
unorderedFilterChain.addAll(httpBldr.getFilters());
unorderedFilterChain.addAll(authBldr.getFilters());
unorderedFilterChain.addAll(buildCustomFilterList(element, pc));
我们debug一下,结果如下:
刚好和上面FilterChainProxy里debug的结果相匹配。good~
那还剩下最后一句看名字也知道是创建自定义的Filter列表了。
2. 自定义Filter
先上源码:
List<OrderDecorator> buildCustomFilterList(Element element, ParserContext pc) {
List<Element> customFilterElts = DomUtils.getChildElementsByTagName(element, Elements.CUSTOM_FILTER);
List<OrderDecorator> customFilters = new ArrayList<OrderDecorator>();
final String ATT_AFTER = "after";
final String ATT_BEFORE = "before";
final String ATT_POSITION = "position";
for (Element elt: customFilterElts) {
String after = elt.getAttribute(ATT_AFTER);
String before = elt.getAttribute(ATT_BEFORE);
String position = elt.getAttribute(ATT_POSITION);
String ref = elt.getAttribute(ATT_REF);
if (!StringUtils.hasText(ref)) {
pc.getReaderContext().error("The '" + ATT_REF + "' attribute must be supplied", pc.extractSource(elt));
}
RuntimeBeanReference bean = new RuntimeBeanReference(ref);
if(WebConfigUtils.countNonEmpty(new String[] {after, before, position}) != 1) {
pc.getReaderContext().error("A single '" + ATT_AFTER + "', '" + ATT_BEFORE + "', or '" +
ATT_POSITION + "' attribute must be supplied", pc.extractSource(elt));
}
if (StringUtils.hasText(position)) {
customFilters.add(new OrderDecorator(bean, SecurityFilters.valueOf(position)));
} else if (StringUtils.hasText(after)) {
SecurityFilters order = SecurityFilters.valueOf(after);
if (order == SecurityFilters.LAST) {
customFilters.add(new OrderDecorator(bean, SecurityFilters.LAST));
} else {
customFilters.add(new OrderDecorator(bean, order.getOrder() + 1));
}
} else if (StringUtils.hasText(before)) {
SecurityFilters order = SecurityFilters.valueOf(before);
if (order == SecurityFilters.FIRST) {
customFilters.add(new OrderDecorator(bean, SecurityFilters.FIRST));
} else {
customFilters.add(new OrderDecorator(bean, order.getOrder() - 1));
}
}
}
return customFilters;
}
首先查找http标签里有没有custom-filter子标签(可以多个)
然后分别获取after、before和position属性值,再获取ref属性值。这些属性的主要作用是将ref装饰起来,使之拥有优先级。以便之后可以进行排序。
也就是说,自定义Filter其实很简单,我们只需要写好Filter,然后通过custom-filter标签配置即可。
五、总结
我们从web.xml中的配置和Spring的配置两个位置着手,分析了spring加载时,http标签的解析过程,以及filterChainProxy在执行拦截时所采取的动作。可以看出,Spring Security的核心其实就是由一系列的Filter组成的FilterChain。如果我们想扩展Spring Security的功能,最主要的就是扩展它的拦截器了。
好了,啰啰嗦嗦写了这么多,今天的分析就到这儿吧,水平薄弱,分析的难免会有些问题,欢迎指正。之后有时间我们再从一些实用性的角度继续分析。