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

架构

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

过滤器回顾

Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此通常首先查看过滤器的角色会很有帮助。 下图显示了单个 HTTP 请求的处理程序的典型分层。spring-doc.cadn.net.cn

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

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

  • 防止下游Filter实例或Servlet免于被调用。 在这种情况下,Filter通常会写入HttpServletResponse.spring-doc.cadn.net.cn

  • 修改HttpServletRequestHttpServletResponse由下游使用Filter实例和Servlet.spring-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仅影响下游Filter实例和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适合Filter实例和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.spring-doc.cadn.net.cn

filterchainproxy
图 3.FilterChainProxy

SecurityFilterChain 安全过滤器链

SecurityFilterChainFilterChainProxy 用于确定哪个 Spring SecurityFilter应为当前请求调用实例。spring-doc.cadn.net.cn

下图显示了SecurityFilterChain.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

下图显示了多个SecurityFilterChain实例:spring-doc.cadn.net.cn

Multi SecurityFilterChain
图 5.多个 SecurityFilterChain

在多个 SecurityFilterChain 图中,FilterChainProxy决定哪个SecurityFilterChain应该使用。 只有第一个SecurityFilterChainthat matches 被调用。 如果 URL 为/api/messages/请求时,它首先匹配SecurityFilterChain0的模式/api/**,所以只有SecurityFilterChain0被调用,即使它也匹配SecurityFilterChainn. 如果 URL 为/messages/请求时,它与SecurityFilterChain0的模式/api/**所以FilterChainProxy继续尝试每个SecurityFilterChain. 假设没有其他SecurityFilterChaininstances match、SecurityFilterChainn被调用。spring-doc.cadn.net.cn

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

安全过滤器

安全筛选器使用 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()
    }

}

上述配置将产生以下结果Filter订购:spring-doc.cadn.net.cn

Filter 添加者

Csrf过滤器spring-doc.cadn.net.cn

HttpSecurity#csrfspring-doc.cadn.net.cn

用户名密码身份验证过滤器spring-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. 首先,CsrfFilter用于防止 CSRF 攻击spring-doc.cadn.net.cn

  2. 其次,调用身份验证筛选器来验证请求。spring-doc.cadn.net.cn

  3. 第三,AuthorizationFilter用于授权请求。spring-doc.cadn.net.cn

可能还有其他Filter上面未列出的实例。 如果要查看为特定请求调用的过滤器列表,可以打印它们spring-doc.cadn.net.cn

打印安全过滤器

通常,查看安全列表很有用Filter为特定请求调用的 s。 例如,您希望确保已添加的过滤器位于安全过滤器列表中。spring-doc.cadn.net.cn

筛选器列表在应用程序启动时以 INFO 级别打印,因此您可以在控制台输出上看到类似于以下内容的内容,例如: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]

这将很好地了解为每个过滤器链配置的安全过滤器。spring-doc.cadn.net.cn

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

将自定义筛选器添加到筛选器链

大多数情况下,默认安全筛选器足以为您的应用程序提供安全性。 但是,有时您可能希望添加自定义Filter添加到安全过滤器链中。spring-doc.cadn.net.cn

例如,假设您要添加一个Filter这将获取租户 ID 标头,并检查当前用户是否有权访问该租户。 前面的描述已经给了我们在哪里添加过滤器的线索,因为我们需要知道当前用户,所以我们需要在认证过滤器之后添加它。spring-doc.cadn.net.cn

首先,让我们创建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)
    }

}

上面的示例代码执行以下作:spring-doc.cadn.net.cn

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

而不是实施Filter,您可以从 OncePerRequestFilter 扩展,后者是每个请求仅调用一次的过滤器的基类,并提供doFilterInternal方法与HttpServletRequestHttpServletResponse参数。spring-doc.cadn.net.cn

现在,我们需要将过滤器添加到安全过滤器链中。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 HttpSecurity#addFilterBefore要添加TenantFilterAuthorizationFilter.

通过在AuthorizationFilter我们正在确保TenantFilter在身份验证筛选器之后调用。 您还可以使用HttpSecurity#addFilterAfter在特定过滤器之后添加过滤器,或将过滤器添加到特定过滤器之后,或者HttpSecurity#addFilterAt在Filter链中的特定Filter位置添加Filter。spring-doc.cadn.net.cn

就是这样,现在TenantFilter将在过滤器链中调用,并将检查当前用户是否有权访问租户 ID。spring-doc.cadn.net.cn

当你将过滤器声明为 Spring bean 时要小心,要么用@Component或者在配置中将其声明为 bean,因为 Spring Boot 会自动将其注册到嵌入式容器中。 这可能会导致过滤器被调用两次,一次由容器调用,一次由 Spring Security 调用,并且顺序不同。spring-doc.cadn.net.cn

例如,如果你仍然想将过滤器声明为 Spring bean 以利用依赖关系注入,并避免重复调用,你可以通过声明一个FilterRegistrationBeanbean 并将其enabledproperty 设置为false:spring-doc.cadn.net.cn

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

处理安全异常

ExceptionTranslationFilter作为安全筛选器之一插入到 FilterChainProxy 中。spring-doc.cadn.net.cn

下图显示了ExceptionTranslationFilter到其他组件:spring-doc.cadn.net.cn

exceptiontranslationfilter

如果应用程序没有抛出AccessDeniedExceptionAuthenticationException然后ExceptionTranslationFilter不执行任何作。spring-doc.cadn.net.cn

的伪代码ExceptionTranslationFilter看起来像这样:spring-doc.cadn.net.cn

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 否则,Access Denied (访问被拒绝

在身份验证之间保存请求

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

请求缓存

HttpServletRequest保存在RequestCache. 当用户成功进行身份验证时,RequestCache用于重放原始请求。 这RequestCacheAwareFilter是使用RequestCache以保存HttpServletRequest.spring-doc.cadn.net.cn

默认情况下,HttpSessionRequestCache被使用。 下面的代码演示了如何自定义RequestCache实现,用于检查HttpSession对于已保存的请求,如果名为continue存在。spring-doc.cadn.net.cn

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

阻止保存请求
@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以保存HttpServletRequest.spring-doc.cadn.net.cn

Logging

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

让我们考虑一个用户尝试将POST请求到启用了 CSRF 保护但未启用 CSRF 令牌的资源。 如果没有日志,用户将看到 403 错误,并且没有解释请求被拒绝的原因。 但是,如果为 Spring Security 启用日志记录,则会看到如下日志消息: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]

很明显,CSRF 令牌丢失了,这就是请求被拒绝的原因。spring-doc.cadn.net.cn

要将应用程序配置为记录所有安全事件,可以将以下内容添加到应用程序中:spring-doc.cadn.net.cn

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>