对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

本节讨论 Spring Security 在基于 Servlet 的应用程序中的高级体系结构。 我们在参考的“身份验证授权防范漏洞利用”部分中基于这种高层次的理解。Spring中文文档

过滤器综述

Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先查看过滤器的作用是有帮助的。 下图显示了单个 HTTP 请求的处理程序的典型分层。Spring中文文档

过滤链
图 1.过滤器链

客户端向应用程序发送请求,容器创建一个 ,其中包含实例,并且应根据请求 URI 的路径处理 。 在 Spring MVC 应用程序中,是 DispatcherServlet 的实例。 最多可以处理单个 和 . 但是,可以使用多个方法:FilterChainFilterServletHttpServletRequestServletServletHttpServletRequestHttpServletResponseFilterSpring中文文档

  • 防止下游实例或 被调用。 在本例中,通常会写入 .FilterServletFilterHttpServletResponseSpring中文文档

  • 修改下游实例使用的 or 和 .HttpServletRequestHttpServletResponseFilterServletSpring中文文档

的力量来自传递到其中的东西。FilterFilterChainSpring中文文档

FilterChain使用示例
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
}

由于 a 只影响下游实例,而 ,因此每个实例的调用顺序非常重要。FilterFilterServletFilterSpring中文文档

DelegatingFilterProxy

Spring 提供了一个名为 DelegatingFilterProxy 的实现,它允许在 Servlet 容器的生命周期和 Spring 的 . Servlet 容器允许使用自己的标准注册实例,但它不知道 Spring 定义的 Beans。 您可以通过标准的 Servlet 容器机制进行注册,但将所有工作委托给实现 .FilterApplicationContextFilterDelegatingFilterProxyFilterSpring中文文档

下面是 Filter 实例和 FilterChain 的适应情况。DelegatingFilterProxySpring中文文档

委派FilterProxy
图2.DelegatingFilterProxy

DelegatingFilterProxy抬头查找豆过滤器0从 然后调用ApplicationContext豆过滤器0. 以下列表显示了 的伪代码:DelegatingFilterProxySpring中文文档

DelegatingFilterProxy伪代码
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 的过滤器。 例如,在 DelegatingFilterProxy 中,是delegate豆过滤器0.
2 将工作委托给 Spring Bean。

另一个好处是它允许延迟查找 Bean 实例。 这很重要,因为容器需要先注册实例,然后才能启动容器。 但是,Spring 通常使用 a 来加载 Spring Beans,直到需要注册实例后才会完成。DelegatingFilterProxyFilterFilterContextLoaderListenerFilterSpring中文文档

1 懒惰地获取已注册为 Spring Bean 的过滤器。 例如,在 DelegatingFilterProxy 中,是delegate豆过滤器0.
2 将工作委托给 Spring Bean。

过滤器链代理

Spring Security 的 Servlet 支持包含在 中。 是 Spring Security 提供的一项特殊功能,允许通过 SecurityFilterChain 委托给多个实例。 由于是 Bean,因此它通常包装在 DelegatingFilterProxy 中。FilterChainProxyFilterChainProxyFilterFilterFilterChainProxySpring中文文档

下图显示了 的作用。FilterChainProxySpring中文文档

filterchain代理
图3.过滤器链代理

安全过滤器链

FilterChainProxy 使用 SecurityFilterChain 来确定应为当前请求调用哪些 Spring Security 实例。FilterSpring中文文档

下图显示了 的作用。SecurityFilterChainSpring中文文档

安全过滤器链
图4.安全过滤器链

的安全过滤器通常是 Beans,但它们是注册的,而不是 DelegatingFilterProxy。 为直接向 Servlet 容器或 DelegatingFilterProxy 注册提供了许多优点。 首先,它为Spring Security的所有Servlet支持提供了一个起点。 出于这个原因,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,那么添加调试点是一个很好的起点。SecurityFilterChainFilterChainProxyFilterChainProxyFilterChainProxySpring中文文档

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

此外,它还在确定何时应调用 a 方面提供了更大的灵活性。 在 Servlet 容器中,仅根据 URL 调用实例。 但是,可以使用接口根据其中的任何内容确定调用。SecurityFilterChainFilterFilterChainProxyHttpServletRequestRequestMatcherSpring中文文档

下图显示了多个实例:SecurityFilterChainSpring中文文档

多重安全过滤器链
图5.多重安全过滤器链

Multiple SecurityFilterChain 图中,确定应使用哪个。 仅调用第一个匹配的。 如果请求 的 URL,则它首先与 的模式匹配,因此仅调用,即使它也匹配 。 如果请求的 URL 与 的模式不匹配,则继续尝试每个 . 假设没有其他实例匹配,则调用。FilterChainProxySecurityFilterChainSecurityFilterChain/api/messages/SecurityFilterChain0/api/**SecurityFilterChain0SecurityFilterChainn/messages/SecurityFilterChain0/api/**FilterChainProxySecurityFilterChainSecurityFilterChainSecurityFilterChainnSpring中文文档

请注意,仅配置了三个安全实例。 但是,配置了四个安全实例。 需要注意的是,每个都可以是唯一的,并且可以单独配置。 事实上,如果应用程序希望 Spring Security 忽略某些请求,则 a 的安全实例可能为零。SecurityFilterChain0FilterSecurityFilterChainnFilterSecurityFilterChainSecurityFilterChainFilterSpring中文文档

安全过滤器

使用 SecurityFilterChain API 将安全过滤器插入到 FilterChainProxy 中。 这些过滤器可用于许多不同的目的,例如身份验证授权漏洞利用保护等。 筛选器按特定顺序执行,以保证在正确的时间调用它们,例如,应先调用执行身份验证的筛选器,然后再调用执行授权的筛选器。 通常没有必要知道 Spring Security 的顺序。 但是,有时了解排序是有益的,如果您想了解它们,可以查看 FilterOrderRegistration 代码FilterFilterFilterSpring中文文档

为了举例说明上面的段落,让我们考虑以下安全配置:Spring中文文档

@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()
    }

}

上述配置将导致以下排序:FilterSpring中文文档

滤波器 添加者

Csrf过滤器Spring中文文档

HttpSecurity#csrfSpring中文文档

用户名PasswordAuthenticationFilterSpring中文文档

HttpSecurity#formLoginSpring中文文档

BasicAuthentication筛选器Spring中文文档

HttpSecurity#httpBasicSpring中文文档

AuthorizationFilterSpring中文文档

HttpSecurity#authorizeHttpRequestsSpring中文文档

  1. 首先,调用 来防止 CSRF 攻击CsrfFilterSpring中文文档

  2. 其次,调用身份验证筛选器来对请求进行身份验证。Spring中文文档

  3. 第三,调用 来授权请求。AuthorizationFilterSpring中文文档

可能还有其他未在上面列出的实例。 如果要查看为特定请求调用的筛选器列表,可以打印它们FilterSpring中文文档

打印安全过滤器

通常,查看为特定请求调用的安全列表很有用。 例如,您希望确保添加的筛选器位于安全筛选器列表中。FilterSpring中文文档

筛选器列表在应用程序启动时以 INFO 级别打印,因此您可以在控制台输出中看到如下内容,例如:Spring中文文档

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]

这将很好地了解为每个过滤器链配置的安全过滤器。Spring中文文档

但这还不是全部,您还可以将应用程序配置为打印每个请求的每个单独筛选器的调用。 这有助于查看是否为特定请求调用了您添加的筛选器,或者检查异常的来源。 为此,您可以将应用程序配置为记录安全事件Spring中文文档

将自定义过滤器添加到过滤器链

大多数情况下,默认安全筛选器足以为应用程序提供安全性。 但是,有时您可能希望将自定义项添加到安全筛选器链中。FilterSpring中文文档

例如,假设要添加一个获取租户 ID 标头的 it,并检查当前用户是否有权访问该租户。 前面的描述已经为我们提供了在何处添加过滤器的线索,因为我们需要知道当前用户,我们需要在身份验证过滤器之后添加它。FilterSpring中文文档

首先,让我们创建:FilterSpring中文文档

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)
    }

}

上面的示例代码执行以下操作:Spring中文文档

1 从请求标头中获取租户 ID。
2 检查当前用户是否有权访问租户 ID。
3 如果用户具有访问权限,则调用链中的其余筛选器。
4 如果用户无权访问,则抛出 .AccessDeniedException

您可以从 OncePerRequestFilter 扩展,而不是实现 ,OncePerRequestFilter 是过滤器的基类,每个请求仅调用一次,并提供具有 and 参数的方法。FilterdoFilterInternalHttpServletRequestHttpServletResponseSpring中文文档

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

@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#addFilterBeforeTenantFilterAuthorizationFilter

通过在身份验证过滤器之前添加过滤器,我们确保在身份验证过滤器之后调用。 还可以用于在特定过滤器之后添加过滤器,或在过滤器链中的特定过滤器位置添加过滤器。AuthorizationFilterTenantFilterHttpSecurity#addFilterAfterHttpSecurity#addFilterAtSpring中文文档

就是这样,现在将在筛选器链中调用,并检查当前用户是否有权访问租户 ID。TenantFilterSpring中文文档

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

例如,如果您仍然想将过滤器声明为 Spring Bean 以利用依赖注入,并避免重复调用,您可以通过声明 Bean 并将其属性设置为 :FilterRegistrationBeanenabledfalseSpring中文文档

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}
滤波器 添加者

Csrf过滤器Spring中文文档

HttpSecurity#csrfSpring中文文档

用户名PasswordAuthenticationFilterSpring中文文档

HttpSecurity#formLoginSpring中文文档

BasicAuthentication筛选器Spring中文文档

HttpSecurity#httpBasicSpring中文文档

AuthorizationFilterSpring中文文档

HttpSecurity#authorizeHttpRequestsSpring中文文档

可能还有其他未在上面列出的实例。 如果要查看为特定请求调用的筛选器列表,可以打印它们FilterSpring中文文档

1 从请求标头中获取租户 ID。
2 检查当前用户是否有权访问租户 ID。
3 如果用户具有访问权限,则调用链中的其余筛选器。
4 如果用户无权访问,则抛出 .AccessDeniedException

您可以从 OncePerRequestFilter 扩展,而不是实现 ,OncePerRequestFilter 是过滤器的基类,每个请求仅调用一次,并提供具有 and 参数的方法。FilterdoFilterInternalHttpServletRequestHttpServletResponseSpring中文文档

1 用于在 .HttpSecurity#addFilterBeforeTenantFilterAuthorizationFilter

处理安全异常

ExceptionTranslationFilter作为安全过滤器之一插入到 FilterChainProxy 中。Spring中文文档

下图显示了与其他组件的关系:ExceptionTranslationFilterSpring中文文档

ExceptionTranslationFilter
  • 数字 1首先,调用以调用应用程序的其余部分。ExceptionTranslationFilterFilterChain.doFilter(request, response)Spring中文文档

  • 数字 2如果用户未通过身份验证或该用户是 ,则启动身份验证AuthenticationExceptionSpring中文文档

  • 数字 3否则,如果它是 ,则拒绝访问。 调用 来处理拒绝的访问。AccessDeniedExceptionAccessDeniedHandlerSpring中文文档

如果应用程序没有抛出 an 或 ,则不执行任何操作。AccessDeniedExceptionAuthenticationExceptionExceptionTranslationFilterSpring中文文档

的伪代码如下所示:ExceptionTranslationFilterSpring中文文档

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

如果应用程序没有抛出 an 或 ,则不执行任何操作。AccessDeniedExceptionAuthenticationExceptionExceptionTranslationFilterSpring中文文档

1 筛选器评审中所述,调用等同于调用应用程序的其余部分。 这意味着,如果应用程序的另一部分(FilterSecurityInterceptor 或方法安全性)抛出 或在此处捕获和处理。FilterChain.doFilter(request, response)AuthenticationExceptionAccessDeniedException
2 如果用户未通过身份验证或用户是 ,则启动身份验证AuthenticationException
3 否则,访问被拒绝

在身份验证之间保存请求

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

请求缓存

保存在 RequestCache 中。 当用户成功进行身份验证时,将用于重播原始请求。 RequestCacheAwareFilter 使用 to 获取用户身份验证后保存的内容,而 使用 to 在检测到 之后保存 ,然后再将用户重定向到登录终结点。HttpServletRequestRequestCacheRequestCacheHttpServletRequestExceptionTranslationFilterRequestCacheHttpServletRequestAuthenticationExceptionSpring中文文档

默认情况下,使用 an。 下面的代码演示了如何自定义用于检查已保存请求的实现(如果存在 named 参数)。HttpSessionRequestCacheRequestCacheHttpSessioncontinueSpring中文文档

RequestCache仅检查已保存的请求(如果参数存在)continue
@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"/>

阻止保存请求

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

阻止保存请求
@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

Logging

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

让我们考虑一个示例,其中用户尝试向启用了 CSRF 保护的资源发出请求,而无需 CSRF 令牌。 如果没有日志,用户将看到 403 错误,其中没有解释请求被拒绝的原因。 但是,如果为 Spring Security 启用日志记录,您将看到如下日志消息:POSTSpring中文文档

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 http://localhost: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中文文档

若要将应用程序配置为记录所有安全事件,可以将以下内容添加到应用程序:Spring中文文档

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>