🔔 前提
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
DelegatingFilterProxy
是 Spring Framework
提供的一个过滤器,它充当一个代理,将请求委托给目标过滤器来处理。让我们来分析一下 DelegatingFilterProxy
的源码。
接下来,探究下实例化 DelegatingFilterProxy
对象时,DelegatingFilterProxy
做了什么。
1 类结构
先看下 DelegatingFilterProxy
类的继承链
通过类图,可以看到 DelegatingFilterProxy
继承了 Filter
接口,最终生成了一个过滤器。
2 类属性
摘出一段源码
public class DelegatingFilterProxy extends GenericFilterBean {
@Nullable
private String contextAttribute;
@Nullable
private String targetBeanName;
private boolean targetFilterLifecycle = false;
// ...
}
根据上面代码,看到了熟悉的 targetBeanName
和 targetFilterLifecycle
参数。如果需要配置这些参数,示例如下:
🚀:示例二
<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
调用。