对于最新的稳定版本,请使用 Spring Security 6.4.3spring-doc.cadn.net.cn

架构

本节讨论 Spring Security 在基于 Servlet 的应用程序中的高级体系结构。 我们在参考的 AuthenticationAuthorizationProtection Against Exploits 部分建立了这种高层次的理解。spring-doc.cadn.net.cn

回顾Filters

Spring Security 的 Servlet 支持基于 ServletFilters 中,因此查看Filters 通常排在第一位。 下图显示了单个 HTTP 请求的处理程序的典型分层。spring-doc.cadn.net.cn

filterchain (筛选链)
图 1.FilterChain (筛选链)

客户端向应用程序发送请求,容器创建一个FilterChain其中包含Filters 和Servlet它应该处理HttpServletRequest基于请求 URI 的路径。 在 Spring MVC 应用程序中,ServletDispatcherServlet. 最多一个Servlet可以处理单个HttpServletRequestHttpServletResponse. 但是,不止一个Filter可用于:spring-doc.cadn.net.cn

  • 防止下游Filters 或Servlet免于被调用。 在本例中,Filter通常会写入HttpServletResponse.spring-doc.cadn.net.cn

  • 修改HttpServletRequestHttpServletResponse由下游使用Filters 和Servletspring-doc.cadn.net.cn

的强大功能Filter来自FilterChain这被传递到它里面。spring-doc.cadn.net.cn

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
}

由于Filter仅影响下游Filters 和Servlet,则每个Filter非常重要。spring-doc.cadn.net.cn

委托过滤器代理

Spring 提供了一个Filter名为DelegatingFilterProxy这允许在 Servlet 容器的生命周期和 Spring 的生命周期之间架起桥梁ApplicationContext. Servlet 容器允许注册Filter使用自己的标准,但它不知道 Spring 定义的 Bean。DelegatingFilterProxy可以通过标准的 Servlet 容器机制进行注册,但将所有工作委托给实现Filter.spring-doc.cadn.net.cn

这是一张如何作的图片DelegatingFilterProxy适合Filters 和FilterChain.spring-doc.cadn.net.cn

DelegatingFilterProxy
图 2.委托过滤器代理

DelegatingFilterProxy查找Bean 过滤器0ApplicationContext然后调用Bean 过滤器0. 的伪代码DelegatingFilterProxy可以在下面看到。spring-doc.cadn.net.cn

DelegatingFilterProxy伪代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	// Lazily get Filter that was registered as a Spring Bean
	// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
	Filter delegate = getFilterBean(someBeanName);
	// delegate work to the Spring Bean
	delegate.doFilter(request, response);
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
	// Lazily get Filter that was registered as a Spring Bean
	// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
	val delegate: Filter = getFilterBean(someBeanName)
	// delegate work to the Spring Bean
	delegate.doFilter(request, response)
}

另一个好处DelegatingFilterProxy是它允许延迟查找Filterbean 实例。 这很重要,因为容器需要注册Filter实例。 但是, Spring 通常使用ContextLoaderListener加载 Spring Bean,这只有在Filter需要注册实例。spring-doc.cadn.net.cn

FilterChainProxy

Spring Security 的 Servlet 支持包含在FilterChainProxy.FilterChainProxy是一种特殊的Filter由 Spring Security 提供,允许委托给多个Filter实例SecurityFilterChain. 因为FilterChainProxy是一个 Bean,它通常包装在 DelegatingFilterProxy 中。spring-doc.cadn.net.cn

filterchainproxy
图 3.FilterChainProxy

SecurityFilterChain 安全过滤器链

SecurityFilterChainFilterChainProxy 用于确定哪个 Spring SecurityFilters 的请求。spring-doc.cadn.net.cn

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

安全过滤器SecurityFilterChain通常是 Bean,但它们是使用FilterChainProxy而不是 DelegatingFilterProxyFilterChainProxy为直接向 Servlet 容器或 DelegatingFilterProxy 注册提供了许多优势。 首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。 因此,如果您尝试对 Spring Security 的 Servlet 支持进行故障排除,请在FilterChainProxy是一个很好的起点。spring-doc.cadn.net.cn

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

此外,它还在确定何时SecurityFilterChain应该调用。 在 Servlet 容器中,Filter仅根据 URL 调用。 然而FilterChainProxy可以根据HttpServletRequest通过利用RequestMatcher接口。spring-doc.cadn.net.cn

事实上FilterChainProxy可用于确定哪个SecurityFilterChain应该使用。 这允许为应用程序的不同切片提供完全独立的配置。spring-doc.cadn.net.cn

Multi SecurityFilterChain
图 5.多个 SecurityFilterChain

在多个 SecurityFilterChainFilterChainProxy决定哪个SecurityFilterChain应该使用。 只有第一个SecurityFilterChain的匹配项。 如果 URL 为/api/messages/请求时,它将首先匹配SecurityFilterChain0的模式/api/**,所以只有SecurityFilterChain0将被调用,即使它也匹配SecurityFilterChainn. 如果 URL 为/messages/时,它不会匹配SecurityFilterChain0的模式/api/**所以FilterChainProxy将继续尝试每个SecurityFilterChain. 假设没有其他人,SecurityFilterChain实例匹配SecurityFilterChainn将被调用。spring-doc.cadn.net.cn

请注意,SecurityFilterChain0只有三个安全Filters 实例。 然而SecurityFilterChainn有四项安全保障Filter已配置。 需要注意的是,每个SecurityFilterChain可以是唯一的,并且可以单独配置。 实际上,SecurityFilterChain可能具有零安全性Filter如果应用程序希望 Spring Security 忽略某些请求,则 sspring-doc.cadn.net.cn

安全过滤器

Spring Security 使用许多 Servlet 过滤器(Jakarta Servlet Spec,第 6 章)来为您的应用程序提供安全性。 安全筛选器使用 SecurityFilterChain API 插入到 FilterChainProxy 中。 这些过滤器可用于多种不同的目的,例如身份验证授权漏洞利用保护等。 过滤器按特定顺序执行,以保证它们在正确的时间被调用,例如,Filter执行身份验证的Filter执行授权。 通常不需要知道 Spring Security 的Filters. 但是,有时了解顺序是有益的,如果您想了解它们,可以检查FilterOrderRegistration法典.spring-doc.cadn.net.cn

为了举例说明上述段落,让我们考虑以下安全配置:spring-doc.cadn.net.cn

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

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

}

The above configuration will result in the following Filter ordering:spring-doc.cadn.net.cn

Filter Added by

CsrfFilterspring-doc.cadn.net.cn

HttpSecurity#csrfspring-doc.cadn.net.cn

UsernamePasswordAuthenticationFilterspring-doc.cadn.net.cn

HttpSecurity#formLoginspring-doc.cadn.net.cn

BasicAuthenticationFilterspring-doc.cadn.net.cn

HttpSecurity#httpBasicspring-doc.cadn.net.cn

AuthorizationFilterspring-doc.cadn.net.cn

HttpSecurity#authorizeHttpRequestsspring-doc.cadn.net.cn

  1. First, the CsrfFilter is invoked to protect against CSRF attacks.spring-doc.cadn.net.cn

  2. Second, the authentication filters are invoked to authenticate the request.spring-doc.cadn.net.cn

  3. Third, the AuthorizationFilter is invoked to authorize the request.spring-doc.cadn.net.cn

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 print them.spring-doc.cadn.net.cn

Printing the Security Filters

Often times, it is useful to see the list of security Filters that are invoked for a particular request. For example, you want to make sure that the filter you have added is in the list of the security filters.spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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]

And that will give a pretty good idea of the security filters that are configured for each filter chain.spring-doc.cadn.net.cn

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 log the security events.spring-doc.cadn.net.cn

Adding a Custom Filter to the Filter Chain

Mostly of the times, 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.spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

First, let’s create the Filter:spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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.

Instead of implementing Filter, you can extend from 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.spring-doc.cadn.net.cn

Now, we need to add the filter to the security filter chain.spring-doc.cadn.net.cn

Java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); (1)
    return http.build();
}
Kotlin
@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.

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.spring-doc.cadn.net.cn

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-doc.cadn.net.cn

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-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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

Handling Security Exceptions

ExceptionTranslationFilter is inserted into the FilterChainProxy as one of the Security Filters.spring-doc.cadn.net.cn

exceptiontranslationfilter
  • number 1 First, the ExceptionTranslationFilter invokes FilterChain.doFilter(request, response) to invoke the rest of the application.spring-doc.cadn.net.cn

  • number 2 If the user is not authenticated or it is an AuthenticationException, then Start Authentication.spring-doc.cadn.net.cn

  • number 3 Otherwise if it is an AccessDeniedException, then Access Denied. The AccessDeniedHandler is invoked to handle access denied.spring-doc.cadn.net.cn

If the application does not throw an AccessDeniedException or an AuthenticationException, then ExceptionTranslationFilter does not do anything.spring-doc.cadn.net.cn

The pseudocode for ExceptionTranslationFilter looks something like this:spring-doc.cadn.net.cn

ExceptionTranslationFilter pseudocode
try {
	filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication(); (2)
	} else {
		accessDenied(); (3)
	}
}
1 You will recall from A Review of Filters that invoking FilterChain.doFilter(request, response) is the equivalent of invoking the rest of the application. This means that if another part of the application, (i.e. FilterSecurityInterceptor or method security) throws an AuthenticationException or AccessDeniedException it will be caught and handled here.
2 If the user is not authenticated or it is an AuthenticationException, then Start Authentication.
3 Otherwise, Access Denied

Saving Requests Between Authentication

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 implementation.spring-doc.cadn.net.cn

RequestCache

The HttpServletRequest is saved in the RequestCache. When the user successfully authenticates, the RequestCache is used to replay the original request. The RequestCacheAwareFilter uses the RequestCache to get the saved HttpServletRequest after the user authenticates, while the ExceptionTranslationFilter uses the RequestCache to save the HttpServletRequest after it detects AuthenticationException, before redirecting the user to the login endpoint.spring-doc.cadn.net.cn

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.spring-doc.cadn.net.cn

RequestCache Only Checks for Saved Requests if continue Parameter Present
@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.spring-doc.cadn.net.cn

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

The RequestCacheAwareFilter uses the RequestCache to replay the original request.spring-doc.cadn.net.cn

Logging

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.spring-doc.cadn.net.cn

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:spring-doc.cadn.net.cn

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]

It becomes clear that the CSRF token is missing and that is why the request is being denied.spring-doc.cadn.net.cn

To configure your application to log all the security events, you can add the following to your application:spring-doc.cadn.net.cn

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>