此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.3.1Spring中文文档

在最终用户可以登录的应用程序中,请务必考虑如何防止跨站点请求伪造 (CSRF)。Spring中文文档

默认情况下,Spring Security 会针对不安全的 HTTP 方法(例如 POST 请求)防止 CSRF 攻击,因此不需要额外的代码。 您可以使用以下命令显式指定默认配置:Spring中文文档

配置 CSRF 保护
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf(Customizer.withDefaults());
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf { }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf/>
</http>

要了解有关应用程序的 CSRF 保护的更多信息,请考虑以下用例:Spring中文文档

了解 CSRF 保护的组件

CSRF 保护由 CsrfFilter 中的几个组件提供:Spring中文文档

CSRF公司
图 1. 组件CsrfFilter

CSRF保护分为两部分:Spring中文文档

  1. 通过委托给 CsrfTokenRequestHandler,使 CsrfToken 可供应用程序使用。Spring中文文档

  2. 确定请求是否需要 CSRF 保护,加载并验证令牌,并处理 AccessDeniedExceptionSpring中文文档

CSRF 处理
图2. 加工CsrfFilter

迁移到 Spring Security 6

从 Spring Security 5 迁移到 Spring Security 6 时,有一些更改可能会影响您的应用程序。 以下是 Spring Security 6 中更改的 CSRF 保护方面的概述:Spring中文文档

Spring Security 6 中的更改需要对单页应用程序进行额外的配置,因此您可能会发现单页应用程序部分特别有用。Spring中文文档

有关迁移 Spring Security 5 应用程序的更多信息,请参阅迁移一章的漏洞利用保护部分。Spring中文文档

Spring Security 6 中的更改需要对单页应用程序进行额外的配置,因此您可能会发现单页应用程序部分特别有用。Spring中文文档

CsrfToken

使用 .CsrfTokenCsrfTokenRepositorySpring中文文档

默认情况下,HttpSessionCsrfTokenRepository 用于在会话中存储令牌。 Spring Security 还提供了 CookieCsrfTokenRepository,用于在 cookie 中存储令牌。 您还可以指定自己的实现,以将令牌存储在您喜欢的任何位置。Spring中文文档

使用HttpSessionCsrfTokenRepository

默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将预期的 CSRF 令牌存储在 by using HttpSessionCsrfTokenRepository 中,因此不需要额外的代码。HttpSessionSpring中文文档

默认情况下,从名为 HTTP 请求标头或请求参数的 HTTP 请求标头中读取令牌。HttpSessionCsrfTokenRepositoryX-CSRF-TOKEN_csrfSpring中文文档

您可以使用以下配置显式指定默认配置:Spring中文文档

配置HttpSessionCsrfTokenRepository
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = HttpSessionCsrfTokenRepository()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>

您可以使用 CookieCsrfTokenRepository 在 cookie 中保留 它以支持基于 JavaScript 的应用程序CsrfTokenSpring中文文档

写入名为的 cookie,并从 HTTP 请求标头或 request 参数中读取它。 这些默认值来自 Angular 及其前身 AngularJSCookieCsrfTokenRepositoryXSRF-TOKENX-XSRF-TOKEN_csrfSpring中文文档

有关此主题的更多最新信息,请参阅跨站点请求伪造 (XSRF) 保护指南和 HttpClientXsrfModuleSpring中文文档

您可以使用以下配置进行配置:CookieCsrfTokenRepositorySpring中文文档

该示例显式设置为 。 这是让 JavaScript 框架(如 Angular)读取它所必需的。 如果您不需要直接使用 JavaScript 读取 cookie,我们建议省略(改用)以提高安全性。HttpOnlyfalseHttpOnlynew CookieCsrfTokenRepository()Spring中文文档

自定义CsrfTokenRepository

在某些情况下,您可能希望实现自定义 CsrfTokenRepositorySpring中文文档

实现接口后,您可以将 Spring Security 配置为通过以下配置使用它:CsrfTokenRepositorySpring中文文档

配置自定义CsrfTokenRepository
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(new CustomCsrfTokenRepository())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = CustomCsrfTokenRepository()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="example.CustomCsrfTokenRepository"/>

有关此主题的更多最新信息,请参阅跨站点请求伪造 (XSRF) 保护指南和 HttpClientXsrfModuleSpring中文文档

该示例显式设置为 。 这是让 JavaScript 框架(如 Angular)读取它所必需的。 如果您不需要直接使用 JavaScript 读取 cookie,我们建议省略(改用)以提高安全性。HttpOnlyfalseHttpOnlynew CookieCsrfTokenRepository()Spring中文文档

处理CsrfToken

可供应用程序使用 . 此组件还负责解析 from HTTP 标头或请求参数。CsrfTokenCsrfTokenRequestHandlerCsrfTokenSpring中文文档

默认情况下,XorCsrfTokenRequestAttributeHandler 用于提供 . Spring Security 还提供了 CsrfTokenRequestAttributeHandler 用于选择退出 BREACH 保护。 还可以指定自己的实现,以自定义处理和解析令牌的策略。CsrfTokenSpring中文文档

使用 (BREACH)XorCsrfTokenRequestAttributeHandler

它使 作为称为 的属性可用,并且还为 BBREACH 提供保护。XorCsrfTokenRequestAttributeHandlerCsrfTokenHttpServletRequest_csrfSpring中文文档

还使用名称 作为请求属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfXorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNameSpring中文文档

此实现还将请求中的令牌值解析为请求标头(默认情况下是 X-CSRF-TOKENX-XSRF-TOKEN 之一)或请求参数(默认情况下)。_csrfSpring中文文档

通过将随机性编码到 CSRF 令牌值中来提供 BREACH 保护,以确保每个请求返回的更改。 当令牌稍后解析为标头值或请求参数时,将对其进行解码以获取原始令牌,然后将其与持久化的 CsrfToken 进行比较。CsrfTokenSpring中文文档

默认情况下,Spring Security 会保护 CSRF 令牌免受 BREACH 攻击,因此不需要额外的代码。 您可以使用以下配置显式指定默认配置:Spring中文文档

配置 BREACH 保护
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>

使用CsrfTokenRequestAttributeHandler

使 可用作称为 的属性。CsrfTokenRequestAttributeHandlerCsrfTokenHttpServletRequest_csrfSpring中文文档

还使用名称 作为请求属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNameSpring中文文档

此实现还将请求中的令牌值解析为请求标头(默认情况下是 X-CSRF-TOKENX-XSRF-TOKEN 之一)或请求参数(默认情况下)。_csrfSpring中文文档

其主要用途是选择退出 的 BREACH 保护,可以使用以下配置进行配置:CsrfTokenRequestAttributeHandlerCsrfTokenSpring中文文档

选择退出 BREACH 保护
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>

自定义CsrfTokenRequestHandler

您可以实现该接口来自定义处理和解析令牌的策略。CsrfTokenRequestHandlerSpring中文文档

该接口是一个可以使用 lambda 表达式实现的接口,以自定义请求处理。 您将需要实现完整的接口,以自定义如何从请求解析令牌。 有关使用委派实现用于处理和解析令牌的自定义策略的示例,请参阅为单页应用程序配置 CSRFCsrfTokenRequestHandler@FunctionalInterfaceSpring中文文档

实现接口后,您可以将 Spring Security 配置为通过以下配置使用它:CsrfTokenRequestHandlerSpring中文文档

配置自定义CsrfTokenRequestHandler
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(new CustomCsrfTokenRequestHandler())
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = CustomCsrfTokenRequestHandler()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="example.CustomCsrfTokenRequestHandler"/>

还使用名称 作为请求属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfXorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNameSpring中文文档

通过将随机性编码到 CSRF 令牌值中来提供 BREACH 保护,以确保每个请求返回的更改。 当令牌稍后解析为标头值或请求参数时,将对其进行解码以获取原始令牌,然后将其与持久化的 CsrfToken 进行比较。CsrfTokenSpring中文文档

还使用名称 作为请求属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNameSpring中文文档

该接口是一个可以使用 lambda 表达式实现的接口,以自定义请求处理。 您将需要实现完整的接口,以自定义如何从请求解析令牌。 有关使用委派实现用于处理和解析令牌的自定义策略的示例,请参阅为单页应用程序配置 CSRFCsrfTokenRequestHandler@FunctionalInterfaceSpring中文文档

延迟加载CsrfToken

默认情况下,Spring Security 会推迟加载,直到需要为止。CsrfTokenSpring中文文档

每当使用不安全的 HTTP 方法(如 POST)发出请求时,都需要 。 此外,任何将令牌呈现到响应的请求都需要它,例如具有包含隐藏的 CSRF 令牌的标记的网页。CsrfToken<form><input>Spring中文文档

由于 Spring Security 默认情况下也会存储 in 中,因此延迟的 CSRF 令牌可以通过不需要在每个请求上加载会话来提高性能。CsrfTokenHttpSessionSpring中文文档

如果要选择退出延迟令牌并导致在每个请求上加载令牌,则可以使用以下配置执行此操作:CsrfTokenSpring中文文档

选择退出递延的 CSRF 代币
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
		// set the name of the attribute the CsrfToken will be populated on
		requestHandler.setCsrfRequestAttributeName(null);
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRequestHandler(requestHandler)
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        val requestHandler = XorCsrfTokenRequestAttributeHandler()
        // set the name of the attribute the CsrfToken will be populated on
        requestHandler.setCsrfRequestAttributeName(null)
        http {
            // ...
            csrf {
                csrfTokenRequestHandler = requestHandler
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
	class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler">
	<b:property name="csrfRequestAttributeName">
		<b:null/>
	</b:property>
</b:bean>

通过将 设置为 ,必须首先加载以确定要使用的属性名称。 这会导致在每个请求上加载 。csrfRequestAttributeNamenullCsrfTokenCsrfTokenSpring中文文档

每当使用不安全的 HTTP 方法(如 POST)发出请求时,都需要 。 此外,任何将令牌呈现到响应的请求都需要它,例如具有包含隐藏的 CSRF 令牌的标记的网页。CsrfToken<form><input>Spring中文文档

通过将 设置为 ,必须首先加载以确定要使用的属性名称。 这会导致在每个请求上加载 。csrfRequestAttributeNamenullCsrfTokenCsrfTokenSpring中文文档

与 CSRF 保护集成

为了使同步器令牌模式能够防止 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。 这必须包含在请求的一部分(表单参数、HTTP 标头或其他部分)中,而浏览器不会自动将其包含在 HTTP 请求中。Spring中文文档

以下各节介绍前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式:Spring中文文档

HTML 表单

要提交 HTML 表单,CSRF 令牌必须作为隐藏输入包含在表单中。 例如,呈现的 HTML 可能如下所示:Spring中文文档

HTML 形式的 CSRF 令牌
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

以下视图技术自动将实际的 CSRF 令牌包含在具有不安全 HTTP 方法(如 POST)的表单中:Spring中文文档

如果这些选项不可用,则可以利用 公开为名为 _csrfHttpServletRequest 属性这一事实。 以下示例使用 JSP 执行此操作:CsrfTokenSpring中文文档

具有请求属性的 HTML 形式的 CSRF 令牌
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	name="${_csrf.parameterName}"
	value="${_csrf.token}"/>
</form>

JavaScript 应用程序

JavaScript 应用程序通常使用 JSON 而不是 HTML。 如果使用 JSON,则可以在 HTTP 请求标头中提交 CSRF 令牌,而不是请求参数。Spring中文文档

为了获取CSRF令牌,您可以配置Spring Security以将预期的CSRF令牌存储在cookie中。 通过将预期的令牌存储在 cookie 中,Angular 等 JavaScript 框架可以自动将实际的 CSRF 令牌作为 HTTP 请求标头包含在内。Spring中文文档

在将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,BREACH 保护和延迟令牌有特殊注意事项。 下一节将提供完整的配置示例。Spring中文文档

您可以在以下部分中了解不同类型的 JavaScript 应用程序:Spring中文文档

单页应用程序

将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,有一些特殊的注意事项。Spring中文文档

回想一下,Spring Security 默认情况下提供 CsrfToken 的 BREACH 保护在 Cookie 中存储预期的 CSRF 令牌时,JavaScript 应用程序将只能访问普通令牌值,而无权访问编码值。 需要提供用于解析实际令牌值的自定义请求处理程序Spring中文文档

此外,存储 CSRF 令牌的 cookie 将在身份验证成功和注销成功后被清除。 默认情况下,Spring Security 会延迟加载新的 CSRF 令牌,并且需要额外的工作才能返回新的 cookie。Spring中文文档

身份验证成功和注销成功后需要刷新令牌,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除上一个令牌。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求(如 POST)。Spring中文文档

为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:Spring中文文档

为单页应用程序配置 CSRF
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   (1)
				.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())            (2)
			)
			.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); (3)
		return http.build();
	}
}

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
	private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
		/*
		 * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
		 * the CsrfToken when it is rendered in the response body.
		 */
		this.delegate.handle(request, response, csrfToken);
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		/*
		 * If the request contains a request header, use CsrfTokenRequestAttributeHandler
		 * to resolve the CsrfToken. This applies when a single-page application includes
		 * the header value automatically, which was obtained via a cookie containing the
		 * raw CsrfToken.
		 */
		if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
			return super.resolveCsrfTokenValue(request, csrfToken);
		}
		/*
		 * In all other cases (e.g. if the request contains a request parameter), use
		 * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
		 * when a server-side rendered form includes the _csrf request parameter as a
		 * hidden input.
		 */
		return this.delegate.resolveCsrfTokenValue(request, csrfToken);
	}
}

final class CsrfCookieFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
		// Render the token value to a cookie by causing the deferred token to be loaded
		csrfToken.getToken();

		filterChain.doFilter(request, response);
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()    (1)
                csrfTokenRequestHandler = SpaCsrfTokenRequestHandler()                 (2)
            }
        }
        http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) (3)
        return http.build()
    }
}

class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
    private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()

    override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
        /*
         * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
         * the CsrfToken when it is rendered in the response body.
         */
        delegate.handle(request, response, csrfToken)
    }

    override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String {
        /*
         * If the request contains a request header, use CsrfTokenRequestAttributeHandler
         * to resolve the CsrfToken. This applies when a single-page application includes
         * the header value automatically, which was obtained via a cookie containing the
         * raw CsrfToken.
         */
        return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
            super.resolveCsrfTokenValue(request, csrfToken)
        } else {
            /*
             * In all other cases (e.g. if the request contains a request parameter), use
             * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
             * when a server-side rendered form includes the _csrf request parameter as a
             * hidden input.
             */
            delegate.resolveCsrfTokenValue(request, csrfToken)
        }
    }
}

class CsrfCookieFilter : OncePerRequestFilter() {

    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val csrfToken = request.getAttribute("_csrf") as CsrfToken
        // Render the token value to a cookie by causing the deferred token to be loaded
        csrfToken.token
        filterChain.doFilter(request, response)
    }
}
<http>
	<!-- ... -->
	<csrf
		token-repository-ref="tokenRepository"                        (1)
		request-handler-ref="requestHandler"/>                        (2)
	<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> (3)
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
	class="example.SpaCsrfTokenRequestHandler"/>
<b:bean id="csrfCookieFilter"
	class="example.CsrfCookieFilter"/>
1 配置为 ,以便 JavaScript 应用程序可以读取 cookie。CookieCsrfTokenRepositoryHttpOnlyfalse
2 配置一个自定义,该自定义器根据 CSRF 令牌是 HTTP 请求标头 () 还是请求参数 () 来解析 CSRF 令牌。CsrfTokenRequestHandlerX-XSRF-TOKEN_csrf
3 配置自定义以在每个请求上加载,如果需要,它将返回一个新的 cookie。FilterCsrfToken

多页应用程序

对于在每个页面上加载 JavaScript 的多页面应用程序,在 Cookie 中公开 CSRF 令牌的替代方法是在标记中包含 CSRF 令牌。 HTML 可能如下所示:metaSpring中文文档

HTML 元标记中的 CSRF 令牌
<html>
<head>
	<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
	<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
	<!-- ... -->
</head>
<!-- ... -->
</html>

为了在请求中包含 CSRF 令牌,您可以利用 公开为名为 _csrfHttpServletRequest 属性这一事实。 以下示例使用 JSP 执行此操作:CsrfTokenSpring中文文档

具有请求属性的 HTML 元标记中的 CSRF 令牌
<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->
</html>

一旦 meta 标记包含 CSRF 标记,JavaScript 代码就可以读取 meta 标记并将 CSRF 标记作为标头包含在内。 如果使用 jQuery,则可以使用以下代码执行此操作:Spring中文文档

在 AJAX 请求中包含 CSRF 令牌
$(function () {
	var token = $("meta[name='_csrf']").attr("content");
	var header = $("meta[name='_csrf_header']").attr("content");
	$(document).ajaxSend(function(e, xhr, options) {
		xhr.setRequestHeader(header, token);
	});
});

其他 JavaScript 应用程序

JavaScript 应用程序的另一个选项是在 HTTP 响应标头中包含 CSRF 令牌。Spring中文文档

实现此目的的一种方法是将 与 CsrfTokenArgumentResolver 一起使用。 以下是适用于应用程序中所有控制器端点的示例:@ControllerAdvice@ControllerAdviceSpring中文文档

HTTP 响应标头中的 CSRF 令牌
@ControllerAdvice
public class CsrfControllerAdvice {

	@ModelAttribute
	public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
		response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
	}

}
@ControllerAdvice
class CsrfControllerAdvice {

	@ModelAttribute
	fun getCsrfToken(response: HttpServletResponse, csrfToken: CsrfToken) {
		response.setHeader(csrfToken.headerName, csrfToken.token)
	}

}

由于这适用于应用程序中的所有终结点,因此将导致在每个请求上加载 CSRF 令牌,这可能会在使用 HttpSessionCsrfTokenRepository 时否定延迟令牌的好处。 但是,在使用 CookieCsrfTokenRepository 时,这通常不是问题。@ControllerAdviceSpring中文文档

请务必记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后调用的。 这意味着,仅当请求通过筛选器链传递到应用程序时,才会应用此功能。 有关向筛选器链添加筛选器以便更早访问 的示例,请参阅单页应用程序的配置。@ControllerAdviceHttpServletResponseSpring中文文档

CSRF 令牌现在将在响应标头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)中提供,适用于控制器建议适用的任何自定义端点。 对后端的任何请求都可用于从响应中获取令牌,后续请求可以将令牌包含在同名的请求标头中。Spring中文文档

移动应用程序

JavaScript 应用程序一样,移动应用程序通常使用 JSON 而不是 HTML。 提供浏览器流量的后端应用程序可以选择禁用 CSRF。 在这种情况下,不需要额外的工作。Spring中文文档

但是,同时提供浏览器流量并因此仍需要 CSRF 保护的后端应用程序可能会继续存储在会话中而不是 cookie 中CsrfTokenSpring中文文档

在这种情况下,与后端集成的典型模式是公开端点,以允许前端(移动或浏览器客户端)按需请求 CSRF 令牌。 使用此模式的好处是,CSRF 令牌可以继续延迟,并且仅在请求需要 CSRF 保护时才需要从会话中加载。 使用自定义终结点还意味着客户端应用程序可以通过发出显式请求来请求按需生成新令牌(如有必要)。/csrfSpring中文文档

此模式可用于需要 CSRF 保护的任何类型的应用程序,而不仅仅是移动应用程序。 虽然在这些情况下通常不需要这种方法,但它是与受 CSRF 保护的后端集成的另一种选择。Spring中文文档

下面是使用 CsrfTokenArgumentResolver 的终结点示例:/csrfSpring中文文档

端点/csrf
@RestController
public class CsrfController {

    @GetMapping("/csrf")
    public CsrfToken csrf(CsrfToken csrfToken) {
        return csrfToken;
    }

}
@RestController
class CsrfController {

    @GetMapping("/csrf")
    fun csrf(csrfToken: CsrfToken): CsrfToken {
        return csrfToken
    }

}

在向服务器进行身份验证之前,如果需要上述终结点,则可以考虑添加。.requestMatchers("/csrf").permitAll()Spring中文文档

应在启动或初始化应用程序时(例如,在加载时)以及身份验证成功和注销成功后调用此终结点以获取 CSRF 令牌。Spring中文文档

身份验证成功和注销成功后需要刷新令牌,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除上一个令牌。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求(如 POST)。Spring中文文档

获取 CSRF 令牌后,您需要自行将其作为 HTTP 请求标头(默认为 X-CSRF-TOKENX-XSRF-TOKEN 之一)包含在内。Spring中文文档

在将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,BREACH 保护和延迟令牌有特殊注意事项。 下一节将提供完整的配置示例。Spring中文文档

身份验证成功和注销成功后需要刷新令牌,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除上一个令牌。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求(如 POST)。Spring中文文档

1 配置为 ,以便 JavaScript 应用程序可以读取 cookie。CookieCsrfTokenRepositoryHttpOnlyfalse
2 配置一个自定义,该自定义器根据 CSRF 令牌是 HTTP 请求标头 () 还是请求参数 () 来解析 CSRF 令牌。CsrfTokenRequestHandlerX-XSRF-TOKEN_csrf
3 配置自定义以在每个请求上加载,如果需要,它将返回一个新的 cookie。FilterCsrfToken

由于这适用于应用程序中的所有终结点,因此将导致在每个请求上加载 CSRF 令牌,这可能会在使用 HttpSessionCsrfTokenRepository 时否定延迟令牌的好处。 但是,在使用 CookieCsrfTokenRepository 时,这通常不是问题。@ControllerAdviceSpring中文文档

请务必记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后调用的。 这意味着,仅当请求通过筛选器链传递到应用程序时,才会应用此功能。 有关向筛选器链添加筛选器以便更早访问 的示例,请参阅单页应用程序的配置。@ControllerAdviceHttpServletResponseSpring中文文档

此模式可用于需要 CSRF 保护的任何类型的应用程序,而不仅仅是移动应用程序。 虽然在这些情况下通常不需要这种方法,但它是与受 CSRF 保护的后端集成的另一种选择。Spring中文文档

在向服务器进行身份验证之前,如果需要上述终结点,则可以考虑添加。.requestMatchers("/csrf").permitAll()Spring中文文档

身份验证成功和注销成功后需要刷新令牌,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除上一个令牌。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求(如 POST)。Spring中文文档

处理AccessDeniedException

要处理诸如 ,您可以将 Spring Security 配置为以您喜欢的任何方式处理这些异常。 例如,您可以使用以下配置配置自定义拒绝访问页面:AccessDeniedExceptionInvalidCsrfTokenExceptionSpring中文文档

配置AccessDeniedHandler
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.exceptionHandling((exceptionHandling) -> exceptionHandling
				.accessDeniedPage("/access-denied")
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            exceptionHandling {
                accessDeniedPage = "/access-denied"
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<access-denied-handler error-page="/access-denied"/>
</http>

CSRF测试

您可以使用 Spring Security 的测试支持CsrfRequestPostProcessor 来测试 CSRF 保护,如下所示:Spring中文文档

测试 CSRF 保护
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfTests {

	private MockMvc mockMvc;

	@BeforeEach
	public void setUp(WebApplicationContext applicationContext) {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
			.apply(springSecurity())
			.build();
	}

	@Test
	public void loginWhenValidCsrfTokenThenSuccess() throws Exception {
		this.mockMvc.perform(post("/login").with(csrf())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().is3xxRedirection())
			.andExpect(header().string(HttpHeaders.LOCATION, "/"));
	}

	@Test
	@WithMockUser
	public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
		this.mockMvc.perform(post("/logout").with(csrf())
				.accept(MediaType.TEXT_HTML))
			.andExpect(status().is3xxRedirection())
			.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"));
	}
}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfTests {
	private lateinit var mockMvc: MockMvc

	@BeforeEach
	fun setUp(applicationContext: WebApplicationContext) {
		mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
			.apply<DefaultMockMvcBuilder>(springSecurity())
			.build()
	}

	@Test
	fun loginWhenValidCsrfTokenThenSuccess() {
		mockMvc.perform(post("/login").with(csrf())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().is3xxRedirection)
			.andExpect(header().string(HttpHeaders.LOCATION, "/"))
	}

	@Test
	@WithMockUser
	@Throws(Exception::class)
	fun logoutWhenValidCsrfTokenThenSuccess() {
		mockMvc.perform(post("/logout").with(csrf())
				.accept(MediaType.TEXT_HTML))
			.andExpect(status().is3xxRedirection)
			.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"))
	}
}

禁用 CSRF 保护

默认情况下,CSRF 保护处于启用状态,这会影响与后端的集成测试应用程序。 在禁用 CSRF 保护之前,请考虑它是否对应用程序有意义Spring中文文档

您还可以考虑是否只有某些端点不需要 CSRF 保护并配置忽略规则,如以下示例所示:Spring中文文档

忽略请求
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .csrf((csrf) -> csrf
                .ignoringRequestMatchers("/api/*")
            );
        return http.build();
    }
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                ignoringRequestMatchers("/api/*")
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf request-matcher-ref="csrfMatcher"/>
</http>
<b:bean id="csrfMatcher"
    class="org.springframework.security.web.util.matcher.AndRequestMatcher">
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
            <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
                <b:constructor-arg value="/api/*"/>
            </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>

如果需要禁用 CSRF 保护,可以使用以下配置执行此操作:Spring中文文档

禁用 CSRF
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf.disable());
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            csrf {
                disable()
            }
        }
        return http.build()
    }
}
<http>
	<!-- ... -->
	<csrf disabled="true"/>
</http>

CSRF 注意事项

在实施针对 CSRF 攻击的保护时,有一些特殊的注意事项。 本节讨论这些注意事项,因为它们与 servlet 环境有关。 有关更一般性的讨论,请参阅 CSRF 注意事项Spring中文文档

登录

对于登录请求,需要 CSRF 以防止伪造登录尝试非常重要。 Spring Security 的 servlet 支持开箱即用。Spring中文文档

注销

对于注销请求,必须要求 CSRF 以防止伪造注销尝试。 如果启用了CSRF保护(默认),则Spring Security将仅处理HTTP POST请求。 这可确保注销需要 CSRF 令牌,并且恶意用户无法强制注销您的用户。LogoutFilterSpring中文文档

最简单的方法是使用表单注销用户。 如果你真的想要一个链接,你可以使用 JavaScript 让链接执行 POST(可能在隐藏的表单上)。 对于禁用了 JavaScript 的浏览器,您可以选择让链接将用户带到执行 POST 的注销确认页面。Spring中文文档

如果您确实想将 HTTP GET 与注销一起使用,您可以这样做。 但是,请记住,通常不建议这样做。 例如,当使用任何 HTTP 方法请求 URL 时,以下内容将注销:/logoutSpring中文文档

使用任何 HTTP 方法注销
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.logout((logout) -> logout
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
			);
		return http.build();
	}
}
import org.springframework.security.config.annotation.web.invoke

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            // ...
            logout {
                logoutRequestMatcher = AntPathRequestMatcher("/logout")
            }
        }
        return http.build()
    }
}

有关详细信息,请参阅“注销”一章。Spring中文文档

CSRF 和会话超时

默认情况下,Spring Security 将 CSRF 令牌存储在 using HttpSessionCsrfTokenRepository 中。 这可能会导致会话过期,没有 CSRF 令牌可供验证。HttpSessionSpring中文文档

我们已经讨论了会话超时的一般解决方案。 本节讨论 CSRF 超时的细节,因为它与 servlet 支持有关。Spring中文文档

您可以将 CSRF 令牌的存储更改为 cookie 中。 有关详细信息,请参阅使用 CookieCsrfTokenRepository 部分。Spring中文文档

如果令牌确实过期,您可能希望通过指定自定义 AccessDeniedHandler 来自定义它的处理方式。 自定义可以按照您喜欢的任何方式进行处理。AccessDeniedHandlerInvalidCsrfTokenExceptionSpring中文文档

分片(文件上传)

我们已经讨论过如何保护多部分请求(文件上传)免受 CSRF 攻击,从而导致先有鸡还是先有蛋的问题。 当 JavaScript 可用时,我们建议在 HTTP 请求标头中包含 CSRF 令牌以回避该问题。Spring中文文档

如果 JavaScript 不可用,以下各节将讨论将 CSRF 令牌放置在 servlet 应用程序的正文url 中的选项。Spring中文文档

您可以在 Spring 参考的 Multipart Resolver 部分和 MultipartFilter javadoc 中找到有关将多部分表单与 Spring 配合使用的更多信息。Spring中文文档

将 CSRF 令牌放在正文中

我们已经讨论了将 CSRF 代币放在正文中的权衡。 在本节中,我们将讨论如何配置 Spring Security 以从正文中读取 CSRF。Spring中文文档

要从正文中读取 CSRF 令牌,请在 Spring Security 过滤器之前指定。 在 Spring Security 过滤器之前指定 意味着没有调用 的授权,这意味着任何人都可以在您的服务器上放置临时文件。 但是,只有授权用户才能提交由您的应用程序处理的文件。 通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。MultipartFilterMultipartFilterMultipartFilterSpring中文文档

配置MultipartFilter
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
		insertFilters(servletContext, new MultipartFilter());
	}
}
class SecurityApplicationInitializer : AbstractSecurityWebApplicationInitializer() {
    override fun beforeSpringSecurityFilterChain(servletContext: ServletContext?) {
        insertFilters(servletContext, MultipartFilter())
    }
}
<filter>
	<filter-name>MultipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>MultipartFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

要确保在具有 XML 配置的 Spring Security 过滤器之前指定该元素,您可以确保将 的元素放在文件内的 之前。MultipartFilter<filter-mapping>MultipartFilterspringSecurityFilterChainweb.xmlSpring中文文档

在 URL 中包含 CSRF 令牌

如果不允许未经授权的用户上传临时文件是不可接受的,另一种方法是将 放在 Spring Security 过滤器之后,并将 CSRF 作为查询参数包含在表单的 action 属性中。 由于 公开为名为 _csrfHttpServletRequest 属性,因此我们可以使用它来创建一个包含 CSRF 令牌的属性。 以下示例使用 JSP 执行此操作:MultipartFilterCsrfTokenactionSpring中文文档

CSRF 代币在行动
<form method="post"
	action="./upload?${_csrf.parameterName}=${_csrf.token}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

我们已经讨论了将 CSRF 代币放入正文中的权衡。Spring中文文档

在 Spring 的 Servlet 支持中,使用 HiddenHttpMethodFilter 覆盖 HTTP 方法。 您可以在参考文档的 HTTP 方法转换部分找到更多信息。Spring中文文档

您可以在 Spring 参考的 Multipart Resolver 部分和 MultipartFilter javadoc 中找到有关将多部分表单与 Spring 配合使用的更多信息。Spring中文文档

要确保在具有 XML 配置的 Spring Security 过滤器之前指定该元素,您可以确保将 的元素放在文件内的 之前。MultipartFilter<filter-mapping>MultipartFilterspringSecurityFilterChainweb.xmlSpring中文文档

延伸阅读

现在,您已经查看了 CSRF 保护,请考虑了解有关漏洞利用保护(包括安全标头HTTP 防火墙)的更多信息,或者继续学习如何测试应用程序。Spring中文文档