Architecture

本节讨论基于 Servlet 应用程序中 Spring Security 的高级架构。我们基于 “@13” 的高级理解,在参考的 “@14” 和 “@15” 节中构建。

This section discusses Spring Security’s high-level architecture within Servlet based applications. We build on this high-level understanding within the Authentication, Authorization, and Protection Against Exploits sections of the reference.

A Review of Filters

Spring Security 的 Servlet 支持基于 Servlet 过滤器,所以首先了解过滤器的一般作用很有帮助。下图显示单个 HTTP 请求处理程序的典型分层结构。

Spring Security’s Servlet support is based on Servlet Filters, so it is helpful to look at the role of Filters generally first. The following image shows the typical layering of the handlers for a single HTTP request.

filterchain
Figure 1. FilterChain

客户端向应用程序发送请求,容器创建 FilterChain ,其中包含根据请求 URI 的路径处理 HttpServletRequestFilter 实例和 Servlet 。在 Spring MVC 应用程序中, Servlet 是 {spring-framework-reference-url}web.html#mvc-servlet[DispatcherServlet] 的实例。最多一个 Servlet 可以处理一个 HttpServletRequestHttpServletResponse 。然而,可以使用多个 Filter 来:

The client sends a request to the application, and the container creates a FilterChain, which contains the Filter instances and Servlet that should process the HttpServletRequest, based on the path of the request URI. In a Spring MVC application, the Servlet is an instance of {spring-framework-reference-url}web.html#mvc-servlet[DispatcherServlet]. At most, one Servlet can handle a single HttpServletRequest and HttpServletResponse. However, more than one Filter can be used to:

  • Prevent downstream Filter instances or the Servlet from being invoked. In this case, the Filter typically writes the HttpServletResponse.

  • Modify the HttpServletRequest or HttpServletResponse used by the downstream Filter instances and the Servlet.

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

The power of the Filter comes from the FilterChain that is passed into it.

FilterChain Usage Example
  • 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 的顺序非常重要。

Since a Filter impacts only downstream Filter instances and the Servlet, the order in which each Filter is invoked is extremely important.

DelegatingFilterProxy

Spring 提供名为 {spring-framework-api-url}org/springframework/web/filter/DelegatingFilterProxy.html[DelegatingFilterProxy] 的 Filter 实现,该实现允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet 容器允许使用自己的标准注册 Filter 实例,但它并不知道 Spring 定义的 Bean。您可以通过标准 Servlet 容器机制注册 DelegatingFilterProxy ,但将所有工作委托给实现 Filter 的 Spring Bean。

Spring provides a Filter implementation named {spring-framework-api-url}org/springframework/web/filter/DelegatingFilterProxy.html[DelegatingFilterProxy] that allows bridging between the Servlet container’s lifecycle and Spring’s ApplicationContext. The Servlet container allows registering Filter instances by using its own standards, but it is not aware of Spring-defined Beans. You can register DelegatingFilterProxy through the standard Servlet container mechanisms but delegate all the work to a Spring Bean that implements Filter.

以下是 DelegatingFilterProxy 如何适应 <<`Filter` 实例和 FilterChain,servlet-filters-review>> 的图片。

Here is a picture of how DelegatingFilterProxy fits into the <<`Filter` instances and the FilterChain,servlet-filters-review>>.

delegatingfilterproxy
Figure 2. DelegatingFilterProxy

DelegatingFilterProxyApplicationContext 查找 Bean Filter0,然后调用 Bean Filter0。以下列表显示的 DelegatingFilterProxy 伪代码:

DelegatingFilterProxy looks up Bean Filter0 from the ApplicationContext and then invokes Bean Filter0. The following listing shows pseudo code of DelegatingFilterProxy:

DelegatingFilterProxy Pseudo Code
  • 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 Lazily get Filter that was registered as a Spring Bean. For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0.
2 Delegate work to the Spring Bean.

DelegatingFilterProxy 的另一个好处是它可以延迟查找 Filter Bean 实例。这一点很重要,因为容器需要在容器启动前注册 Filter 实例。然而,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,而此操作直至需要注册 Filter 实例后才会完成。

Another benefit of DelegatingFilterProxy is that it allows delaying looking up Filter bean instances. This is important because the container needs to register the Filter instances before the container can start up. However, Spring typically uses a ContextLoaderListener to load the Spring Beans, which is not done until after the Filter instances need to be registered.

FilterChainProxy

Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是 Spring Security 提供的一个特殊 Filter,允许通过 <<`SecurityFilterChain`,servlet-securityfilterchain>> 委托给多个 Filter 实例。由于 FilterChainProxy 是一个 Bean,因此通常将它包装在 DelegatingFilterProxy 中。

Spring Security’s Servlet support is contained within FilterChainProxy. FilterChainProxy is a special Filter provided by Spring Security that allows delegating to many Filter instances through <<`SecurityFilterChain`,servlet-securityfilterchain>>. Since FilterChainProxy is a Bean, it is typically wrapped in a DelegatingFilterProxy.

下图显示了 FilterChainProxy 的作用。

The following image shows the role of FilterChainProxy.

filterchainproxy
Figure 3. FilterChainProxy

SecurityFilterChain

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

{security-api-url}org/springframework/security/web/SecurityFilterChain.html[SecurityFilterChain] is used by FilterChainProxy to determine which Spring Security Filter instances should be invoked for the current request.

下图显示了 SecurityFilterChain 的角色。

The following image shows the role of SecurityFilterChain.

securityfilterchain
Figure 4. SecurityFilterChain

通常 SecurityFilterChainSecurity Filters 为 Bean,但它们使用 FilterChainProxy 而不是 DelegatingFilterProxy 进行注册。FilterChainProxy 为在 Servlet 容器或 DelegatingFilterProxy 直接进行注册提供了多项优势。首先,它为 Spring Security 的所有 Servlet 支持提供了起点。因此,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,添加一个 FilterChainProxy 中的调试点将是一个绝佳的起点。

The servlet-security-filters in SecurityFilterChain are typically Beans, but they are registered with FilterChainProxy instead of DelegatingFilterProxy. FilterChainProxy provides a number of advantages to registering directly with the Servlet container or DelegatingFilterProxy. First, it provides a starting point for all of Spring Security’s Servlet support. For that reason, if you try to troubleshoot Spring Security’s Servlet support, adding a debug point in FilterChainProxy is a great place to start.

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

Second, since FilterChainProxy is central to Spring Security usage, it can perform tasks that are not viewed as optional. For example, it clears out the SecurityContext to avoid memory leaks. It also applies Spring Security’s HttpFirewall to protect applications against certain types of attacks.

此外,它为在何时调用 SecurityFilterChain 时确定更好的灵活性。在 Servlet 容器中,Filter 的实例根据 URL 单独进行调用。但是,FilterChainProxy 可以使用 RequestMatcher 接口根据 HttpServletRequest 中的任何内容来确定调用。

In addition, it provides more flexibility in determining when a SecurityFilterChain should be invoked. In a Servlet container, Filter instances are invoked based upon the URL alone. However, FilterChainProxy can determine invocation based upon anything in the HttpServletRequest by using the RequestMatcher interface.

下图所示多 SecurityFilterChain 实例:

The following image shows multiple SecurityFilterChain instances:

multi securityfilterchain
Figure 5. Multiple SecurityFilterChain

Multiple SecurityFilterChain 图中,FilterChainProxy 决定要使用哪一个 SecurityFilterChain。调用第一个匹配到的 SecurityFilterChain。如果请求 URL 为 /api/messages/,它会首先匹配 /api/, so only SecurityFilterChain0 is invoked, even though it also matches on SecurityFilterChainn. If a URL of /messages/ is requested, it does not match on the SecurityFilterChain0 pattern of /api/SecurityFilterChain0 模式,因此 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例匹配,则调用 SecurityFilterChainn

In the Multiple SecurityFilterChain figure, FilterChainProxy decides which SecurityFilterChain should be used. Only the first SecurityFilterChain that matches is invoked. If a URL of /api/messages/ is requested, it first matches on the SecurityFilterChain0 pattern of /api/, so only SecurityFilterChain0 is invoked, even though it also matches on SecurityFilterChainn. If a URL of /messages/ is requested, it does not match on the SecurityFilterChain0 pattern of /api/, so FilterChainProxy continues trying each SecurityFilterChain. Assuming that no other SecurityFilterChain instances match, SecurityFilterChainn is invoked.

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

Notice that SecurityFilterChain0 has only three security Filter instances configured. However, SecurityFilterChainn has four security Filter instances configured. It is important to note that each SecurityFilterChain can be unique and can be configured in isolation. In fact, a SecurityFilterChain might have zero security Filter instances if the application wants Spring Security to ignore certain requests.

Security Filters

安全过滤器通过 SecurityFilterChain API 插入到 FilterChainProxy 中。这些过滤器可用于多种不同的目的,如 authenticationauthorizationexploit protection 等。过滤器按特定顺序执行,以保证它们在正确的时间被调用,例如,执行身份验证的 Filter 应在执行授权的 Filter 之前被调用。通常没有必要了解 Spring 安全的 Filter`s. However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the https://github.com/spring-projects/spring-security/tree/{gh-tag}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`FilterOrderRegistration 代码的顺序。

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a specific order to guarantee that they are invoked at the right time, for example, the Filter that performs authentication should be invoked before the Filter that performs authorization. It is typically not necessary to know the ordering of Spring Security’s Filter`s. However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the `FilterOrderRegistration code.

为了解上面的段落,我们考虑以下安全配置:

To exemplify the above paragraph, let’s consider the following security configuration:

  • 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 顺序:

The above configuration will result in the following Filter ordering:

Filter Added by

CsrfFilter

HttpSecurity#csrf

UsernamePasswordAuthenticationFilter

HttpSecurity#formLogin

BasicAuthenticationFilter

HttpSecurity#httpBasic

AuthorizationFilter

HttpSecurity#authorizeHttpRequests

  1. First, the CsrfFilter is invoked to protect against CSRF attacks.

  2. Second, the authentication filters are invoked to authenticate the request.

  3. Third, the AuthorizationFilter is invoked to authorize the request.

可能还有其他未列出的 Filter 实例。如果您希望看到针对特定请求调用的过滤器列表,您可以 print them

There might be other Filter instances that are not listed above. If you want to see the list of filters invoked for a particular request, you can servlet-print-filters.

Printing the Security Filters

通常情况下,查看针对特定请求调用的安全 Filter 列表会很有用。例如,您希望确保 filter you have added 在安全过滤器列表中。

Often times, it is useful to see the list of security `Filter`s that are invoked for a particular request. For example, you want to make sure that the adding-custom-filter is in the list of the security filters.

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

The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example:

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]

这将很好地展示针对 each filter chain 配置的安全过滤器。

And that will give a pretty good idea of the security filters that are configured for servlet-securityfilterchain.

但并非如此,您还可配置应用程序打印针对各个请求单独调用各个过滤器的过程。这有助于查看您添加的过滤器是否针对特定请求调用,或查看异常从何处产生。为此,您可以配置应用程序 log the security events

But that is not all, you can also configure your application to print the invocation of each individual filter for each request. That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from. To do that, you can configure your application to servlet-logging.

Adding a Custom Filter to the Filter Chain

大多数情况下,默认安全过滤器足以用于向应用程序提供安全性。但是,有时您可能希望向安全过滤器链添加一个自定义 Filter

Most of the time, the default security filters are enough to provide security to your application. However, there might be times that you want to add a custom Filter to the security filter chain.

例如,假设您希望添加一个 Filter,获得租户 ID 头并检查当前用户是否访问该租户。前面的描述已让我们了解需要在何处添加过滤器,因为我们需要知道当前用户,我们必须在身份验证过滤器后添加它。

For example, let’s say that you want to add a Filter that gets a tenant id header and check if the current user has access to that tenant. The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters.

首先,让我们创建 Filter

First, let’s create the 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
    }

}

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

The sample code above does the following:

1 Get the tenant id from the request header.
2 Check if the current user has access to the tenant id.
3 If the user has access, then invoke the rest of the filters in the chain.
4 If the user does not have access, then throw an AccessDeniedException.

您可以扩展 {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter],而不实现 Filter ,该类是只在每次请求时调用一次的过滤器的基类,并使用 HttpServletRequestHttpServletResponse 参数提供 doFilterInternal 方法。

Instead of implementing Filter, you can extend from {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter] which is a base class for filters that are only invoked once per request and provides a doFilterInternal method with the HttpServletRequest and HttpServletResponse parameters.

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

Now, we need to add the filter to the security filter chain.

  • 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 Use HttpSecurity#addFilterBefore to add the TenantFilter before the AuthorizationFilter.

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

By adding the filter before the AuthorizationFilter we are making sure that the TenantFilter is invoked after the authentication filters. You can also use HttpSecurity#addFilterAfter to add the filter after a particular filter or HttpSecurity#addFilterAt to add the filter at a particular filter position in the filter chain.

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

And that’s it, now the TenantFilter will be invoked in the filter chain and will check if the current user has access to the tenant id.

当您将过滤器声明为 Spring Bean 时务必要小心,可以通过使用 @Component 注释或在配置中将其声明为 Bean,因为 Spring Boot 将自动 用嵌入式容器注册它。这可能导致过滤器被调用两次,一次由容器,一次由 Spring 安全,而且调用顺序不同。

Be careful when you declare your filter as a Spring bean, either by annotating it with @Component or by declaring it as a bean in your configuration, because Spring Boot will automatically register it with the embedded container. That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.

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

If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a FilterRegistrationBean bean and setting its enabled property to false:

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

Handling Security Exceptions

{security-api-url}org/springframework/security/web/access/ExceptionTranslationFilter.html[ExceptionTranslationFilter] 允许将 {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException] 和 {security-api-url}/org/springframework/security/core/AuthenticationException.html[AuthenticationException] 转换为 HTTP 响应。

The {security-api-url}org/springframework/security/web/access/ExceptionTranslationFilter.html[ExceptionTranslationFilter] allows translation of {security-api-url}org/springframework/security/access/AccessDeniedException.html[AccessDeniedException] and {security-api-url}/org/springframework/security/core/AuthenticationException.html[AuthenticationException] into HTTP responses.

ExceptionTranslationFilter 作为 Security Filters 之一被插入到 FilterChainProxy 中。

ExceptionTranslationFilter is inserted into the FilterChainProxy as one of the Security Filters.

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

The following image shows the relationship of ExceptionTranslationFilter to other components:

exceptiontranslationfilter
  • number 1 First, the ExceptionTranslationFilter invokes FilterChain.doFilter(request, response) to invoke the rest of the application.

  • number 2 If the user is not authenticated or it is an AuthenticationException, then Start Authentication.

    • The SecurityContextHolder is cleared out.

    • The HttpServletRequest is savedrequests so that it can be used to replay the original request once authentication is successful.

    • The AuthenticationEntryPoint is used to request credentials from the client. For example, it might redirect to a log in page or send a WWW-Authenticate header.

  • number 3 Otherwise, if it is an AccessDeniedException, then Access Denied. The AccessDeniedHandler is invoked to handle access denied.

如果应用程序未抛出 AccessDeniedExceptionAuthenticationException,那么 ExceptionTranslationFilter 不会有任何动作。

If the application does not throw an AccessDeniedException or an AuthenticationException, then ExceptionTranslationFilter does not do anything.

ExceptionTranslationFilter 的伪代码类似于以下内容:

The pseudocode for ExceptionTranslationFilter looks something like this:

ExceptionTranslationFilter pseudocode
try {
	filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication(); (2)
	} else {
		accessDenied(); (3)
	}
}
1 As described in A Review of Filters, invoking FilterChain.doFilter(request, response) is the equivalent of invoking the rest of the application. This means that if another part of the application, (<<`FilterSecurityInterceptor`,servlet-authorization-filtersecurityinterceptor>> or method security) throws an AuthenticationException or AccessDeniedException it is caught and handled here.
2 If the user is not authenticated or it is an AuthenticationException, Start Authentication.
3 Otherwise, Access Denied

Saving Requests Between Authentication

Handling Security Exceptions 所示,当请求没有经过认证并且用于需要认证的资源时,需要保存针对经过认证资源的请求,以便在认证成功后重新请求。在 Spring Security 中,这是通过使用 <<`RequestCache`,requestcache>> 实现保存 HttpServletRequest 来完成的。

As illustrated in Handling Security Exceptions, when a request has no authentication and is for a resource that requires authentication, there is a need to save the request for the authenticated resource to re-request after authentication is successful. In Spring Security this is done by saving the HttpServletRequest using a <<`RequestCache`,requestcache>> implementation.

RequestCache

HttpServletRequest 保存到 {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[RequestCache]。当用户成功进行身份验证时,使用 RequestCache 重播原始请求。<<`RequestCacheAwareFilter`,requestcacheawarefilter>> 便使用 RequestCache 来保存 HttpServletRequest

The HttpServletRequest is saved in the {security-api-url}org/springframework/security/web/savedrequest/RequestCache.html[RequestCache]. When the user successfully authenticates, the RequestCache is used to replay the original request. The <<`RequestCacheAwareFilter`,requestcacheawarefilter>> is what uses the RequestCache to save the HttpServletRequest.

默认情况下,会使用 HttpSessionRequestCache。下面的代码演示了如何自定义 RequestCache 实现,它用于在有 continue 参数时检查 HttpSession 是否有已保存请求。

By default, an HttpSessionRequestCache is used. The code below demonstrates how to customize the RequestCache implementation that is used to check the HttpSession for a saved request if the parameter named continue is present.

@9

RequestCache Only Checks for Saved Requests if continue Parameter Present
  • 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"/>

Prevent the Request From Being Saved

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

There are a number of reasons you may want to not store the user’s unauthenticated request in the session. You may want to offload that storage onto the user’s browser or store it in a database. Or you may want to shut off this feature since you always want to redirect the user to the home page instead of the page they tried to visit before login.

为此,你可以使用 {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[实现 NullRequestCache]。

To do that, you can use {security-api-url}org/springframework/security/web/savedrequest/NullRequestCache.html[the NullRequestCache implementation].

Prevent the Request From Being Saved
  • 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

{security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[RequestCacheAwareFilter] 使用 <<`RequestCache`,requestcache>> 保存 HttpServletRequest

The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[RequestCacheAwareFilter] uses the <<`RequestCache`,requestcache>> to save the HttpServletRequest.

Logging

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

Spring Security provides comprehensive logging of all security related events at the DEBUG and TRACE level. This can be very useful when debugging your application because for security measures Spring Security does not add any detail of why a request has been rejected to the response body. If you come across a 401 or 403 error, it is very likely that you will find a log message that will help you understand what is going on.

我们来考虑一个示例,其中一个用户尝试向启用了 CSRF protection 的资源进行 POST 请求,却没有 CSRF 令牌。在没有日志的情况下,用户会看到一个 403 错误,却没有解释为什么拒绝了请求。但是,如果你启用 Spring Security 的日志记录,则会看到如下日志消息:

Let’s consider an example where a user tries to make a POST request to a resource that has CSRF protection enabled without the CSRF token. With no logs, the user will see a 403 error with no explanation of why the request was rejected. However, if you enable logging for Spring Security, you will see a log message like this:

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 令牌,这就是请求被拒绝的原因。

It becomes clear that the CSRF token is missing and that is why the request is being denied.

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

To configure your application to log all the security events, you can add the following to your application:

application.properties in Spring Boot
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>