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

跨站点请求伪造 (CSRF)

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

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

了解 CSRF 保护的组件

CSRF 保护由 CsrfFilter 中组成的几个组件提供:spring-doc.cn

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

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

  1. 通过委托给 CsrfTokenRequestHandler,使 CsrfToken 对应用程序可用。spring-doc.cn

  2. 确定请求是否需要 CSRF 保护,加载并验证令牌,并处理 AccessDeniedExceptionspring-doc.cn

CSRF 处理
图 2. 加工CsrfFilter
  • 数字 1首先,加载 DeferredCsrfToken,它包含对 CsrfTokenRepository 的引用,以便以后可以加载持久化的 (in编号 4)。CsrfTokenspring-doc.cn

  • 编号 2其次,将 (创建自 ) 提供给 CsrfTokenRequestHandler,它负责填充请求属性以使其可用于应用程序的其余部分。Supplier<CsrfToken>DeferredCsrfTokenCsrfTokenspring-doc.cn

  • 编号 3接下来,主 CSRF 保护处理开始并检查当前请求是否需要 CSRF 保护。如果不需要,则继续筛选链并结束处理。spring-doc.cn

  • 编号 4如果需要 CSRF 保护,则持久化最终从 .CsrfTokenDeferredCsrfTokenspring-doc.cn

  • 号码 5继续,使用 CsrfTokenRequestHandler 解析客户端提供的实际 CSRF 令牌(如果有)。spring-doc.cn

  • 数字 6将实际的 CSRF 令牌与持久化的 .如果有效,则继续筛选链并结束处理。CsrfTokenspring-doc.cn

  • 编号 7如果实际的 CSRF 令牌无效(或缺失),则将 an 传递给 AccessDeniedHandler 并结束处理。AccessDeniedExceptionspring-doc.cn

迁移到 Spring Security 6

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

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

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

持久化CsrfToken

使用 .CsrfTokenCsrfTokenRepositoryspring-doc.cn

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

使用HttpSessionCsrfTokenRepository

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

默认情况下,该 API 从名为 的 HTTP 请求标头或请求参数中读取令牌。HttpSessionCsrfTokenRepositoryX-CSRF-TOKEN_csrfspring-doc.cn

您可以使用以下配置显式指定默认配置:spring-doc.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 将 内容保存在 Cookie 中以支持基于 JavaScript 的应用程序CsrfTokenspring-doc.cn

默认情况下,写入名为 的 Cookie,并从名为 的 HTTP 请求标头或请求参数中读取该 Cookie。 这些默认值来自 Angular 及其前身 AngularJS。CookieCsrfTokenRepositoryXSRF-TOKENX-XSRF-TOKEN_csrfspring-doc.cn

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

您可以使用以下配置进行配置:CookieCsrfTokenRepositoryspring-doc.cn

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

自定义CsrfTokenRepository

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

实现接口后,可以将 Spring Security 配置为通过以下配置使用它:CsrfTokenRepositoryspring-doc.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

可供使用 的应用程序使用 . 此组件还负责解析 from HTTP 标头或请求参数。CsrfTokenCsrfTokenRequestHandlerCsrfTokenspring-doc.cn

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

使用 (BREACH)XorCsrfTokenRequestAttributeHandler

使 作为名为 的属性提供,并额外为 BREACH 提供保护。XorCsrfTokenRequestAttributeHandlerCsrfTokenHttpServletRequest_csrfspring-doc.cn

该 还使用 name 作为 request 属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfXorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNamespring-doc.cn

此实现还将请求中的令牌值解析为请求标头(默认为 X-CSRF-TOKENX-XSRF-TOKEN 之一)或请求参数(默认)。_csrfspring-doc.cn

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

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

使 作为名为 .CsrfTokenRequestAttributeHandlerCsrfTokenHttpServletRequest_csrfspring-doc.cn

该 还使用 name 作为 request 属性提供。 此名称不可配置,但可以使用 更改名称。CsrfTokenCsrfToken.class.getName()_csrfCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeNamespring-doc.cn

此实现还将请求中的令牌值解析为请求标头(默认为 X-CSRF-TOKENX-XSRF-TOKEN 之一)或请求参数(默认)。_csrfspring-doc.cn

的主要用途是选择退出 的 BREACH 保护,可以使用以下配置进行配置:CsrfTokenRequestAttributeHandlerCsrfTokenspring-doc.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

您可以实施该接口来自定义处理和解析令牌的策略。CsrfTokenRequestHandlerspring-doc.cn

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

实现接口后,可以将 Spring Security 配置为通过以下配置使用它:CsrfTokenRequestHandlerspring-doc.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 会延迟加载,直到需要为止。CsrfTokenspring-doc.cn

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

因为 Spring Security 默认情况下也存储在 中,所以延迟的 CSRF 令牌可以通过不要求在每个请求上加载会话来提高性能。CsrfTokenHttpSessionspring-doc.cn

如果您想选择退出延迟令牌并导致在每个请求上加载 ,则可以使用以下配置来实现:CsrfTokenspring-doc.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>

通过设置 to ,必须首先加载 以确定要使用的属性名称。 这会导致 to 在每个请求上加载。csrfRequestAttributeNamenullCsrfTokenCsrfTokenspring-doc.cn

与 CSRF 保护集成

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

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

HTML 表单

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

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

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

如果这些选项不可用,则可以利用 作为名为 _csrfHttpServletRequest 属性公开的事实。 以下示例使用 JSP 执行此操作:CsrfTokenspring-doc.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.cn

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

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

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

单页应用程序

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

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

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

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

为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:spring-doc.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 配置并设置为 ,以便 JavaScript 应用程序可以读取 Cookie。CookieCsrfTokenRepositoryHttpOnlyfalse
2 配置一个自定义,根据它是 HTTP 请求标头 () 还是请求参数 () 来解析 CSRF 令牌。CsrfTokenRequestHandlerX-XSRF-TOKEN_csrf
3 配置自定义以在每个请求上加载,如果需要,它将返回新的 Cookie。FilterCsrfToken

多页应用程序

对于在每个页面上加载 JavaScript 的多页面应用程序,在 Cookie 中公开 CSRF 令牌的替代方法是将 CSRF 令牌包含在标签中。 HTML 可能如下所示:metaspring-doc.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 令牌,你可以利用 作为名为 _csrfHttpServletRequest 属性公开的事实。 以下示例使用 JSP 执行此操作:CsrfTokenspring-doc.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.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.cn

实现此目的的一种方法是将 a 与 CsrfTokenArgumentResolver 一起使用。 下面是一个适用于应用程序中的所有控制器终结点的示例:@ControllerAdvice@ControllerAdvicespring-doc.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)
	}

}

因为这适用于应用程序中的所有端点,所以它会导致在每个请求上加载 CSRF 令牌,这可能会抵消使用 HttpSessionCsrfTokenRepository 时延迟令牌的好处。 但是,在使用 CookieCsrfTokenRepository 时,这通常不是问题。@ControllerAdvicespring-doc.cn

重要的是要记住,控制器端点和控制器建议是在 Spring Security 过滤器链之后调用的。 这意味着,仅当请求通过过滤器链传递到您的应用程序时,才会应用此功能。 有关向过滤器链添加过滤器以便提前访问 .@ControllerAdviceHttpServletResponsespring-doc.cn

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

移动应用程序

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

但是,也提供浏览器流量并因此仍然需要 CSRF 保护的后端应用程序可能会继续将 存储在会话中,而不是存储在 cookie 中CsrfTokenspring-doc.cn

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

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

以下是使用 CsrfTokenArgumentResolver 的终端节点示例:/csrfspring-doc.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.cn

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

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

获得 CSRF 令牌后,您需要自己将其作为 HTTP 请求标头(默认为 X-CSRF-TOKENX-XSRF-TOKEN 之一)包含在内。spring-doc.cn

处理AccessDeniedException

要处理诸如 之类的异常,可以将 Spring Security 配置为以您喜欢的任何方式处理这些异常。 例如,您可以使用以下配置配置自定义 access denied 页面:AccessDeniedExceptionInvalidCsrfTokenExceptionspring-doc.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.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
	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 保护之前,请考虑它对您的应用程序是否有意义spring-doc.cn

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

登录

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

注销

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

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

如果您确实想将 HTTP GET 与 logout 一起使用,则可以这样做。 但是,请记住,这通常不建议这样做。 例如,当使用任何 HTTP 方法请求 URL 时,以下将注销:/logoutspring-doc.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.cn

CSRF 和会话超时

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

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

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

如果令牌确实过期,您可能希望通过指定自定义 AccessDeniedHandler 来自定义其处理方式。 自定义可以按照您喜欢的任何方式进行处理。AccessDeniedHandlerInvalidCsrfTokenExceptionspring-doc.cn

分段(文件上传)

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

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

您可以在 Spring 参考的 Multipart Resolver 部分和 MultipartFilter javadoc 中找到有关在 Spring 中使用多部分形式的更多信息。spring-doc.cn

将 CSRF 令牌放在正文中

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

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

要确保在使用 XML 配置的 Spring Security 过滤器之前指定,可以确保将 the 的元素放在文件中的 the 之前。MultipartFilter<filter-mapping>MultipartFilterspringSecurityFilterChainweb.xmlspring-doc.cn

在 URL 中包含 CSRF 令牌

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

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

隐藏的HttpMethodFilter

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

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

延伸阅读

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