跨站请求伪造 (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 中的更改对单页应用 (SPA) 需要额外的配置,因此你可能会发现单页应用部分特别有用。

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

持久化 CsrfToken

CsrfToken 使用 CsrfTokenRepository 进行持久化。

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

使用 HttpSessionCsrfTokenRepository

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

HttpSessionCsrfTokenRepository 从会话(无论是内存、缓存还是数据库)中读取令牌。如果你需要直接访问会话属性,请首先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName 配置会话属性名称。

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

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

你可以使用CookieCsrfTokenRepositoryCsrfToken 持久化到 cookie 中,以支持基于 JavaScript 的应用

默认情况下,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)

XorCsrfTokenRequestAttributeHandlerCsrfToken 作为名为 _csrfHttpServletRequest 属性提供,并额外提供 BREACH 防护。

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

此实现还从请求中解析令牌值,可以是请求头(默认是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

CsrfTokenRequestAttributeHandlerCsrfToken 作为名为 _csrfHttpServletRequest 属性提供。

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

此实现还从请求中解析令牌值,可以是请求头(默认是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。此外,任何将令牌渲染到响应中的请求也需要它,例如包含用于 CSRF 令牌的隐藏 <input><form> 标签的网页。

因为 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 作为名为_csrfHttpServletRequest 属性公开的事实。以下示例使用 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,可以将 CSRF 令牌包含在 HTTP 请求头中,而不是请求参数中进行提交。

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

将单页应用 (SPA) 与 Spring Security 的 CSRF 防护集成时,需要特别考虑 BREACH 防护和延迟加载的令牌。完整配置示例在下一节提供。

你可以在以下部分阅读有关不同类型 JavaScript 应用的信息

单页应用

将单页应用 (SPA) 与 Spring Security 的 CSRF 防护集成时需要特别考虑。

回想一下,Spring Security 默认提供对 CsrfTokenBREACH 防护。当将预期的 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)
			);
		return http.build();
	}
}

final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
	private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
	private final CsrfTokenRequestHandler xor = 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.xor.handle(request, response, csrfToken);
		/*
		 * Render the token value to a cookie by causing the deferred token to be loaded.
		 */
		csrfToken.get();
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		String headerValue = request.getHeader(csrfToken.getHeaderName());
		/*
		 * 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.
		 *
		 * 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 (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
	}
}
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)
            }
        }
        return http.build()
    }
}

class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
    private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
    private val xor: 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.
         */
        xor.handle(request, response, csrfToken)
        /*
         * Render the token value to a cookie by causing the deferred token to be loaded.
         */
        csrfToken.get()
    }

    override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
        val headerValue = request.getHeader(csrfToken.headerName)
        /*
         * 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(headerValue)) {
            plain
        } 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.
             */
            xor
        }.resolveCsrfTokenValue(request, csrfToken)
    }
}
<http>
	<!-- ... -->
	<csrf
		token-repository-ref="tokenRepository"                                        (1)
		request-handler-ref="requestHandler"/>                                        (2)
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
	class="example.SpaCsrfTokenRequestHandler"/>
1 配置 CookieCsrfTokenRepository 并将 HttpOnly 设置为 false,以便 JavaScript 应用可以读取 cookie。
2 配置一个自定义的 CsrfTokenRequestHandler,它根据 CSRF 令牌是 HTTP 请求头(X-XSRF-TOKEN)还是请求参数(_csrf)来解析令牌。此实现还会导致延迟加载的 CsrfToken 在每个请求上都加载,如果需要,会返回一个新的 cookie。

多页应用

对于 JavaScript 在每个页面上加载的多页应用,除了在cookie 中公开 CSRF 令牌外,另一种选择是将 CSRF 令牌包含在你的 meta 标签中。HTML 可能看起来像这样

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

为了在请求中包含 CSRF 令牌,你可以利用 CsrfToken 作为名为_csrfHttpServletRequest 属性公开的事实。以下示例使用 JSP 实现这一点

包含请求属性的 HTML Meta 标签中的 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,可以使用以下代码实现

在 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 应用来说,另一种选择是将 CSRF 令牌包含在 HTTP 响应头中。

实现此目标的一种方法是使用带有CsrfTokenArgumentResolver@ControllerAdvice。以下是一个适用于应用中所有 controller 端点的 @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 时,这通常不是问题。

重要的是要记住,controller 端点和 controller advice 在 Spring Security 过滤器链之后调用。这意味着只有当请求通过过滤器链到达你的应用时,此 @ControllerAdvice 才会应用。请参阅单页应用的配置,了解如何在过滤器链中添加过滤器以更早访问 HttpServletResponse 的示例。

现在,对于 controller advice 适用的任何自定义端点,CSRF 令牌将在响应头中(默认是X-CSRF-TOKENX-XSRF-TOKEN)可用。对后端的任何请求都可以用于从响应中获取令牌,随后的请求可以在同名请求头中包含该令牌。

移动应用

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

然而,既提供浏览器流量又因此仍需要 CSRF 防护的后端应用可能会继续将 CsrfToken 存储在会话 (session) 中,而不是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

Multipart(文件上传)

我们已经讨论了如何保护 multipart 请求(文件上传)免受 CSRF 攻击会导致一个先有鸡还是先有蛋问题。当 JavaScript 可用时,我们推荐在 HTTP 请求头中包含 CSRF 令牌以规避此问题。

如果 JavaScript 不可用,以下小节将讨论在 Servlet 应用程序中将 CSRF 令牌放在请求体URL中的选项。

更多关于在 Spring 中使用 multipart 表单的信息可以在 Spring 参考文档的Multipart 解析器小节和MultipartFilter javadoc中找到。

将 CSRF 令牌放在请求体中

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

为了从请求体中读取 CSRF 令牌,MultipartFilter 需要在 Spring Security 过滤器之前指定。在 Spring Security 过滤器之前指定 MultipartFilter 意味着调用 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>

为了确保在使用 XML 配置时,MultipartFilter 在 Spring Security 过滤器之前指定,你可以确保 MultipartFilter<filter-mapping> 元素放置在 web.xml 文件中 springSecurityFilterChain 之前。

在 URL 中包含 CSRF 令牌

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

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

HiddenHttpMethodFilter

我们已经讨论了将 CSRF 令牌放在请求体中的权衡。

在 Spring 的 Servlet 支持中,覆盖 HTTP 方法是通过使用HiddenHttpMethodFilter完成的。更多信息请参阅参考文档的HTTP 方法转换小节。

延伸阅读

现在你已经回顾了 CSRF 保护,可以考虑深入了解漏洞利用防护,包括安全头部HTTP 防火墙,或者继续学习如何测试你的应用程序。