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

跨站点请求伪造 (CSRF)

在最终用户可以登录的应用程序中,重要的是要考虑如何防止跨站点请求伪造 (CSRF)。spring-doc.cadn.net.cn

默认情况下, Spring Security 可以防止针对不安全的 HTTP 方法(例如 POST 请求)的 CSRF 攻击,因此不需要额外的代码。 您可以使用以下内容显式指定默认配置:spring-doc.cadn.net.cn

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

了解 CSRF 保护的组件

CSRF 保护由多个组件提供,这些组件组成在CsrfFilter:spring-doc.cadn.net.cn

CSRF (企业社会责任)
图 1.CsrfFilter组件

CSRF 保护分为两部分:spring-doc.cadn.net.cn

  1. 使CsrfTokenavailable 提供给应用程序,方法是将CsrfTokenRequestHandler.spring-doc.cadn.net.cn

  2. 确定请求是否需要 CSRF 保护,加载并验证令牌,以及处理AccessDeniedException.spring-doc.cadn.net.cn

CSRF 处理
图 2.CsrfFilter加工

迁移到 Spring Security 6

从 Spring Security 5 迁移到 6 时,有一些更改可能会影响您的应用程序。 以下是 Spring Security 6 中已更改的 CSRF 保护方面的概述:spring-doc.cadn.net.cn

Spring Security 6 中的更改需要对单页应用程序进行额外配置,因此您可能会发现 Single-Page Applications 部分特别有用。spring-doc.cadn.net.cn

有关迁移 Spring Security 5 应用程序的更多信息,请参见迁移一章的 Exploit Protection 部分。spring-doc.cadn.net.cn

持久化CsrfToken

CsrfToken使用CsrfTokenRepository.spring-doc.cadn.net.cn

默认情况下,HttpSessionCsrfTokenRepository用于在 Session 中存储令牌。 Spring Security 还提供了CookieCsrfTokenRepository用于在 Cookie 中存储令牌。 您还可以指定自己的 implementation 以将 token 存储在您喜欢的任何位置。spring-doc.cadn.net.cn

使用HttpSessionCsrfTokenRepository

默认情况下,Spring Security 将预期的 CSRF 令牌存储在HttpSession通过使用HttpSessionCsrfTokenRepository,因此不需要其他代码。spring-doc.cadn.net.cn

HttpSessionCsrfTokenRepository从名为X-CSRF-TOKEN或 request 参数_csrf默认情况下。spring-doc.cadn.net.cn

您可以使用以下配置显式指定默认配置:spring-doc.cadn.net.cn

配置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写入名为XSRF-TOKEN并从名为X-XSRF-TOKEN或 request 参数_csrf默认情况下。 这些默认值来自 Angular 及其前身 AngularJS。spring-doc.cadn.net.cn

有关此主题的最新信息,请参阅跨站点请求伪造 (XSRF) 保护指南和 HttpClientXsrfModulespring-doc.cadn.net.cn

您可以配置CookieCsrfTokenRepository使用以下配置:spring-doc.cadn.net.cn

该示例显式地将HttpOnlyfalse. 这是让 JavaScript 框架(比如 Angular)读取它所必需的。 如果您不需要直接使用 JavaScript 读取 cookie,我们建议省略HttpOnly(通过使用new CookieCsrfTokenRepository()而是)来提高安全性。spring-doc.cadn.net.cn

自定义CsrfTokenRepository

在某些情况下,您可能希望实现自定义CsrfTokenRepository.spring-doc.cadn.net.cn

实施CsrfTokenRepository接口,你可以将 Spring Security 配置为通过以下配置来使用它:spring-doc.cadn.net.cn

配置自定义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"/>

处理CsrfToken

CsrfToken可供应用程序使用CsrfTokenRequestHandler. 此组件还负责解析CsrfToken从 HTTP 标头或请求参数。spring-doc.cadn.net.cn

默认情况下,XorCsrfTokenRequestAttributeHandler用于提供CsrfToken. Spring Security 还提供了CsrfTokenRequestAttributeHandler选择退出 BREACH 保护。 您还可以指定自己的实现来自定义处理和解析令牌的策略。spring-doc.cadn.net.cn

使用XorCsrfTokenRequestAttributeHandler(泄露)

XorCsrfTokenRequestAttributeHandler使CsrfTokenHttpServletRequest属性调用_csrf,此外还为 BREACH 提供保护。spring-doc.cadn.net.cn

CsrfToken也可以使用名称CsrfToken.class.getName(). 此名称不可配置,但名称_csrf可以使用XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName.spring-doc.cadn.net.cn

此实现还将请求中的令牌值解析为请求标头(X-CSRF-TOKENX-XSRF-TOKEN)或请求参数 (_csrf默认情况下)。spring-doc.cadn.net.cn

BREACH 保护是通过将随机性编码到 CSRF 令牌值中来提供的,以确保返回的CsrfToken更改每个请求。 当令牌稍后解析为标头值或请求参数时,将对其进行解码以获取原始令牌,然后将其与坚持CsrfToken.spring-doc.cadn.net.cn

默认情况下, Spring Security 保护 CSRF 令牌免受 BREACH 攻击,因此不需要额外的代码。 您可以使用以下配置显式指定默认配置:spring-doc.cadn.net.cn

配置 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

CsrfTokenRequestAttributeHandler使CsrfTokenHttpServletRequest属性调用_csrf.spring-doc.cadn.net.cn

CsrfToken也可以使用名称CsrfToken.class.getName(). 此名称不可配置,但名称_csrf可以使用CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName.spring-doc.cadn.net.cn

此实现还将请求中的令牌值解析为请求标头(X-CSRF-TOKENX-XSRF-TOKEN)或请求参数 (_csrf默认情况下)。spring-doc.cadn.net.cn

主要用途CsrfTokenRequestAttributeHandler是选择退出 BREACH 保护CsrfToken,可以使用以下配置进行配置:spring-doc.cadn.net.cn

选择退出 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

您可以实现CsrfTokenRequestHandler接口自定义处理和解析 Token 的策略。spring-doc.cadn.net.cn

CsrfTokenRequestHandlerinterface 是一个@FunctionalInterface这可以使用 Lambda 表达式来实现,以自定义请求处理。 您需要实现完整的接口来自定义如何从请求中解析令牌。 有关使用委派实现用于处理和解析令牌的自定义策略的示例,请参阅Configure CSRF for Single-Page Applicationspring-doc.cadn.net.cn

实施CsrfTokenRequestHandler接口,你可以将 Spring Security 配置为通过以下配置来使用它:spring-doc.cadn.net.cn

配置自定义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"/>

Deferred Loading 的CsrfToken

默认情况下,Spring Security 会延迟CsrfToken直到需要为止。spring-doc.cadn.net.cn

CsrfToken每当使用不安全的 HTTP 方法(如 POST)发出请求时,都需要。 此外,将令牌呈现到响应的任何请求都需要它,例如带有<form>标记中包含隐藏的<input>对于 CSRF 令牌。spring-doc.cadn.net.cn

由于 Spring Security 还会存储CsrfTokenHttpSession默认情况下,延迟的 CSRF 令牌可以通过不要求在每个请求上加载 session 来提高性能。spring-doc.cadn.net.cn

如果您想选择退出延迟令牌,并导致CsrfToken要在每个请求上加载,您可以使用以下配置进行加载:spring-doc.cadn.net.cn

选择退出延迟的 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>

通过设置csrfRequestAttributeNamenullCsrfToken必须首先加载以确定要使用的属性名称。 这会导致CsrfToken在每个请求时加载。spring-doc.cadn.net.cn

与 CSRF 保护集成

为了使同步器令牌模式防止 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。 这必须包含在浏览器不会自动包含在 HTTP 请求中的请求部分(表单参数、HTTP 标头或其他部分)中。spring-doc.cadn.net.cn

以下部分描述了前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式:spring-doc.cadn.net.cn

HTML 表单

要提交 HTML 表单,CSRF 令牌必须作为隐藏输入包含在表单中。 例如,呈现的 HTML 可能如下所示:spring-doc.cadn.net.cn

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

以下视图技术会自动将实际的 CSRF 令牌包含在具有不安全 HTTP 方法的形式中,例如 POST:spring-doc.cadn.net.cn

如果这些选项不可用,您可以利用CsrfToken公开为HttpServletRequest名为_csrf. 以下示例使用 JSP 执行此作:spring-doc.cadn.net.cn

HTML 表单中具有 Request 属性的 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-doc.cadn.net.cn

为了获得 CSRF 令牌,你可以将 Spring Security 配置为将预期的 CSRF 令牌存储在 cookie 中。 通过将预期的令牌存储在 cookie 中,JavaScript 框架(如 Angular)可以自动将实际的 CSRF 令牌作为 HTTP 请求标头包含。spring-doc.cadn.net.cn

在将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,对 BREACH 保护和延迟令牌有一些特殊注意事项。 下一节将提供完整的配置示例。spring-doc.cadn.net.cn

您可以在以下各节中了解不同类型的 JavaScript 应用程序:spring-doc.cadn.net.cn

单页应用程序

将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,有一些特殊的注意事项。spring-doc.cadn.net.cn

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

此外,在鉴权成功和注销成功时,存储 CSRF Token 的 Cookie 将被清空。 默认情况下, Spring Security 会延迟加载新的 CSRF 令牌,并且需要额外的工作才能返回新的 cookie。spring-doc.cadn.net.cn

需要在身份验证成功和注销成功后刷新令牌,因为CsrfAuthenticationStrategyCsrfLogoutHandler将清除之前的 Token。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求,例如 POST。spring-doc.cadn.net.cn

为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:spring-doc.cadn.net.cn

为单页应用程序配置 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 配置CookieCsrfTokenRepositoryHttpOnly设置为false,以便 JavaScript 应用程序可以读取 Cookie。
2 配置自定义CsrfTokenRequestHandler,根据 CSRF 令牌是否为 HTTP 请求标头 (X-XSRF-TOKEN) 或请求参数 (_csrf).
3 配置自定义Filter加载CsrfToken在每个请求中,如果需要,它将返回一个新的 Cookie。

多页应用程序

对于在每个页面上加载 JavaScript 的多页面应用程序,在 cookie 中公开 CSRF 令牌的另一种方法是将 CSRF 令牌包含在meta标签。 HTML 可能如下所示:spring-doc.cadn.net.cn

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

为了在请求中包含 CSRF 令牌,您可以利用CsrfToken公开为HttpServletRequest名为_csrf. 以下示例使用 JSP 执行此作:spring-doc.cadn.net.cn

HTML Meta 标记中具有 Request 属性的 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-doc.cadn.net.cn

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

实现此目的的一种方法是使用@ControllerAdvice使用CsrfTokenArgumentResolver. 以下是@ControllerAdvice这适用于应用程序中的所有控制器端点:spring-doc.cadn.net.cn

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

}

因为这个@ControllerAdvice应用于应用程序中的所有端点,则会导致在每个请求上加载 CSRF 令牌,这可能会抵消在使用HttpSessionCsrfTokenRepository. 但是,在使用CookieCsrfTokenRepository.spring-doc.cadn.net.cn

重要的是要记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后调用的。 这意味着@ControllerAdvice仅当请求通过筛选条件链传递到您的应用程序时,才会应用。 请参阅 单页应用程序的配置 ,了解向过滤器链添加过滤器以尽早访问HttpServletResponse.spring-doc.cadn.net.cn

CSRF 令牌现在将在响应标头 (X-CSRF-TOKENX-XSRF-TOKEN默认情况下),对于控制器建议适用的任何自定义终端节点。 对后端的任何请求都可用于从响应中获取令牌,并且后续请求可以将令牌包含在具有相同名称的请求标头中。spring-doc.cadn.net.cn

移动应用程序

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

但是,如果后端应用程序也提供浏览器流量,因此仍然需要 CSRF 保护,则可以继续存储CsrfToken 在 session 中,而不是在 cookie 中spring-doc.cadn.net.cn

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

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

以下是/csrf端点,该端点使用CsrfTokenArgumentResolver:spring-doc.cadn.net.cn

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

在启动或初始化应用程序时(例如,在加载时),以及在身份验证成功和注销成功后,应调用此端点以获取 CSRF 令牌。spring-doc.cadn.net.cn

需要在身份验证成功和注销成功后刷新令牌,因为CsrfAuthenticationStrategyCsrfLogoutHandler将清除之前的 Token。 如果不获取新令牌,客户端应用程序将无法执行不安全的 HTTP 请求,例如 POST。spring-doc.cadn.net.cn

获得 CSRF 令牌后,您需要将其作为 HTTP 请求标头(以下X-CSRF-TOKENX-XSRF-TOKEN默认情况下)自己。spring-doc.cadn.net.cn

处理AccessDeniedException

要处理AccessDeniedExceptionInvalidCsrfTokenException,您可以配置 Spring Security 以您喜欢的任何方式处理这些异常。 例如,您可以使用以下配置配置自定义 access denied 页面:spring-doc.cadn.net.cn

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

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

您还可以考虑是否只有某些终端节点不需要 CSRF 保护并配置忽略规则,如以下示例所示:spring-doc.cadn.net.cn

忽略请求
@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-doc.cadn.net.cn

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

登录

要求对登录请求使用 CSRF 以防止伪造登录尝试非常重要。 Spring Security 的 servlet 支持是开箱即用的。spring-doc.cadn.net.cn

注销

对于注销请求,要求 CSRF 以防止伪造注销尝试,这一点很重要。 如果启用了 CSRF 保护(默认),则 Spring Security 的LogoutFilter将仅处理 HTTP POST 请求。 这可确保注销需要 CSRF 令牌,并且恶意用户无法强制注销您的用户。spring-doc.cadn.net.cn

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

如果您确实想将 HTTP GET 与 logout 一起使用,则可以这样做。 但是,请记住,这通常不建议这样做。 例如,当/logout使用任何 HTTP 方法请求 URL:spring-doc.cadn.net.cn

使用任何 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()
    }
}

有关更多信息,请参阅 Logout 章节。spring-doc.cadn.net.cn

CSRF 和会话超时

默认情况下,Spring Security 将 CSRF 令牌存储在HttpSession使用HttpSessionCsrfTokenRepository. 这可能会导致会话过期,没有留下 CSRF 令牌进行验证。spring-doc.cadn.net.cn

我们已经讨论了会话超时的一般解决方案。 本节讨论与 Servlet 支持相关的 CSRF 超时的细节。spring-doc.cadn.net.cn

您可以将 CSRF 令牌的存储更改为在 cookie 中。 有关详细信息,请参阅使用CookieCsrfTokenRepository部分。spring-doc.cadn.net.cn

如果令牌确实过期,您可能希望通过指定习惯AccessDeniedHandler. 自定义AccessDeniedHandler可以处理InvalidCsrfTokenException随便你怎么。spring-doc.cadn.net.cn

分段(文件上传)

我们已经讨论了保护分段请求(文件上传)免受 CSRF 攻击如何导致先有鸡还是先有蛋的问题。 当 JavaScript 可用时,我们建议在 HTTP 请求标头中包含 CSRF 令牌,以避免该问题。spring-doc.cadn.net.cn

如果 JavaScript 不可用,以下各节将讨论在 Servlet 应用程序中将 CSRF 令牌放置在 body 和 url 中的选项。spring-doc.cadn.net.cn

您可以在 Spring 参考的 Multipart Resolver 部分中找到有关在 Spring 中使用多部分表单的更多信息,以及MultipartFilterJavadoc.spring-doc.cadn.net.cn

将 CSRF 令牌放在正文中

我们已经讨论了将 CSRF 令牌放在 body 中的权衡。 在本节中,我们将讨论如何配置 Spring Security 以从正文中读取 CSRF。spring-doc.cadn.net.cn

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

配置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>

要确保MultipartFilter在具有 XML 配置的 Spring Security 过滤器之前指定,则可以确保<filter-mapping>元素的MultipartFilter放置在springSecurityFilterChainweb.xml文件。spring-doc.cadn.net.cn

在 URL 中包含 CSRF 令牌

如果不允许未经授权的用户上传临时文件,另一种方法是将MultipartFilter在 Spring Security 过滤器之后,并将 CSRF 作为查询参数包含在表单的 action 属性中。 由于CsrfToken公开为HttpServletRequest名为_csrf,我们可以使用它来创建一个action其中包含 CSRF 令牌。 以下示例使用 JSP 执行此作:spring-doc.cadn.net.cn

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

隐藏的HttpMethodFilter

我们已经讨论了将 CSRF 令牌放入 body 中的权衡。spring-doc.cadn.net.cn

在 Spring 的 Servlet 支持中,覆盖 HTTP 方法是通过使用HiddenHttpMethodFilter. 您可以在参考文档的 HTTP 方法转换部分找到更多信息。spring-doc.cadn.net.cn

延伸阅读

现在您已经查看了 CSRF 保护,请考虑了解有关漏洞利用保护的更多信息,包括安全标头HTTP 防火墙,或者继续学习如何测试您的应用程序。spring-doc.cadn.net.cn