跨站点请求伪造 (CSRF)

在最终用户可以登录的应用程序中,重要的是要考虑如何防范跨站点请求伪造 (CSRF)

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

配置 CSRF 防护
  • Java

  • Kotlin

  • XML

@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 保护的更多信息,请考虑以下用例

了解 CSRF 保护的组件

CSRF 保护由几个组件提供,这些组件组合在 CsrfFilter

csrf
图 1. CsrfFilter 组件

CSRF 保护分为两个部分

  1. 通过委托给 CsrfTokenRequestHandler,使应用程序能够使用 CsrfToken

  2. 确定请求是否需要 CSRF 保护,加载和验证令牌,以及 处理 AccessDeniedException

csrf processing
图 2. CsrfFilter 处理
  • number 1 首先,加载 DeferredCsrfToken,它包含对 CsrfTokenRepository 的引用,以便稍后(在 number 4 中)加载持久化的 CsrfToken

  • number 2 其次,将 Supplier<CsrfToken>(从 DeferredCsrfToken 创建)提供给 CsrfTokenRequestHandler,它负责填充请求属性,使应用程序的其余部分可以使用 CsrfToken

  • number 3 接下来,开始主要的 CSRF 保护处理,并检查当前请求是否需要 CSRF 保护。如果不需要,则继续过滤器链并结束处理。

  • number 4 如果需要 CSRF 保护,则最终会从 DeferredCsrfToken 中加载持久化的 CsrfToken

  • number 5 继续,使用 CsrfTokenRequestHandler 解析客户端提供的实际 CSRF 令牌(如果有)。

  • number 6 将实际 CSRF 令牌与持久化的 CsrfToken 进行比较。如果有效,则继续过滤器链并结束处理。

  • number 7 如果实际 CSRF 令牌无效(或缺失),则将 AccessDeniedException 传递给 AccessDeniedHandler 并结束处理。

迁移到 Spring Security 6

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

  • CsrfToken 的加载现在 默认情况下是延迟的,以通过不再要求在每个请求上加载会话来提高性能。

  • CsrfToken 现在默认情况下在每个请求上都包含 随机性,以保护 CSRF 令牌免受 BREACH 攻击。

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

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

持久化 CsrfToken

CsrfToken 使用 CsrfTokenRepository 持久化。

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

使用 HttpSessionCsrfTokenRepository

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

HttpSessionCsrfTokenRepository 默认情况下从名为 X-CSRF-TOKEN 的 HTTP 请求标头或请求参数 _csrf 中读取令牌。

您可以使用以下配置显式指定默认配置

配置 HttpSessionCsrfTokenRepository
  • Java

  • Kotlin

  • XML

@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"/>

您可以将 CsrfToken 持久化到 cookie 中,以 支持基于 JavaScript 的应用程序,方法是使用 CookieCsrfTokenRepository.

CookieCsrfTokenRepository 默认情况下会写入名为 XSRF-TOKEN 的 cookie,并从名为 X-XSRF-TOKEN 的 HTTP 请求头或请求参数 _csrf 中读取它。这些默认值来自 Angular 及其前身 AngularJS.

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

您可以使用以下配置来配置 CookieCsrfTokenRepository

此示例显式将 HttpOnly 设置为 false。这是为了让 JavaScript 框架(如 Angular)能够读取它。如果您不需要直接使用 JavaScript 读取 cookie 的功能,我们建议省略 HttpOnly(通过使用 new CookieCsrfTokenRepository() 代替),以提高安全性。

自定义 CsrfTokenRepository

在某些情况下,您可能希望实现自定义的 CsrfTokenRepository.

实现 CsrfTokenRepository 接口后,您可以使用以下配置来配置 Spring Security 以使用它

配置自定义 CsrfTokenRepository
  • Java

  • Kotlin

  • XML

@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 提供给应用程序。此组件还负责从 HTTP 标头或请求参数中解析 CsrfToken

默认情况下,XorCsrfTokenRequestAttributeHandler 用于提供对 CsrfTokenBREACH 保护。Spring Security 还提供 CsrfTokenRequestAttributeHandler 用于选择退出 BREACH 保护。您也可以指定 您自己的实现 来自定义处理和解析令牌的策略。

使用 XorCsrfTokenRequestAttributeHandler (BREACH)

XorCsrfTokenRequestAttributeHandler 使 CsrfToken 作为名为 _csrfHttpServletRequest 属性可用,并额外提供对 BREACH 的保护。

CsrfToken 也作为请求属性使用名称 CsrfToken.class.getName() 可用。此名称不可配置,但名称 _csrf 可以使用 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改。

此实现还从请求中解析令牌值,作为请求头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)或请求参数(默认情况下为 _csrf)。

BREACH 保护通过将随机性编码到 CSRF 令牌值中来提供,以确保返回的 CsrfToken 在每次请求时都发生变化。当令牌稍后作为头值或请求参数解析时,它会被解码以获取原始令牌,然后将其与 持久化的 CsrfToken 进行比较。

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

配置 BREACH 保护
  • Java

  • Kotlin

  • XML

@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 使 CsrfToken 作为名为 _csrfHttpServletRequest 属性可用。

CsrfToken 也作为请求属性使用名称 CsrfToken.class.getName() 可用。此名称不可配置,但名称 _csrf 可以使用 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改。

此实现还从请求中解析令牌值,作为请求头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN)或请求参数(默认情况下为 _csrf)。

CsrfTokenRequestAttributeHandler 的主要用途是选择退出对 CsrfToken 的 BREACH 保护,这可以使用以下配置进行配置

选择退出 BREACH 保护
  • Java

  • Kotlin

  • XML

@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 接口来自定义处理和解析令牌的策略。

CsrfTokenRequestHandler 接口是一个 @FunctionalInterface,可以使用 lambda 表达式来实现,以自定义请求处理。您需要实现完整的接口来自定义如何从请求中解析令牌。请参阅 为单页应用程序配置 CSRF,了解使用委托来实现自定义策略以处理和解析令牌的示例。

实现 CsrfTokenRequestHandler 接口后,您可以使用以下配置将 Spring Security 配置为使用它

配置自定义 CsrfTokenRequestHandler
  • Java

  • Kotlin

  • XML

@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"/>

延迟加载 CsrfToken

默认情况下,Spring Security 会延迟加载 CsrfToken,直到需要它为止。

当使用 不安全的 HTTP 方法(例如 POST)发出请求时,需要 CsrfToken。此外,任何将令牌渲染到响应的请求也需要它,例如包含 <form> 标签的网页,该标签包含用于 CSRF 令牌的隐藏 <input>

由于 Spring Security 默认情况下还将 CsrfToken 存储在 HttpSession 中,因此延迟的 CSRF 令牌可以通过不需要在每次请求时加载会话来提高性能。

如果您想选择退出延迟令牌并使 CsrfToken 在每次请求时加载,可以使用以下配置

选择退出延迟 CSRF 令牌
  • Java

  • Kotlin

  • XML

@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>

通过将 csrfRequestAttributeName 设置为 null,必须首先加载 CsrfToken 才能确定要使用的属性名称。这会导致 CsrfToken 在每次请求时加载。

与 CSRF 保护集成

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

以下部分描述了前端或客户端应用程序与受 CSRF 保护的后端应用程序集成的各种方式

HTML 表单

要提交 HTML 表单,必须将 CSRF 令牌作为隐藏输入包含在表单中。例如,渲染的 HTML 可能如下所示

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

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

如果这些选项不可用,您可以利用以下事实:CsrfToken 作为 HttpServletRequest 属性名为 _csrf 公开。以下示例使用 JSP 完成此操作

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 令牌,而不是请求参数。

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

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

您可以在以下部分了解不同类型的 JavaScript 应用程序

单页应用程序

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

回想一下,Spring Security 默认情况下提供 CsrfToken 的 BREACH 保护。当将预期的 CSRF 令牌存储 在 cookie 中 时,JavaScript 应用程序将只能访问纯令牌值,而 *不会* 访问编码值。将需要提供一个 自定义请求处理程序 来解析实际令牌值。

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

在身份验证成功和注销成功后刷新令牌是必需的,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除之前的令牌。客户端应用程序将无法执行不安全的 HTTP 请求(例如 POST),而无需获取新的令牌。

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

为单页应用程序配置 CSRF
  • Java

  • Kotlin

  • XML

@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 配置 CookieCsrfTokenRepository 并将 HttpOnly 设置为 false,以便 JavaScript 应用程序可以读取 cookie。
2 配置一个自定义的 CsrfTokenRequestHandler,它根据 CSRF 令牌是 HTTP 请求头 (X-XSRF-TOKEN) 还是请求参数 (_csrf) 来解析 CSRF 令牌。
3 配置一个自定义的 Filter,以便在每个请求上加载 CsrfToken,如果需要,它将返回一个新的 cookie。

多页面应用程序

对于在每个页面上加载 JavaScript 的多页面应用程序,除了将 CSRF 令牌暴露在 cookie 中 之外,还可以将 CSRF 令牌包含在您的 meta 标签中。HTML 可能看起来像这样

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 来实现这一点

使用请求属性的 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>

一旦元标签包含了 CSRF 令牌,JavaScript 代码就可以读取元标签并将 CSRF 令牌作为头信息包含在内。如果您使用 jQuery,您可以使用以下代码来实现这一点

在 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 令牌。

实现这一点的一种方法是使用带有 CsrfTokenArgumentResolver@ControllerAdvice。以下是适用于应用程序中所有控制器端点的 @ControllerAdvice 的示例

HTTP 响应头中的 CSRF 令牌
  • Java

  • Kotlin

@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 Security 过滤器链之后调用的。这意味着,只有当请求通过过滤器链到达您的应用程序时,才会应用此 @ControllerAdvice。有关在过滤器链中添加过滤器以更早地访问 HttpServletResponse 的示例,请参见 单页面应用程序 的配置。

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

移动应用程序

JavaScript 应用程序 类似,移动应用程序通常使用 JSON 而不是 HTML。提供浏览器流量的后台应用程序可以选择 禁用 CSRF。在这种情况下,不需要额外的操作。

但是,一个也提供浏览器流量的后台应用程序,因此仍然需要 CSRF 保护,可以继续将 CsrfToken 存储在会话中,而不是 存储在 Cookie 中

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

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

以下是使用 CsrfTokenArgumentResolver/csrf 端点的示例

/csrf 端点
  • Java

  • Kotlin

@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()

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

在身份验证成功和注销成功后刷新令牌是必需的,因为 CsrfAuthenticationStrategyCsrfLogoutHandler 将清除之前的令牌。客户端应用程序将无法执行不安全的 HTTP 请求(例如 POST),而无需获取新的令牌。

获得 CSRF 令牌后,您需要将其作为 HTTP 请求头(默认情况下为 X-CSRF-TOKENX-XSRF-TOKEN 之一)自己包含。

处理 AccessDeniedException

要处理 AccessDeniedException(例如 InvalidCsrfTokenException),您可以配置 Spring Security 以任何您喜欢的方式处理这些异常。例如,您可以使用以下配置配置自定义拒绝访问页面

配置 AccessDeniedHandler
  • Java

  • Kotlin

  • XML

@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 防护,例如

测试 CSRF 防护
  • Java

  • Kotlin

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
	public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
		this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden());
	}

	@Test
	public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
		this.mockMvc.perform(post("/login")
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden());
	}

	@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
	fun loginWhenInvalidCsrfTokenThenForbidden() {
		mockMvc.perform(post("/login").with(csrf().useInvalidToken())
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden)
	}

	@Test
	fun loginWhenMissingCsrfTokenThenForbidden() {
		mockMvc.perform(post("/login")
				.accept(MediaType.TEXT_HTML)
				.param("username", "user")
				.param("password", "password"))
			.andExpect(status().isForbidden)
	}

	@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 防护之前,请考虑它是否 对您的应用程序有意义

您也可以考虑是否只有某些端点不需要 CSRF 防护,并配置一个忽略规则,如下例所示

忽略请求
  • Java

  • Kotlin

  • XML

@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 防护,可以使用以下配置

禁用 CSRF
  • Java

  • Kotlin

  • XML

@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 注意事项

登录

重要的是要 要求登录请求使用 CSRF,以防止伪造登录尝试。Spring Security 的 servlet 支持开箱即用地执行此操作。

注销

重要的是要 要求注销请求使用 CSRF,以防止伪造注销尝试。如果启用了 CSRF 防护(默认情况下),Spring Security 的 LogoutFilter 将仅处理 HTTP POST 请求。这确保注销需要 CSRF 令牌,并且恶意用户无法强制注销您的用户。

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

如果您确实想使用 HTTP GET 进行注销,您可以这样做。但是,请记住,这通常不建议这样做。例如,当使用任何 HTTP 方法请求/logout URL 时,以下操作将注销

使用任何 HTTP 方法注销
  • Java

  • Kotlin

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

有关更多信息,请参阅注销章节。

CSRF 和会话超时

默认情况下,Spring Security 使用HttpSessionCsrfTokenRepository将 CSRF 令牌存储在HttpSession中。这会导致会话过期的情况,从而导致没有 CSRF 令牌可供验证。

我们已经讨论了会话超时的通用解决方案。本节讨论与 servlet 支持相关的 CSRF 超时的具体情况。

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

如果令牌确实过期,您可能希望通过指定自定义AccessDeniedHandler来定制其处理方式。自定义AccessDeniedHandler可以按您喜欢的方式处理InvalidCsrfTokenException

多部分(文件上传)

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

如果 JavaScript 不可用,以下部分将讨论在 servlet 应用程序中将 CSRF 令牌放置在主体URL中的选项。

您可以在 Spring 参考的多部分解析器部分和MultipartFilter javadoc中找到有关在 Spring 中使用多部分表单的更多信息。

将 CSRF 令牌放置在主体中

我们已经讨论过将 CSRF 令牌放置在主体中的权衡。在本节中,我们将讨论如何配置 Spring Security 以从主体读取 CSRF。

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

配置 MultipartFilter
  • Java

  • Kotlin

  • XML

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 过滤器之前指定,您可以确保 MultipartFilter<filter-mapping> 元素位于 web.xml 文件中 springSecurityFilterChain 之前。

在 URL 中包含 CSRF 令牌

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

操作中的 CSRF 令牌
<form method="post"
	action="./upload?${_csrf.parameterName}=${_csrf.token}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

我们已经讨论了将 CSRF 令牌放置在主体中的权衡取舍。

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

进一步阅读

现在您已经回顾了 CSRF 保护,请考虑进一步了解 漏洞保护,包括 安全头HTTP 防火墙,或者继续学习如何 测试 您的应用程序。