架构

本节讨论 Spring Security 在基于 Servlet 的应用程序中的高级架构。我们在 身份验证授权防范漏洞 参考部分中构建了这种高级理解。

过滤器回顾

Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先了解过滤器的作用非常有用。下图显示了单个 HTTP 请求处理程序的典型分层结构。

filterchain
图 1. 过滤器链

客户端向应用程序发送请求,容器创建一个 FilterChain,其中包含应根据请求 URI 的路径处理 HttpServletRequestFilter 实例和 Servlet。在 Spring MVC 应用程序中,ServletDispatcherServlet 的实例。最多一个 Servlet 可以处理单个 HttpServletRequestHttpServletResponse。但是,可以使用多个 Filter

  • 阻止下游 Filter 实例或 Servlet 被调用。在这种情况下,Filter 通常会写入 HttpServletResponse

  • 修改下游 Filter 实例和 Servlet 使用的 HttpServletRequestHttpServletResponse

Filter 的强大功能来自传递给它的 FilterChain

FilterChain 使用示例
  • Java

  • Kotlin

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    // do something before the rest of the application
    chain.doFilter(request, response) // invoke the rest of the application
    // do something after the rest of the application
}

由于 Filter 仅影响下游 Filter 实例和 Servlet,因此每个 Filter 被调用的顺序非常重要。

DelegatingFilterProxy

Spring 提供了一个名为 DelegatingFilterProxyFilter 实现,它允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet 容器允许使用自己的标准注册 Filter 实例,但它不知道 Spring 定义的 Bean。您可以通过标准 Servlet 容器机制注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter 的 Spring Bean。

以下是 DelegatingFilterProxy 如何融入 Filter 实例和 FilterChain 的图片。

delegatingfilterproxy
图 2. DelegatingFilterProxy

DelegatingFilterProxyApplicationContext 中查找 Bean Filter0,然后调用 Bean Filter0。以下清单显示了 DelegatingFilterProxy 的伪代码

DelegatingFilterProxy 伪代码
  • Java

  • Kotlin

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	Filter delegate = getFilterBean(someBeanName); (1)
	delegate.doFilter(request, response); (2)
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
	val delegate: Filter = getFilterBean(someBeanName) (1)
	delegate.doFilter(request, response) (2)
}
1 延迟获取注册为 Spring Bean 的 Filter。对于 DelegatingFilterProxy 中的示例,delegateBean Filter0 的实例。
2 将工作委托给 Spring Bean。

DelegatingFilterProxy 的另一个好处是它允许延迟查找 Filter bean 实例。这很重要,因为容器需要在容器启动之前注册 Filter 实例。但是,Spring 通常使用 ContextLoaderListener 加载 Spring Bean,这在 Filter 实例需要注册之后才会完成。

FilterChainProxy

Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,它允许通过 SecurityFilterChain 将请求委托给多个 Filter 实例。由于 FilterChainProxy 是一个 Bean,它通常被包装在一个 DelegatingFilterProxy 中。

下图显示了 FilterChainProxy 的作用。

filterchainproxy
图 3. FilterChainProxy

SecurityFilterChain

SecurityFilterChainFilterChainProxy 用于确定哪些 Spring Security Filter 实例应该被调用以处理当前请求。

下图显示了 SecurityFilterChain 的作用。

securityfilterchain
图 4. SecurityFilterChain

SecurityFilterChain 中的 安全过滤器 通常是 Bean,但它们是注册到 FilterChainProxy 而不是 DelegatingFilterProxy 中。FilterChainProxy 提供了许多优点,使其优于直接注册到 Servlet 容器或 DelegatingFilterProxy 中。首先,它为所有 Spring Security 的 Servlet 支持提供了一个起点。因此,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,在 FilterChainProxy 中添加一个调试点是一个很好的起点。

其次,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行不被视为可选的任务。例如,它会清除 SecurityContext 以避免内存泄漏。它还会应用 Spring Security 的 HttpFirewall 来保护应用程序免受某些类型的攻击。

此外,它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在 Servlet 容器中,Filter 实例是根据 URL 来调用的。但是,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容来确定调用。

下图显示了多个 SecurityFilterChain 实例。

multi securityfilterchain
图 5. 多个 SecurityFilterChain

多个 SecurityFilterChain 图中,FilterChainProxy 决定使用哪个 SecurityFilterChain。仅调用第一个匹配的 SecurityFilterChain。如果请求的 URL 为 /api/messages/,它首先匹配 SecurityFilterChain0 的模式 /api/**,因此仅调用 SecurityFilterChain0,即使它也匹配 SecurityFilterChainn。如果请求的 URL 为 /messages/,它不匹配 SecurityFilterChain0 的模式 /api/**,因此 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例匹配,则调用 SecurityFilterChainn

请注意,SecurityFilterChain0 仅配置了三个安全 Filter 实例。但是,SecurityFilterChainn 配置了四个安全 Filter 实例。重要的是要注意,每个 SecurityFilterChain 都是唯一的,并且可以独立配置。实际上,如果应用程序希望 Spring Security 忽略某些请求,则 SecurityFilterChain 可能没有安全 Filter 实例。

安全过滤器

安全过滤器使用 SecurityFilterChain API 插入到 FilterChainProxy 中。这些过滤器可用于多种不同的目的,例如 身份验证授权漏洞防护 等等。过滤器按特定顺序执行,以确保它们在正确的时间被调用,例如,执行身份验证的 Filter 应该在执行授权的 Filter 之前被调用。通常不需要了解 Spring Security 的 Filter 的顺序。但是,有时了解顺序是有益的,如果您想了解它们,可以查看 FilterOrderRegistration 代码

为了举例说明以上段落,让我们考虑以下安全配置

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }

}
import org.springframework.security.config.web.servlet.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            csrf { }
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            httpBasic { }
            formLogin { }
        }
        return http.build()
    }

}

以上配置将导致以下 Filter 顺序

过滤器 添加者

CsrfFilter

HttpSecurity#csrf

用户名密码身份验证过滤器

HttpSecurity#formLogin

基本身份验证过滤器

HttpSecurity#httpBasic

授权过滤器

HttpSecurity#authorizeHttpRequests

  1. 首先,CsrfFilter 被调用以防止 CSRF 攻击

  2. 其次,身份验证过滤器被调用以验证请求。

  3. 第三,AuthorizationFilter 被调用以授权请求。

可能还有其他未列出的 Filter 实例。如果您想查看特定请求调用的过滤器列表,您可以 打印它们

打印安全过滤器

通常,查看特定请求调用的安全 Filter 列表非常有用。例如,您想确保您 添加的过滤器 在安全过滤器列表中。

过滤器列表在应用程序启动时以 INFO 级别打印,因此您可以在控制台输出中看到类似以下内容

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

这将很好地说明为 每个过滤器链 配置的安全过滤器。

但这还不是全部,您还可以配置您的应用程序以打印每个请求的每个单独过滤器的调用。这有助于查看您添加的过滤器是否被特定请求调用,或者检查异常来自哪里。为此,您可以配置您的应用程序以 记录安全事件

向过滤器链添加自定义过滤器

大多数情况下,默认的安全过滤器足以保护您的应用程序。但是,有时您可能需要向安全过滤器链添加自定义 Filter

例如,假设您要添加一个 Filter 来获取租户 ID 标头并检查当前用户是否有权访问该租户。前面的描述已经给了我们一个关于在哪里添加过滤器的线索,因为我们需要知道当前用户,我们需要在身份验证过滤器之后添加它。

首先,让我们创建 Filter

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;

public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id"); (1)
        boolean hasAccess = isUserAllowed(tenantId); (2)
        if (hasAccess) {
            filterChain.doFilter(request, response); (3)
            return;
        }
        throw new AccessDeniedException("Access denied"); (4)
    }

}

上面的示例代码执行以下操作

1 从请求头获取租户 ID。
2 检查当前用户是否有权访问租户 ID。
3 如果用户有权访问,则调用链中的其余过滤器。
4 如果用户没有访问权限,则抛出 AccessDeniedException 异常。

您可以从 OncePerRequestFilter 扩展,而不是实现 Filter,它是一个仅对每个请求调用一次的过滤器的基类,并提供了一个带有 HttpServletRequestHttpServletResponse 参数的 doFilterInternal 方法。

现在,我们需要将过滤器添加到安全过滤器链中。

  • Java

  • Kotlin

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); (1)
    return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http
        // ...
        .addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) (1)
    return http.build()
}
1 使用 HttpSecurity#addFilterBeforeAuthorizationFilter 之前添加 TenantFilter

通过在 AuthorizationFilter 之前添加过滤器,我们确保在身份验证过滤器之后调用 TenantFilter。您也可以使用 HttpSecurity#addFilterAfter 在特定过滤器之后添加过滤器,或者使用 HttpSecurity#addFilterAt 在过滤器链中的特定过滤器位置添加过滤器。

就是这样,现在 TenantFilter 将在过滤器链中被调用,并检查当前用户是否有权访问租户 ID。

当您将过滤器声明为 Spring bean 时要小心,无论是通过使用 @Component 注释它,还是通过在配置中将其声明为 bean,因为 Spring Boot 会自动 将其注册到嵌入式容器中。这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。

如果您仍然希望将过滤器声明为 Spring bean 以利用依赖注入,例如,并避免重复调用,您可以通过声明一个 FilterRegistrationBean bean 并将其 enabled 属性设置为 false 来告诉 Spring Boot 不要将其注册到容器中。

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

处理安全异常

ExceptionTranslationFilter 作为 FilterChainProxy 中的一个 安全过滤器 插入。

下图显示了 ExceptionTranslationFilter 与其他组件的关系

exceptiontranslationfilter
  • number 1 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其余部分。

  • number 2 如果用户未经身份验证或为 AuthenticationException,则开始身份验证

    • SecurityContextHolder 被清除。

    • HttpServletRequest保存,以便在身份验证成功后可以用来重放原始请求。

    • AuthenticationEntryPoint 用于向客户端请求凭据。例如,它可能会重定向到登录页面或发送 WWW-Authenticate 标头。

  • number 3 否则,如果为 AccessDeniedException,则拒绝访问AccessDeniedHandler 被调用来处理拒绝访问。

如果应用程序没有抛出 AccessDeniedExceptionAuthenticationException,则 ExceptionTranslationFilter 不会执行任何操作。

ExceptionTranslationFilter 的伪代码如下所示

ExceptionTranslationFilter 伪代码
try {
	filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication(); (2)
	} else {
		accessDenied(); (3)
	}
}
1 过滤器回顾 中所述,调用 FilterChain.doFilter(request, response) 等同于调用应用程序的其余部分。这意味着,如果应用程序的另一个部分(FilterSecurityInterceptor 或方法安全)抛出 AuthenticationExceptionAccessDeniedException,它将被捕获并在此处处理。
2 如果用户未经身份验证或为 AuthenticationException,则开始身份验证
3 否则,拒绝访问

在身份验证之间保存请求

处理安全异常 中所示,当请求没有身份验证并且是针对需要身份验证的资源时,需要保存对已验证资源的请求,以便在身份验证成功后重新请求。在 Spring Security 中,这是通过使用 RequestCache 实现来保存 HttpServletRequest 来完成的。

RequestCache

HttpServletRequest 被保存在 RequestCache 中。当用户成功进行身份验证时,RequestCache 用于重放原始请求。RequestCacheAwareFilter 使用 RequestCache 在用户身份验证后获取保存的 HttpServletRequest,而 ExceptionTranslationFilter 使用 RequestCache 在检测到 AuthenticationException 后保存 HttpServletRequest,然后将用户重定向到登录端点。

默认情况下,使用 HttpSessionRequestCache。以下代码演示了如何自定义 RequestCache 实现,该实现用于在存在名为 continue 的参数时检查 HttpSession 中的已保存请求。

RequestCache 仅在存在 continue 参数时检查已保存的请求
  • Java

  • Kotlin

  • XML

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    val httpRequestCache = HttpSessionRequestCache()
    httpRequestCache.setMatchingRequestParameterName("continue")
    http {
        requestCache {
            requestCache = httpRequestCache
        }
    }
    return http.build()
}
<http auto-config="true">
	<!-- ... -->
	<request-cache ref="requestCache"/>
</http>

<b:bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache"
	p:matchingRequestParameterName="continue"/>

阻止保存请求

您可能有多种原因不想将用户的未经身份验证的请求存储在会话中。您可能希望将该存储卸载到用户的浏览器或存储在数据库中。或者,您可能希望关闭此功能,因为您始终希望将用户重定向到主页,而不是他们登录前尝试访问的页面。

为此,您可以使用 NullRequestCache 实现

阻止保存请求
  • Java

  • Kotlin

  • XML

@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -> cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    val nullRequestCache = NullRequestCache()
    http {
        requestCache {
            requestCache = nullRequestCache
        }
    }
    return http.build()
}
<http auto-config="true">
	<!-- ... -->
	<request-cache ref="nullRequestCache"/>
</http>

<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>

RequestCacheAwareFilter

RequestCacheAwareFilter 使用 RequestCache 来重放原始请求。

日志记录

Spring Security 在 DEBUG 和 TRACE 级别提供所有安全相关事件的全面日志记录。这在调试应用程序时非常有用,因为对于安全措施,Spring Security 不会在响应主体中添加任何有关请求被拒绝原因的详细信息。如果您遇到 401 或 403 错误,您很可能会找到一个日志消息,这将帮助您了解发生了什么。

让我们考虑一个示例,其中用户尝试对启用了 CSRF 保护 的资源发出 POST 请求,但没有 CSRF 令牌。如果没有日志,用户将看到一个 403 错误,没有解释为什么请求被拒绝。但是,如果您为 Spring Security 启用日志记录,您将看到类似这样的日志消息

2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for https://127.0.0.1:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]

很明显,CSRF 令牌丢失,这就是请求被拒绝的原因。

要将您的应用程序配置为记录所有安全事件,您可以在应用程序中添加以下内容

Spring Boot 中的 application.properties
logging.level.org.springframework.security=TRACE
logback.xml
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- ... -->
    </appender>
    <!-- ... -->
    <logger name="org.springframework.security" level="trace" additivity="false">
        <appender-ref ref="Console" />
    </logger>
</configuration>