Spring Security的加载过程

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的功能,最主要的就是扩展它的拦截器了。

好了,啰啰嗦嗦写了这么多,今天的分析就到这儿吧,水平薄弱,分析的难免会有些问题,欢迎指正。之后有时间我们再从一些实用性的角度继续分析。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值