Spring-Security | 过滤器注册细节梳理第一篇

335 阅读7分钟

🔔 前提

1 使用版本:

  • Java 版本:17
  • Spring 版本:6.0.9
  • Spring security 版本:6.1.0

2 开发模式:

为了快速研究,使用了最简单的基于 XML 配置的 spring 应用,没有使用 spring boot

第一篇:spring-security 在 web.xml 中配置详解

摘要:本篇主要是搞清楚 spring-security 在 web.xml 中配置如何生效,或者其实就是搞清楚 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>

上面的配置,简单说明下:

  • 是生成一个名字为 springSecurityFilterChain,类型为 DelegatingFilterProxy 类型的实例,然后这个实例会自动加入到 Servlet 容器的过滤器链中。
  • 过滤器路由映射所欲的 URL,即当有请求达到 Servlet 容器时,会依次按照过滤器声明和映射的顺序,依次执行过滤器的逻辑。

神奇的 DelegatingFilterProxy

DelegatingFilterProxySpring Framework 提供的一个过滤器,它充当一个代理,将请求委托给目标过滤器来处理。让我们来分析一下 DelegatingFilterProxy 的源码。

接下来,探究下实例化 DelegatingFilterProxy 对象时,DelegatingFilterProxy 做了什么。

1 类结构

先看下 DelegatingFilterProxy 类的继承链

截屏2023-06-20 下午2.36.55.png

通过类图,可以看到 DelegatingFilterProxy 继承了 Filter 接口,最终生成了一个过滤器。

2 类属性

摘出一段源码

public class DelegatingFilterProxy extends GenericFilterBean {

	@Nullable
	private String contextAttribute;

	@Nullable
	private String targetBeanName;

	private boolean targetFilterLifecycle = false;

	// ...
}

根据上面代码,看到了熟悉的 targetBeanNametargetFilterLifecycle 参数。如果需要配置这些参数,示例如下:

🚀:示例二

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>springSecurityFilterChain</param-value>
        </init-param>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

简单讲下这些参数的作用:

  • targetBeanName: 字符串

targetBeanName 参数是在 DelegatingFilterProxy 的构造函数中使用的一个字符串参数。它用于指定要代理的目标过滤器的 bean 名称。

DelegatingFilterProxy 接收到请求时,它将查找 Spring 容器中与 targetBeanName 参数匹配的目标过滤器的 bean。然后,DelegatingFilterProxy 将委托实际的过滤工作给找到的目标过滤器实例。

通过使用 targetBeanName 参数,你可以指定要使用的目标过滤器的 bean 名称,而不需要直接引用目标过滤器的类或实例。

示例二中,我们将 DelegatingFilterProxy 配置为过滤器,并通过 <init-param> 元素指定了 targetBeanName 参数的值为 springSecurityFilterChain。这意味着 DelegatingFilterProxy 将在 Spring 容器中查找名为 springSecurityFilterChain 的目标过滤器的 bean,并将实际的过滤工作委托给它。

请注意,springSecurityFilterChain 应该替换为实际的目标过滤器的 bean 名称。该名称必须与 Spring 容器中定义的目标过滤器的 bean 名称匹配。

  • targetFilterLifecycle: 布尔

targetFilterLifecycle 参数是在 DelegatingFilterProxy 的构造函数中使用的一个布尔值参数。它用于指定是否由 DelegatingFilterProxy 负责管理目标过滤器的生命周期。

如果将 targetFilterLifecycle 参数设置为 true,则 DelegatingFilterProxy 将负责调用目标过滤器的 init()destroy() 方法。这意味着 DelegatingFilterProxy 将在容器启动时自动调用目标过滤器的 init() 方法,并在容器关闭时调用目标过滤器的 destroy() 方法。

如果将 targetFilterLifecycle 参数设置为 false,则 DelegatingFilterProxy 将不会管理目标过滤器的生命周期。这意味着你需要手动调用目标过滤器的 init()destroy() 方法,确保它们在适当的时候被调用。

默认情况下,targetFilterLifecycle 参数被设置为 false,即 DelegatingFilterProxy 不会管理目标过滤器的生命周期。如果你希望 DelegatingFilterProxy 管理目标过滤器的生命周期,你可以将 targetFilterLifecycle 参数设置为 true

示例二中,通过配置将 targetFilterLifecycle 设置为 true,以便让 DelegatingFilterProxy 管理目标过滤器的生命周期。

3 构造函数

摘出一段源码

public class DelegatingFilterProxy extends GenericFilterBean {

	// ...


	public DelegatingFilterProxy() {
        
	}

	public DelegatingFilterProxy(Filter delegate) {
		Assert.notNull(delegate, "Delegate Filter must not be null");
		this.delegate = delegate;
	}


	public DelegatingFilterProxy(String targetBeanName) {
		this(targetBeanName, null);
	}

	public DelegatingFilterProxy(String targetBeanName, @Nullable WebApplicationContext wac) {
		Assert.hasText(targetBeanName, "Target Filter bean name must not be null or empty");
		this.setTargetBeanName(targetBeanName);
		this.webApplicationContext = wac;
		if (wac != null) {
			this.setEnvironment(wac.getEnvironment());
		}
	}

	// ...
}

接下来说说为什么 DelegatingFilterProxy 定义了多种构造函数,当然是为了满足适用各种场景。可能在使用 web.xml 配置时,感觉不到它的威力,如果使用 java 配置呢,例如:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // 创建 DelegatingFilterProxy 实例
        DelegatingFilterProxy delegatingFilterProxy = new DelegatingFilterProxy();
        delegatingFilterProxy.setTargetBeanName("myFilterBeanName");
        delegatingFilterProxy.setTargetFilterLifecycle(true);

        // 添加 DelegatingFilterProxy 到 FilterRegistration
        FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(
            "MyFilter", delegatingFilterProxy);
        filterRegistration.addMappingForUrlPatterns(
            EnumSet.allOf(DispatcherType.class), true, "/*");
    }

}

在上述示例中,在 onStartup 方法中创建了一个 DelegatingFilterProxy 实例,并设置了targetBeanName 参数和 targetFilterLifecycle 参数。然后,我们将 DelegatingFilterProxy 实例添加到 FilterRegistration 中,以便将其注册为过滤器。

请注意,myFilterBeanName 应该替换为实际的目标过滤器的bean名称,而 MyFilter 是你为过滤器指定的名称。此外,你还可以使用 filterRegistration.addMappingForServletNames 方法将过滤器与指定的Servlet名称关联起来,或使用 filterRegistration.addMappingForUrlPatterns 方法将过滤器与指定的URL模式关联起来。

这样配置后,DelegatingFilterProxy 将在请求到达时使用指定的目标过滤器进行处理,并在容器启动和关闭时管理目标过滤器的生命周期。

通过上面的示例可以看出,可以根据自己的需要调用适用的构造函数来满足自己的需求。

4 标准入口

标准过滤器函数三大件:init, doFilter, destroy,这三个函数也是我们最关心的函数,至于 DelegatingFilterProxy 的奥秘也是通过这几个函数体现出来,当然最重要的还是 init, doFilter

♻️ init


init 函数定义在 DelegatingFilterProxy 所继承的抽象类 GenericFilterBean 中,看下函数源码:

@Override
public final void init(FilterConfig filterConfig) throws ServletException {
        Assert.notNull(filterConfig, "FilterConfig must not be null");

        this.filterConfig = filterConfig;

        // Set bean properties from init parameters.
        PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
        if (!pvs.isEmpty()) {
                try {
                        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                        ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
                        Environment env = this.environment;
                        if (env == null) {
                                env = new StandardServletEnvironment();
                        }
                        bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
                        initBeanWrapper(bw);
                        bw.setPropertyValues(pvs, true);
                }
                catch (BeansException ex) {
                        String msg = "Failed to set bean properties on filter '" +
                                        filterConfig.getFilterName() + "': " + ex.getMessage();
                        logger.error(msg, ex);
                        throw new ServletException(msg, ex);
                }
        }

        // Let subclasses do whatever initialization they like.
        // 重点关注:🔥🔥🔥🔥🔥🔥
        initFilterBean();

        if (logger.isDebugEnabled()) {
                logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
        }
}

接着看下 initFilterBean 的实现,此函数实现定义为 DelegatingFilterProxy 中。

@Override
protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                        // If no target bean name specified, use filter name.
                        if (this.targetBeanName == null) {
                                this.targetBeanName = getFilterName();
                        }
                        // Fetch Spring root application context and initialize the delegate early,
                        // if possible. If the root application context will be started after this
                        // filter proxy, we'll have to resort to lazy initialization.
                        WebApplicationContext wac = findWebApplicationContext();
                        if (wac != null) {
                                // 重点关注:🔥🔥🔥🔥🔥🔥
                                this.delegate = initDelegate(wac);
                        }
                }
        }
}

protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        String targetBeanName = getTargetBeanName();
        Assert.state(targetBeanName != null, "No target bean name set");
        Filter delegate = wac.getBean(targetBeanName, Filter.class);
        if (isTargetFilterLifecycle()) {
                delegate.init(getFilterConfig());
        }
        return delegate;
}

可以看到 initFilterBean 最终生成的委托过滤器

  • 如果通过构造函数传递指定的过滤器对象,则使用传递进来的过滤器
  • 如果通过构造函数没有传递过滤器时,是通过调用 initDelegate 方法生成的。认真看下 initDelegate 源码,就会发现,哦,原来是这样,通过 spring 应用上下文找到对应的 bean,原来真相就是这么的简单

这样就了解了起作用的过滤器对象

♻️ doFilter


doFilter 函数实现定义为 DelegatingFilterProxy 中,直接贴出源码

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {

        // Lazily initialize the delegate if necessary.
        Filter delegateToUse = this.delegate;
        if (delegateToUse == null) {
                synchronized (this.delegateMonitor) {
                        delegateToUse = this.delegate;
                        if (delegateToUse == null) {
                                WebApplicationContext wac = findWebApplicationContext();
                                if (wac == null) {
                                        throw new IllegalStateException("No WebApplicationContext found: " +
                                                        "no ContextLoaderListener or DispatcherServlet registered?");
                                }
                                delegateToUse = initDelegate(wac);
                        }
                        this.delegate = delegateToUse;
                }
        }

        // Let the delegate perform the actual doFilter operation.
        // 重点关注:🔥🔥🔥🔥🔥🔥
        invokeDelegate(delegateToUse, request, response, filterChain);
}

protected void invokeDelegate(
                Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {

        delegate.doFilter(request, response, filterChain);
}

通过源码,发现过滤器的执行最终会交给委托过滤器来执行,看来 DelegatingFilterProxy 就是个包工头,它来拉生意,小弟们干活。

♻️ destroy


destroy 函数实现定义为 DelegatingFilterProxy 中,直接贴出源码

@Override
public void destroy() {
    Filter delegateToUse = this.delegate;
    if (delegateToUse != null) {
        destroyDelegate(delegateToUse);
    }
}

protected void destroyDelegate(Filter delegate) {
    if (isTargetFilterLifecycle()) {
        delegate.destroy();
    }
}

没啥说的了,还记得 targetFilterLifecycle 配置么,如果此配置设置为 true,则委托过滤器就要干活了,DelegatingFilterProxy 更像是个甩手掌柜。

5 疑问点

❓疑问点 1:DelegatingFilterProxy 是怎么和 spring 关联起来的

@Override
protected void initFilterBean() throws ServletException {
    synchronized (this.delegateMonitor) {
        if (this.delegate == null) {
            // If no target bean name specified, use filter name.
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }
            // Fetch Spring root application context and initialize the delegate early,
            // if possible. If the root application context will be started after this
            // filter proxy, we'll have to resort to lazy initialization.
            // 重点关注:🔥🔥🔥🔥🔥🔥
            WebApplicationContext wac = findWebApplicationContext();
            if (wac != null) {
                this.delegate = initDelegate(wac);
            }
        }
    }
}

protected WebApplicationContext findWebApplicationContext() {
    if (this.webApplicationContext != null) {
        // The user has injected a context at construction time -> use it...
        if (this.webApplicationContext instanceof ConfigurableApplicationContext cac && !cac.isActive()) {
            // The context has not yet been refreshed -> do so before returning it...
            cac.refresh();
        }
        return this.webApplicationContext;
    }
    String attrName = getContextAttribute();
    if (attrName != null) {
        return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    }
    else {
        return WebApplicationContextUtils.findWebApplicationContext(getServletContext());
    }
}

initFilterBean 方法中调用了 findWebApplicationContext,且看那 findWebApplicationContext,通过 WebApplicationContextUtils 获取对应的应用上下文,是不是看明白了。


❓疑问点 2:GenericFilterBean 继承的 Aware 接口有什么用

额,这个虽然说 DelegatingFilterProxy 继承了 GenericFilterBean,但是这些感知方法不会被调用,很简单,DelegatingFilterProxy 实例不是 spring bean 呗,你不受 spring 掌控,凭什么给你好处。

那给谁好处了呢,当然是谁继承 GenericFilterBean,并声明为 spring bean 时,就会通过 spring bean 的声明周期,享受各种 Aware 调用。